import {
  includes,
  isNumber,
  isObject,
  isString,
  isNullOrUndefined,
  propertyValueConverters,
} from 'rtl-css-js/core.esm'

// this will be an object of properties that map to their corresponding rtl property (their doppelganger)
const propertiesToConvert = flowRelativeDirectionMapper(
  ['paddingStart', 'paddingLeft', 'paddingRight'],
  ['paddingEnd', 'paddingRight', 'paddingLeft'],
  ['marginStart', 'marginLeft', 'marginRight'],
  ['marginEnd', 'marginRight', 'marginLeft'],
  ['paddingInlineStart', 'paddingLeft', 'paddingRight'],
  ['paddingInlineEnd', 'paddingRight', 'paddingLeft'],
  ['marginInlineStart', 'marginLeft', 'marginRight'],
  ['marginInlineEnd', 'marginRight', 'marginLeft'],
  ['insetInlineStart', 'left', 'right'],
  ['insetInlineEnd', 'right', 'left'],
  ['start', 'left', 'right'],
  ['end', 'right', 'left'],
  ['borderStart', 'borderLeft', 'borderRight'],
  ['borderEnd', 'borderRight', 'borderLeft'],
  ['borderStartColor', 'borderLeftColor', 'borderRightColor'],
  ['borderEndColor', 'borderRightColor', 'borderLeftColor'],
  ['borderStartStyle', 'borderLeftStyle', 'borderRightStyle'],
  ['borderEndStyle', 'borderRightStyle', 'borderLeftStyle'],
  ['borderStartWidth', 'borderLeftWidth', 'borderRightWidth'],
  ['borderEndWidth', 'borderRightWidth', 'borderLeftWidth'],
  ['borderInlineStart', 'borderLeft', 'borderRight'],
  ['borderInlineEnd', 'borderRight', 'borderLeft'],
  ['borderInlineStartColor', 'borderLeftColor', 'borderRightColor'],
  ['borderInlineEndColor', 'borderRightColor', 'borderLeftColor'],
  ['borderInlineStartStyle', 'borderLeftStyle', 'borderRightStyle'],
  ['borderInlineEndStyle', 'borderRightStyle', 'borderLeftStyle'],
  ['borderInlineStartWidth', 'borderLeftWidth', 'borderRightWidth'],
  ['borderInlineEndWidth', 'borderRightWidth', 'borderLeftWidth'],
  ['borderTopStartRadius', 'borderTopLeftRadius', 'borderTopRightRadius'],
  ['borderTopEndRadius', 'borderTopRightRadius', 'borderTopLeftRadius'],
  [
    'borderBottomStartRadius',
    'borderBottomLeftRadius',
    'borderBottomRightRadius',
  ],
  [
    'borderBottomEndRadius',
    'borderBottomRightRadius',
    'borderBottomLeftRadius',
  ],
  ['borderStartStartRadius', 'borderTopLeftRadius', 'borderTopRightRadius'],
  ['borderStartEndRadius', 'borderTopRightRadius', 'borderTopLeftRadius'],
  ['borderEndStartRadius', 'borderBottomLeftRadius', 'borderBottomRightRadius'],
  ['borderEndEndRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius'],
)

// The properties in this list either natively support flow-relative authoring
// or should just not be touched (I'm looking at you `content`)
const propertiesToIgnore = [
  'justifyContent',
  'justifyItems',
  'justifySelf',
  'alignContent',
  'alignItems',
  'alignSelf',
  'grid',
  'gridColumnStart',
  'gridColumnEnd',
  'gridRowStart',
  'gridRowEnd',
  'gridColumn',
  'gridRow',
  'gridArea',
  'gridTemplateColumns',
  'gridTemplateRows',
  'gridTemplate',
  'gridTemplateAreas',
  'content',
]

// this is the same as the propertiesToConvert except for values
const valuesToConvert = flowRelativeDirectionMapper(
  ['ste', 'ltr', 'rtl'],
  ['ets', 'rtl', 'ltr'],
  ['start', 'left', 'right'],
  ['end', 'right', 'left'],
  ['inline-start', 'left', 'right'],
  ['inline-end', 'right', 'left'],
  ['start-resize', 'w-resize', 'e-resize'],
  ['end-resize', 'e-resize', 'w-resize'],
  ['bottomstart-resize', 'sw-resize', 'se-resize'],
  ['bottomend-resize', 'se-resize', 'sw-resize'],
  ['topstart-resize', 'nw-resize', 'ne-resize'],
  ['topend-resize', 'ne-resize', 'nw-resize'],
)

const canHaveLogical = [
  'background',
  'backgroundImage',
  'backgroundPosition',
  'backgroundPositionX',
  'borderColor',
  'borderRadius',
  'borderStyle',
  'borderWidth',
  'boxShadow',
  'mozBoxShadow',
  'webkitBoxShadow',
  'MozBoxShadow',
  'WebkitBoxShadow',
  'margin',
  'padding',
  'textShadow',
  'transform',
  'mozTransform',
  'webkitTransform',
  'MozTransform',
  'WebkitTransform',
  'transformOrigin',
  'mozTransformOrigin',
  'webkitTransformOrigin',
  'MozTransformOrigin',
  'WebkitTransformOrigin',
]

// This regex is used to replace _every_ instance of `ste`, `ets`, `start`,
// and `end` in `backgroundimage` with the corresponding logical value,
// based on the flow direction.
// A situation we're accepting here:
// url('/start/end/ste/ets.png') will be changed to
// url('/left/right/ltr/rtl.png') when in `ltr` mode
const bgImgDirectionRegex = new RegExp(
  '(^|\\W|_)((ste)|(ets)|(start)|(end))(\\W|_|$)',
  'g',
)
const bgPosDirectionRegex = new RegExp('(start)|(end)')

/**
 * Logically converts properties and values in the CSS in JS object
 * based on the flow direction context.
 * @param {Object} object The CSS in JS object
 * @param {'rtl'|'ltr'} flowDirection The logical flow direction
 * @return {Object} The converted CSS in JS object
 */
export default function convert(object, flowDirection) {
  const isRtl =
    typeof flowDirection === 'string' && flowDirection.toLowerCase() === 'rtl'
  const isLtr =
    typeof flowDirection === 'string' && flowDirection.toLowerCase() === 'ltr'

  if (!isRtl && !isLtr) {
    throw new Error(
      `"flowDirection" must either be "rtl" or "ltr", you are trying to pass "${flowDirection}"`,
    )
  }

  return Object.keys(object).reduce((newObj, originalKey) => {
    let originalValue = object[originalKey]

    if (isString(originalValue)) {
      // you're welcome to later code 😺
      originalValue = originalValue.trim()
    }

    // Don't touch values that shouldn't be transformed.
    if (propertiesToIgnore.includes(originalKey)) {
      return {...newObj, ...{[originalKey]: originalValue}}
    }

    // Try to convert otherwise
    const {key, value} = convertProperty(originalKey, originalValue, isRtl)
    newObj[key] = value
    return newObj
  }, {})
}

/**
 * Logically converts a property and its value based on the flow direction context
 * @param {String} originalKey the original property key
 * @param {Number|String|Object} originalValue the original css property value
 * @param {Boolean} isRtl Should conversion happen in RTL context?
 * @return {Object} the new {key, value} pair
 */
function convertProperty(originalKey, originalValue, isRtl) {
  const key = getPropertyDoppelganger(originalKey, isRtl)
  const value = getValueDoppelganger(key, originalValue, isRtl)
  return {key, value}
}

/**
 * Logically gets the direction of the given property based on the flow direction context
 * @param {String} property the name of the property
 * @param {Boolean} isRtl Should conversion happen in RTL context?
 * @return {String} the name of the RTL property
 */
function getPropertyDoppelganger(property, isRtl) {
  const convertedProperty = isRtl
    ? propertiesToConvert.rtl[property]
    : propertiesToConvert.ltr[property]

  return convertedProperty || property
}

// let's try to keep the complexity down...
// If we have to do this much more, let's break this up
/* eslint-disable complexity */
/**
 * Logically converts the given value to the correct version based on the key and flow direction context
 * @param {String} key this is the key (note: this should be the RTL version of the originalKey)
 * @param {String|Number|Object} originalValue the original css property value. If it's an object, then we'll convert that as well
 * @param {Boolean} isRtl Should conversion happen in RTL context?
 * @return {String|Number|Object} the converted value
 */
function getValueDoppelganger(key, originalValue, isRtl) {
  if (isNullOrUndefined(originalValue)) {
    return originalValue
  }

  const flowDirection = isRtl ? 'rtl' : 'ltr'

  if (isObject(originalValue)) {
    return convert(originalValue, flowDirection) // recurssion 🌀
  }

  const {
    isLogical,
    logicallessValue,
    isImportant,
    importantlessValue,
  } = analyzeOriginalValue(originalValue)

  if (canHaveLogical.includes(key) && !isLogical) {
    return originalValue
  }

  const isFourDirectionalShorthand = includes(
    [
      'margin',
      'padding',
      'borderColor',
      'borderRadius',
      'borderStyle',
      'borderWidth',
    ],
    key,
  )

  if (
    isLogical &&
    !isRtl &&
    !isFourDirectionalShorthand &&
    !key.match(/(background)|((t|T)ransformOrigin)/)
  ) {
    return logicallessValue
  }

  const conversionMap = valuesToConvert[flowDirection]

  // The logical props and values changes the default way you write four-directional shorhands
  // so that the order of values is `block-start`, `inline-start`, `block-end` and `inline-end`, which,
  // for the `inline-*` sides, is the opposite of how they are written without the `logical` keyword
  if (isLogical && isFourDirectionalShorthand) {
    return isRtl
      ? logicallessValue
      : convertValues(
          key,
          importantlessValue,
          conversionMap,
          isImportant,
          // Reversing `isRtl` like this is crooked, but for the time being, it is the easiest way to
          // address how values of four-directional shorthand properties with the `logical` keyword
          // should be handled according to the spec.
          !isRtl,
        )
  }

  return convertValues(
    key,
    importantlessValue,
    conversionMap,
    isImportant,
    isRtl,
  )
}
/* eslint-enable complexity */

function analyzeOriginalValue(originalValue) {
  const isNum = isNumber(originalValue)
  const logicallessValue = isNum
    ? originalValue
    : originalValue.replace(/^\s*logical\s*/i, '')
  const isLogical = !isNum && logicallessValue.length !== originalValue.length
  const importantlessValue = isNum
    ? logicallessValue
    : logicallessValue.replace(/\s*!important.*?$/, '')
  const isImportant =
    !isNum && importantlessValue.length !== logicallessValue.length

  return {isLogical, logicallessValue, isImportant, importantlessValue}
}

function convertValues(key, value, conversionMap, isImportant, isRtl) {
  const valueConverter = propertyValueConverters[key]
  const newValue = valueConverter
    ? valueConverter({
        value,
        valuesToConvert: conversionMap,
        isRtl,
        bgImgDirectionRegex,
        bgPosDirectionRegex,
      })
    : conversionMap[value] || value

  return isImportant ? `${newValue} !important` : newValue
}

function flowRelativeDirectionMapper(...logicanDirectionsMap) {
  return logicanDirectionsMap.reduce(
    (all, [origValue, ltrValue, rtlValue]) => ({
      ltr: {
        ...all.ltr,
        ...{
          [origValue]: ltrValue,
        },
      },
      rtl: {
        ...all.rtl,
        ...{
          [origValue]: rtlValue,
        },
      },
    }),
    {ltr: {}, rtl: {}},
  )
}


var assert = require("assert");
var path = require("path");
var fs = require("graceful-fs");
var spawn = require("child_process").spawn;
var Q = require("q");
var EventEmitter = require("events").EventEmitter;
var ReadFileCache = require("./cache").ReadFileCache;
var util = require("./util");
var hasOwn = Object.prototype.hasOwnProperty;

function Watcher(readFileCache, persistent) {
    assert.ok(this instanceof Watcher);
    assert.ok(this instanceof EventEmitter);
    assert.ok(readFileCache instanceof ReadFileCache);

    // During tests (and only during tests), persistent === false so that
    // the test suite can actually finish and exit.
    if (typeof persistent === "undefined") {
        persistent = true;
    }

    EventEmitter.call(this);

    var self = this;
    var sourceDir = readFileCache.sourceDir;
    var dirWatcher = new DirWatcher(sourceDir, persistent);

    Object.defineProperties(self, {
        sourceDir: { value: sourceDir },
        readFileCache: { value: readFileCache },
        dirWatcher: { value: dirWatcher }
    });

    // Watch everything the readFileCache already knows about, and any new
    // files added in the future.
    readFileCache.subscribe(function(relativePath) {
        self.watch(relativePath);
    });

    readFileCache.on("changed", function(relativePath) {
        self.emit("changed", relativePath);
    });

    function handleDirEvent(event, relativePath) {
        if (self.dirWatcher.ready) {
            self.getFileHandler(relativePath)(event);
        }
    }

    dirWatcher.on("added", function(relativePath) {
        handleDirEvent("added", relativePath);
    }).on("deleted", function(relativePath) {
        handleDirEvent("deleted", relativePath);
    }).on("changed", function(relativePath) {
        handleDirEvent("changed", relativePath);
    });
}

util.inherits(Watcher, EventEmitter);
var Wp = Watcher.prototype;

Wp.watch = function(relativePath) {
    this.dirWatcher.add(path.dirname(path.join(
        this.sourceDir, relativePath)));
};

Wp.readFileP = function(relativePath) {
    return this.readFileCache.readFileP(relativePath);
};

Wp.noCacheReadFileP = function(relativePath) {
    return this.readFileCache.noCacheReadFileP(relativePath);
};

Wp.getFileHandler = util.cachedMethod(function(relativePath) {
    var self = this;
    return function handler(event) {
        self.readFileCache.reportPossiblyChanged(relativePath);
    };
});

function orNull(err) {
    return null;
}

Wp.close = function() {
    this.dirWatcher.close();
};

/**
 * DirWatcher code adapted from Jeffrey Lin's original implementation:
 * https://github.com/jeffreylin/jsx_transformer_fun/blob/master/dirWatcher.js
 *
 * Invariant: this only watches the dir inode, not the actual path.
 * That means the dir can't be renamed and swapped with another dir.
 */
function DirWatcher(inputPath, persistent) {
    assert.ok(this instanceof DirWatcher);

    var self = this;
    var absPath = path.resolve(inputPath);

    if (!fs.statSync(absPath).isDirectory()) {
        throw new Error(inputPath + "is not a directory!");
    }

    EventEmitter.call(self);

    self.ready = false;
    self.on("ready", function(){
        self.ready = true;
    });

    Object.defineProperties(self, {
        // Map of absDirPaths to fs.FSWatcher objects from fs.watch().
        watchers: { value: {} },
        dirContents: { value: {} },
        rootPath: { value: absPath },
        persistent: { value: !!persistent }
    });

    process.nextTick(function() {
        self.add(absPath);
        self.emit("ready");
    });
}

util.inherits(DirWatcher, EventEmitter);
var DWp = DirWatcher.prototype;

DWp.add = function(absDirPath) {
    var self = this;
    if (hasOwn.call(self.watchers, absDirPath)) {
        return;
    }

    self.watchers[absDirPath] = fs.watch(absDirPath, {
        persistent: this.persistent
    }).on("change", function(event, filename) {
        self.updateDirContents(absDirPath, event, filename);
    });

    // Update internal dir contents.
    self.updateDirContents(absDirPath);

    // Since we've never seen this path before, recursively add child
    // directories of this path.  TODO: Don't do fs.readdirSync on the
    // same dir twice in a row.  We already do an fs.statSync in
    // this.updateDirContents() and we're just going to do another one
    // here...
    fs.readdirSync(absDirPath).forEach(function(filename) {
        var filepath = path.join(absDirPath, filename);

        // Look for directories.
        if (fs.statSync(filepath).isDirectory()) {
            self.add(filepath);
        }
    });
};

DWp.updateDirContents = function(absDirPath, event, fsWatchReportedFilename) {
    var self = this;

    if (!hasOwn.call(self.dirContents, absDirPath)) {
        self.dirContents[absDirPath] = [];
    }

    var oldContents = self.dirContents[absDirPath];
    var newContents = fs.readdirSync(absDirPath);

    var deleted = {};
    var added = {};

    oldContents.forEach(function(filename) {
        deleted[filename] = true;
    });

    newContents.forEach(function(filename) {
        if (hasOwn.call(deleted, filename)) {
            delete deleted[filename];
        } else {
            added[filename] = true;
        }
    });

    var deletedNames = Object.keys(deleted);
    deletedNames.forEach(function(filename) {
        self.emit(
            "deleted",
            path.relative(
                self.rootPath,
                path.join(absDirPath, filename)
            )
        );
    });

    var addedNames = Object.keys(added);
    addedNames.forEach(function(filename) {
        self.emit(
            "added",
            path.relative(
                self.rootPath,
                path.join(absDirPath, filename)
            )
        );
    });

    // So changed is not deleted or added?
    if (fsWatchReportedFilename &&
        !hasOwn.call(deleted, fsWatchReportedFilename) &&
        !hasOwn.call(added, fsWatchReportedFilename))
    {
        self.emit(
            "changed",
            path.relative(
                self.rootPath,
                path.join(absDirPath, fsWatchReportedFilename)
            )
        );
    }

    // If any of the things removed were directories, remove their watchers.
    // If a dir was moved, hopefully two changed events fired?
    //  1) event in dir where it was removed
    //  2) event in dir where it was moved to (added)
    deletedNames.forEach(function(filename) {
        var filepath = path.join(absDirPath, filename);
        delete self.dirContents[filepath];
        delete self.watchers[filepath];
    });

    // if any of the things added were directories, recursively deal with them
    addedNames.forEach(function(filename) {
        var filepath = path.join(absDirPath, filename);
        if (fs.existsSync(filepath) &&
            fs.statSync(filepath).isDirectory())
        {
            self.add(filepath);
            // mighttttttt need a self.updateDirContents() here in case
            // we're somehow adding a path that replaces another one...?
        }
    });

    // Update state of internal dir contents.
    self.dirContents[absDirPath] = newContents;
};

DWp.close = function() {
    var watchers = this.watchers;
    Object.keys(watchers).forEach(function(filename) {
        watchers[filename].close();
    });
};

exports.Watcher = Watcher;


// The purpose of the `Content` object is to abstract away the data conversions
// to and from raw content entities as strings. For example, you want to be able
// to pass in a Javascript object and have it be automatically converted into a
// JSON string if the `content-type` is set to a JSON-based media type.
// Conversely, you want to be able to transparently get back a Javascript object
// in the response if the `content-type` is a JSON-based media-type.

// One limitation of the current implementation is that it [assumes the `charset` is UTF-8](https://github.com/spire-io/shred/issues/5).

// The `Content` constructor takes an options object, which *must* have either a
// `body` or `data` property and *may* have a `type` property indicating the
// media type. If there is no `type` attribute, a default will be inferred.
var Content = function(options) {
  this.body = options.body;
  this.data = options.data;
  this.type = options.type;
};

Content.prototype = {
  // Treat `toString()` as asking for the `content.body`. That is, the raw content entity.
  //
  //     toString: function() { return this.body; }
  //
  // Commented out, but I've forgotten why. :/
};

// `Content` objects have the following attributes:
Object.defineProperties(Content.prototype,{

// - **type**. Typically accessed as `content.type`, reflects the `content-type`
//   header associated with the request or response. If not passed as an options
//   to the constructor or set explicitly, it will infer the type the `data`
//   attribute, if possible, and, failing that, will default to `text/plain`.
  type: {
    get: function() {
      if (this._type) {
        return this._type;
      } else {
        if (this._data) {
          switch(typeof this._data) {
            case "string": return "text/plain";
            case "object": return "application/json";
          }
        }
      }
      return "text/plain";
    },
    set: function(value) {
      this._type = value;
      return this;
    },
    enumerable: true
  },

// - **data**. Typically accessed as `content.data`, reflects the content entity
//   converted into Javascript data. This can be a string, if the `type` is, say,
//   `text/plain`, but can also be a Javascript object. The conversion applied is
//   based on the `processor` attribute. The `data` attribute can also be set
//   directly, in which case the conversion will be done the other way, to infer
//   the `body` attribute.
  data: {
    get: function() {
      if (this._body) {
        return this.processor.parser(this._body);
      } else {
        return this._data;
      }
    },
    set: function(data) {
      if (this._body&&data) Errors.setDataWithBody(this);
      this._data = data;
      return this;
    },
    enumerable: true
  },

// - **body**. Typically accessed as `content.body`, reflects the content entity
//   as a UTF-8 string. It is the mirror of the `data` attribute. If you set the
//   `data` attribute, the `body` attribute will be inferred and vice-versa. If
//   you attempt to set both, an exception is raised.
  body: {
    get: function() {
      if (this._data) {
        return this.processor.stringify(this._data);
      } else {
        return this._body.toString();
      }
    },
    set: function(body) {
      if (this._data&&body) Errors.setBodyWithData(this);
      this._body = body;
      return this;
    },
    enumerable: true
  },

// - **processor**. The functions that will be used to convert to/from `data` and
//   `body` attributes. You can add processors. The two that are built-in are for
//   `text/plain`, which is basically an identity transformation and
//   `application/json` and other JSON-based media types (including custom media
//   types with `+json`). You can add your own processors. See below.
  processor: {
    get: function() {
      var processor = Content.processors[this.type];
      if (processor) {
        return processor;
      } else {
        // Return the first processor that matches any part of the
        // content type. ex: application/vnd.foobar.baz+json will match json.
        var main = this.type.split(";")[0];
        var parts = main.split(/\+|\//);
        for (var i=0, l=parts.length; i < l; i++) {
          processor = Content.processors[parts[i]]
        }
        return processor || {parser:identity,stringify:toString};
      }
    },
    enumerable: true
  },

// - **length**. Typically accessed as `content.length`, returns the length in
//   bytes of the raw content entity.
  length: {
    get: function() {
      if (typeof Buffer !== 'undefined') {
        return Buffer.byteLength(this.body);
      }
      return this.body.length;
    }
  }
});

Content.processors = {};

// The `registerProcessor` function allows you to add your own processors to
// convert content entities. Each processor consists of a Javascript object with
// two properties:
// - **parser**. The function used to parse a raw content entity and convert it
//   into a Javascript data type.
// - **stringify**. The function used to convert a Javascript data type into a
//   raw content entity.
Content.registerProcessor = function(types,processor) {

// You can pass an array of types that will trigger this processor, or just one.
// We determine the array via duck-typing here.
  if (types.forEach) {
    types.forEach(function(type) {
      Content.processors[type] = processor;
    });
  } else {
    // If you didn't pass an array, we just use what you pass in.
    Content.processors[types] = processor;
  }
};

// Register the identity processor, which is used for text-based media types.
var identity = function(x) { return x; }
  , toString = function(x) { return x.toString(); }
Content.registerProcessor(
  ["text/html","text/plain","text"],
  { parser: identity, stringify: toString });

// Register the JSON processor, which is used for JSON-based media types.
Content.registerProcessor(
  ["application/json; charset=utf-8","application/json","json"],
  {
    parser: function(string) {
      return JSON.parse(string);
    },
    stringify: function(data) {
      return JSON.stringify(data); }});

var qs = require('querystring');
// Register the post processor, which is used for JSON-based media types.
Content.registerProcessor(
  ["application/x-www-form-urlencoded"],
  { parser : qs.parse, stringify : qs.stringify });

// Error functions are defined separately here in an attempt to make the code
// easier to read.
var Errors = {
  setDataWithBody: function(object) {
    throw new Error("Attempt to set data attribute of a content object " +
        "when the body attributes was already set.");
  },
  setBodyWithData: function(object) {
    throw new Error("Attempt to set body attribute of a content object " +
        "when the data attributes was already set.");
  }
}
module.exports = Content;


module.exports = function () {
    var router  = require('./util/router'),
        util    = require('util'),
        tts     = require('./ttsapi').tts,
        format  = '.{format}',
        voice   = 'Alex',
        voices  = ['Agnes','Albert','Alex','Bad','Bahh','Bells','Boing','Bruce','Bubbles','Cellos','Deranged','Fred','Good','Hysterical','Junior','Kathy','Pipe','Princess','Ralph','Trinoids','Vicki','Victoria','Whisper','Zarvox'],
        winston = require('winston'),
        logger  = new (winston.Logger)({ transports: [
            new (winston.transports.Console)({
                "level"    : "debug",
                "json"     : false,
                "colorize" : true
            })
        ]}),
        meta    = {
            "module" : "routes",
            "pid"    : process.pid,
        };

    logger.log('debug', '%s|loading|module="%s"', meta.module, meta.module, meta);

    function fix(err) {
        err.message = err.reason;
        return err;
    }

    /**
     * initialize the routes that the Express Application will serve
     *
     * @param app: an Express Application object
     * @param context: All the URL will start with that context prefix (e.g.
     *                 "/api/..." or "/webservice/...")
     */
    function init(app, context, swagger) {
        logger.log('debug', '%s|adding|routes|context=%s' + (swagger ? "|SWAGGER" : ""), meta.module, context, meta);
        if (swagger) {
            describeModels(swagger);
            swagger.addGET({
                'spec': {
                    "description" : "Text To Speech REST API",
                    "path" : context + format + '/play/{voice}/{text}',
                    "notes" : "The REST API /play/ transform a text phrase into a spoken audio stream playable through an HTML5 <audio> element. You can pre-generate the audio by calling the REST API /generate/ before calling this one, to have the audio start playing as soon as you call the /play/ API.",
                    "method": "GET",
                    "summary" : "Transform a text phrase into an Audio Stream.",
                    "nickname" : "play",
                    "responseClass" : "BinaryAudioStream",
                    "produces" : ["audio/mp4","application/json"],
                    "params" : [
                        swagger.params.path(
                            "voice",
                            "A 'human' voice to use, to speak the phrase",
                            "string",
                            {"values": voices,"valueType":"LIST"},
                            "Alex"
                        ),
                        swagger.params.path("text", "The text phrase to be spoken.", "string")
                    ],
                    "errorResponses" : [
                        fix(swagger.errors.notFound('voice')),
                        fix(swagger.errors.notFound('text')),
                        fix(swagger.errors.invalid('voice'))
                    ]
                },
                'action': function(req, res) {
                    logger.log('debug', '%s|say|voice=%s|text=%s', meta.module, req.params.voice, req.params.text, meta);
                    if (voices.indexOf(req.params.voice) < 0) {
                        swagger.stopWithError(res, {code:400,reason:'The voice ' + req.params.voice + ' is not supported'});
                        return;
                    }
                    tts.play(req.param('text', 'No text passed'),
                                req.param('voice', voice),
                                function (err, data) {
                                    if (err) {
                                        if (!err.code || !err.reason)
                                            err = {code : 500, reason:util.inspect(err)};
                                        swagger.stopWithError(res, err);
                                    } else {
                                        res.writeHead(200, {'Content-Type': 'audio/mp4'});
                                        res.end(data);
                                    }
                                });
                }
            }).addPost({
                'spec': {
                    "description" : "Text To Speech REST API",
                    "path" : context + format + '/generate',
                    "notes" : "To avoid latency, when using the REST API /play/, you can pre-generate the audio on the server by calling this API.",
                    "method": "POST",
                    "summary" : "Generate the audio on the server.",
                    "nickname" : "generate",
                    "responseClass" : "Status",
                    "params" : [
                        swagger.params.body("params", "The text phrase to be pre-generated on the server", 'TextToSpeech', '{"voice" : "Alex", "text":"Hello world", "async" : true}')
                    ],
                    "errorResponses" : [
                        fix(swagger.errors.notFound('voice')),
                        fix(swagger.errors.notFound('text')),
                        fix(swagger.errors.invalid('voice')),
                        fix(swagger.errors.invalid('async'))
                    ]
                },
                'action': function(req, res) {
                    if (!req.body) {
                        swagger.stopWithError(res, {code:400,reason:'The BODY of the request is empty'});
                        return;
                    }
                    logger.log('debug', '%s|generate|voice=%s|text=%s|async=%s', meta.module, req.body.voice, req.body.text, req.body.async, meta);
                    if (voices.indexOf(req.body.voice) < 0) {
                        swagger.stopWithError(res, {code:400,reason:'The voice ' + req.params.voice + ' is not supported'});
                        return;
                    }
                    var async;
                    if (typeof req.body.async != 'undefined') {
                        if (req.body.async === true || req.body.async === 'true') {
                            async = true;
                        } else if (req.body.async === false || req.body.async === 'false') {
                            async = false;
                        } else {
                            swagger.stopWithError(res, {code:400,reason:'The async must be true or false'});
                            return;
                        }
                    } else
                        async = false;

                    tts.play(req.param('text', 'No text passed'),
                        req.param('voice', voice),
                        function (err) {
                            if (async === false) {
                                if (err) {
                                    if (!err.code || !err.reason)
                                        err = {code : 500, reason:util.inspect(err)};
                                    swagger.stopWithError(res, err);
                                } else {
                                    res.writeHead(200, {'Content-Type': 'application/json'});
                                    res.end('{"result":"OK","async":false}');
                                }
                            }
                        });
                        if (async) {
                            res.writeHead(200, {'Content-Type': 'application/json'});
                            res.end('{"result":"OK","async":true}');
                        }
                }
            });
        } else {
            router.add(app, context + '/tts.json/play', 'GET', function (req, res) {
                tts.play(req.param('text', 'No text passed'),
                    req.param('voice', voice),
                    function (err, data) {
                        if (err) {
                            res.writeHead(404, {"Content-Type": "text/html"});
                            res.end('
Unable to generate tts
\n' + err + '
');
                        } else {
                            res.writeHead(200, {'Content-Type': 'audio/mp4'});
                            res.end(data);
                        }
                    });
            });
            router.add(app, context + '/tts.json/generate', 'POST',  function (req, res) {
                var async = req.param('async', 'true') === 'true' ? true : false;
                tts.play(req.param('text', 'No text passed'),
                    req.param('voice', voice),
                    function (err) {
                        if (async === false) {
                            if (err) {
                                res.writeHead(404, {'Content-Type': 'application/json'});
                                res.end('{"result":"FAILED","async":false,"error":' + JSON.stringify(err) + '}');
                            } else {
                                res.writeHead(200, {'Content-Type': 'application/json'});
                                res.end('{"result":"OK","async":false}');
                            }
                        }
                    });
                    if (async) {
                        res.writeHead(200, {'Content-Type': 'application/json'});
                        res.end('{"result":"OK","async":true}');
                    }
            });
        }
        tts.init();
    }

    function describeModels(swagger) {
        swagger.addModels({
            "Status" : {
                "id" : "Status",
                "properties" : {
                    "result" : { "type" : "string", "required" : true },
                    "async"  : { "type" : "boolean", "required" : true }
                }
            },
            "BinaryAudioStream": {
                "id": "BinaryAudioStream",
                "properties": {
                    "AudioStream": {
                        "items": {
                            "type": "byte"
                        },
                        "type": "Array",
                        "required" : true
                    }
                }
            },
            "TextToSpeech" : {
                "id" : "TextToSpeech",
                "properties" : {
                    "voice" : { "type" : "string", "required" : true },
                    "text"  : { "type" : "string", "required" : true },
                    "async" : { "type" : "boolean", "required" : false }
                }
            }
        });
    }

    logger.log('debug', '%s|loaded|module="%s"', meta.module, meta.module, meta);
    return {
        "init"     : init,
    };
}();


var assert = require("assert");
var Q = require("q");
var fs = require("fs");
var path = require("path");
var util = require("./util");
var EventEmitter = require("events").EventEmitter;
var hasOwn = Object.prototype.hasOwnProperty;

/**
 * ReadFileCache is an EventEmitter subclass that caches file contents in
 * memory so that subsequent calls to readFileP return the same contents,
 * regardless of any changes in the underlying file.
 */
function ReadFileCache(sourceDir, charset) {
    assert.ok(this instanceof ReadFileCache);
    assert.strictEqual(typeof sourceDir, "string");

    this.charset = charset;

    EventEmitter.call(this);

    Object.defineProperties(this, {
        sourceDir: { value: sourceDir },
        sourceCache: { value: {} }
    });
}

util.inherits(ReadFileCache, EventEmitter);
var RFCp = ReadFileCache.prototype;

/**
 * Read a file from the cache if possible, else from disk.
 */
RFCp.readFileP = function(relativePath) {
    var cache = this.sourceCache;

    relativePath = path.normalize(relativePath);

    return hasOwn.call(cache, relativePath)
        ? cache[relativePath]
        : this.noCacheReadFileP(relativePath);
};

/**
 * Read (or re-read) a file without using the cache.
 *
 * The new contents are stored in the cache for any future calls to
 * readFileP.
 */
RFCp.noCacheReadFileP = function(relativePath) {
    relativePath = path.normalize(relativePath);

    var added = !hasOwn.call(this.sourceCache, relativePath);
    var promise = this.sourceCache[relativePath] = util.readFileP(
        path.join(this.sourceDir, relativePath), this.charset);

    if (added) {
        this.emit("added", relativePath);
    }

    return promise;
};

/**
 * If you have reason to believe the contents of a file have changed, call
 * this method to re-read the file and compare the new contents to the
 * cached contents.  If the new contents differ from the contents of the
 * cache, the "changed" event will be emitted.
 */
RFCp.reportPossiblyChanged = function(relativePath) {
    var self = this;
    var cached = self.readFileP(relativePath);
    var fresh = self.noCacheReadFileP(relativePath);

    Q.spread([
        cached.catch(orNull),
        fresh.catch(orNull)
    ], function(oldData, newData) {
        if (oldData !== newData) {
            self.emit("changed", relativePath);
        }
    }).done();
};

/**
 * Invoke the given callback for all files currently known to the
 * ReadFileCache, and invoke it in the future when any new files become
 * known to the cache.
 */
RFCp.subscribe = function(callback, context) {
    for (var relativePath in this.sourceCache) {
        if (hasOwn.call(this.sourceCache, relativePath)) {
            callback.call(context || null, relativePath);
        }
    }

    this.on("added", function(relativePath) {
        callback.call(context || null, relativePath);
    });
};

/**
 * Avoid memory leaks by removing listeners and emptying the cache.
 */
RFCp.clear = function() {
    this.removeAllListeners();

    for (var relativePath in this.sourceCache) {
        delete this.sourceCache[relativePath];
    }
};

function orNull(err) {
    return null;
}

exports.ReadFileCache = ReadFileCache;


var events = require('events'),
    util   = require('util');

module.exports = (function () {

    ///////////////////////////// PRIVATE  /////////////////////////////////////

    var verbose          = false;
    var debug            = false;

    //
    var requestQueue     = {};
    var resourceCache    = {};
    var defaultCacheTTL  = 1000;
    var defaultCacheSize = 100;

    var emitThreshold    = 1000; // ms
    var lastEmit         = 0;

    var totalRequests       = 0;
    var totalRequestElapsed = 0;

    /// Stats
    var stats = {
        hit         : 0, // was in the cache
        miss        : 0, // had to fetch
        failed      : 0, // Error while fetching
        queued      : 0, // resource was already fetching and we had to wait for it
        inCache     : 0, // # in the cache now
        waiting     : 0, // RealTime: # waiting now for a resource
        fetching    : 0, // RealTime: # of resources being fetched now
        avgFetchTime: 0
    };

    function bufferedEmitter() {
        var now = new Date().getTime();
        if (now - lastEmit > emitThreshold) {
            this.emit.apply(this, arguments);
            lastEmit = now;
        }
    }

    function weedOutCache() {
        var now = new Date().getTime();

        var deadResource = {
            'key'    : null,
            'epoch'  : Number.MAX_VALUE,  // new Date().getTime();
            'access' : Number.MAX_VALUE,  // new Date().getTime();
            'expire' : 0,                 // defaultCacheTTL
            'hits'   : Number.MIN_VALUE
        };

        // Remove the cache element that will expire first, when multiple matches
        // expire the one with the less hits.
        for (var key in resourceCache) {
            if (resourceCache.hasOwnProperty(key)) {
                if (requestQueue[key]) // This resource cannot be removed because clients are waiting for it
                    continue;
                if (resourceCache[key].epoch + resourceCache[key].expire < now) { // has the resource expire?
                    if (deadResource.epoch + deadResource.expire < now) {// has the one we plan to remove has also expired?
                        if (resourceCache[key].hits / (now - resourceCache[key].epoch) < deadResource.hits / (now - deadResource.epoch)) { // remove the one with less hits per unit of time
                            deadResource = resourceCache[key];
                        }
                    } else { // the one marked has not expired yet, lets remove this one since it has
                        deadResource = resourceCache[key];
                    }
                        // this one has not expire, lets look if it will expire before the one we have marked
                } else if (resourceCache[key].epoch + resourceCache[key].expire < deadResource.epoch + deadResource.expire) {
                    deadResource = resourceCache[key];
                } else if (resourceCache[key].epoch + resourceCache[key].expire === deadResource.epoch + deadResource.expire &&
                           resourceCache[key].hits < deadResource.hits) { // expires at the same time, lets take the one that has been requested less
                    deadResource = resourceCache[key];
                }
            }
        }
        if (resourceCache[deadResource.key]) {
            if (debug) util.log('cache|weedOut|key='+deadResource.key);
            --stats.inCache;
            delete resourceCache[deadResource.key];
        }
        var then = new Date().getTime();
        if (then - now > 500) {
            util.log('Cache|ERROR|NODE slowdown because of the size of the cache (' + defaultCacheSize + ')');
        } else if (then - now > 100) {
            util.log('Cache|WARNING|NODE slowdown because of the size of the cache (' + defaultCacheSize + ')');
        }
        if (debug) util.log('Cache|weedOut|elapse='+ (then - now) + 'ms');
    }

    function execute(task) {
        var self    = this;  // The Cache class that inherits Events
        var started = new Date().getTime();
        var key     = task.shift();
        var obj     = typeof(task[0]) === 'object' ? task.shift()  : null; // if the fetch function is a class, this is the 'this'
        var fetcher = task.shift();  // the fetch function or method (if the previous parameter was an object)
        var args    = task; // All the args to be passed to the fetch function/method without the callback
        args.push(done); // We are adding the 'done' method as the callback
        ++stats.miss;
        ++stats.fetching;
        bufferedEmitter.call(this, 'stats', stats);
        fetcher.apply(obj, args); // Fetch the resource and call 'done' when it is fetched

        function done(err, resource) {
            totalRequestElapsed += ((new Date().getTime()) - started);
            ++totalRequests;
            stats.avgFetchTime = parseInt(totalRequestElapsed / totalRequests);
            if (err || verbose) util.log('cache|execute|done|err='+err+'|result='+(resource ? 'found':'null'));
            if (err) {
                ++stats.failed;
            }
            if (!err && defaultCacheTTL) {    // ttl ===  0 --> expire imediatly.
                if (stats.inCache >= defaultCacheSize) {
                    weedOutCache();
                }
                var now = new Date().getTime();
                resourceCache[key] = {
                    'key'   : key,
                    'epoch' : now,
                    'access': now,
                    'expire': defaultCacheTTL,
                    'hits'  : 0,
                    'data'  : resource
                };
                ++stats.inCache;
            }

            var pendingRequests = requestQueue[key];
            delete requestQueue[key];
            for (var i = 0, size = pendingRequests.length ; i < size ; ++i) {
                if (debug) util.log('cache|calling='+i+'|err='+err+'|resource='+(resource ? 'found':'null'));
                if (!err && defaultCacheTTL) {
                    ++resourceCache[key].hits;
                }
                pendingRequests[i].call(this, err, resource, resourceCache[key]);
                --stats.waiting;
            }
            --stats.fetching;
            if (stats.fetching === 0 && stats.waiting === 0) {
                self.emit('stats', stats);
            } else {
                bufferedEmitter.call(self, 'stats', stats);
            }
        }
    }

    /////////////////////////// PUBLIC CLASS //////////////////////////////////

    function Cache(size, ttl) {
        defaultCacheSize = size || defaultCacheSize;
        defaultCacheTTL  = ttl  || defaultCacheTTL;
        if (verbose) util.log('Cache|defaultCacheSize='+defaultCacheSize+'|defaultCacheTTL='+defaultCacheTTL);
        if (defaultCacheSize > 10000) {
            util.log('Cache|WARNING|Weeding out a BIG (' + defaultCacheSize + ') cache when it is full can degrade the NODE server performance since it is not async');
        }
    }

    util.inherits(Cache, events.EventEmitter);

    Cache.prototype.clear = function() {
        resourceCache = {};
        stats.inCache = 0;
        this.emit('stats', stats);
    };

    Cache.prototype.invalidate = function(key) {
        if (resourceCache[key]) {
            delete resourceCache[key];
            --stats.inCache;
        }
        this.emit('stats', stats);
    };

    Cache.prototype.queue = function(key) {
        var task =  Array.prototype.slice.call(arguments);
        if (task.length < 3)
            throw new Error('Cache: The first parameter has to be the key for the resource and the second parameter the "function" to obtain the resource and the last is the callback');
        var callback = task.pop();
        // The resource is in the cache
        if (resourceCache.hasOwnProperty(key)) {
            ++stats.hit;
            ++resourceCache[key].hits;
            resourceCache[key].access = new Date().getTime();
            process.nextTick(function () {
                callback(null, resourceCache[key].data, resourceCache[key]);
            });
            bufferedEmitter.call(this, 'stats', stats);
            return;
        }
        ++stats.waiting;
        if (requestQueue.hasOwnProperty(key)) {
            requestQueue[key].push(callback);
            ++stats.queued;
            bufferedEmitter.call(this, 'stats', stats);
            if (verbose) util.log('cache|queued|key='+key+'|waiting='+stats.waiting);
            return;
        }
        if (verbose) util.log('cache|fetch|key='+key);
        requestQueue[key] = [callback];
        execute.call(this, task);
    };

///////////////////////////////////////////////////////////////////////////////

    return Cache;
})();


/**
 * Created by joel on 3/9/2016.
 */
var stat = require('./stat');
var Requester = require('./request');

module.exports = API;

/**
 * Represents an instance of a gogs api
 * @param apiUrl {string} the api end point e.g. 'https://try.gogs.io/api/v1'
 * @param requester {Function|undefined} Optional requester module.
 * @returns {API}
 * @constructor
 */
function API (apiUrl, requester) {
  var _this = this;
  _this.apiUrl = apiUrl;

  // Dependency injection. Allow a custom request module.
  var request = requester ? requester(apiUrl) : Requester(apiUrl);

  /**
   * Creates a new user account
   * @param user {object} the user to be created. Requires username, email, password
   * @param authUser {object} the user authenticating this request. Requires token or username and password
   * @param notify {boolean} send notification email to user
   * @return {Promise<object>} the newly created user
   */
  _this.createUser = function (user, authUser, notify) {
    user.send_notify = notify;

    return request('admin/users', authUser, user).then(stat.checkCreatedResponse);
  };

  /**
   * Edits the details on an existing user account
   * @param user {object} the user who's information will be updated. Requires username (note: the username cannot be changed)
   * @param authUser {object} the user authenticating this request. Requires token or username and password
   * @returns {Promise<object>} the updated user object
     */
  _this.editUser = function (user, authUser) {
    return request('admin/users/' + user.username, authUser, user, 'PATCH').then(stat.checkStandardResponse);
  };

  /**
   * Deletes a user
   * @param user {object} the user to delete. Requires username
   * @param authUser {object} the user authenticating this request. Users cannot delete themselves. Requires token or username and password
   * @returns {Promise} resolves if successful
   */
  _this.deleteUser = function (user, authUser) {
    if (user.username === authUser.username) {
      return Promise.reject('Users cannot delete themselves!');
    }

    return request('admin/users/' + user.username, authUser, null, 'DELETE').then(stat.checkNoContentResponse);
  };

  /**
   * Searches for users that match the query
   * @param query {string}
   * @param limit {int} the maximum number of results to return
   * @param authUser {object} the user authenticating this request. If null the email fields will be empty in the result. Requires token or username and password
   * @returns {Promise<array>} an array of user objects
   */
  _this.searchUsers = function (query, limit, authUser) {
    limit = limit || 10; // no zero limit allowed

    return request('users/search?q=' + query + '&limit=' + limit, authUser).then(stat.checkOkResponse);
  };

  /**
   * Retrieves a user
   * @param user {object} the user to retrieve. Requires username
   * @param authUser {object} the user to authenticate as. If null the email field in the response will be empty. Requires token or username and password
   * @returns {Promise<object>} the found user object
   */
  _this.getUser = function (user, authUser) {
    return request('users/' + user.username, authUser).then(stat.checkStandardResponse);
  };

  /**
   * Searches for public repositories that match the query
   * @param query {string}
   * @param uid {int} the id of the user whose repositories will be searched. 0 will search all
   * @param limit {int} the maximum number of results to return
   * @returns {Promise<array>} an array of repository objects
   */
  _this.searchRepos = function (query, uid, limit) {
    uid = uid || 0;
    limit = limit || 10;

    return request('repos/search?q=' + query + '&uid=' + uid + '&limit=' + limit).then(stat.checkOkResponse);
  };

  /**
   * Creates a new repository for the user
   * @param repo {object} the repository being created. Requires name, description, private
   * @param user {object} the user creating the repository. Requires token or username and password
   * @returns {Promise<object>} the new repository object
     */
  _this.createRepo = function (repo, user) {
    return request('user/repos', user, {
      name: repo.name,
      description: repo.description,
      private: repo.private
    }, null).then(stat.checkCreatedResponse);
  };

  /**
   * Returns information about a single repository
   * @param repo {object} the repository that will be retrieved. Requires full_name
   * @param authUser {object} the user authenticating this request. Requires username, password or token
   * @returns {Promise<object>} the repository object
   */
  _this.getRepo = function (repo, authUser) {
	return request('repos/' + repo.full_name, authUser).then(stat.checkStandardResponse);
  };

  /**
   * Returns an array of repositories this user has access to
   * @param user {object} the user who's repositories will be listed. Requires token or username and password
   * @returns {Promise<array>} an array of repository objects
   */
  _this.listRepos = function (user) {
    return request('user/repos', user).then(stat.checkStandardResponse);
  };

  /**
   * Deletes a repository from the user
   * @param repo {object} the repository to delete. Requires name
   * @param user {object} the user that owns the repository. Requires token or username and password
   * @returns {Promise} resolves if successful
   */
  _this.deleteRepo = function (repo, user) {
    return request('repos/' + user.username + '/' + repo.name, user, null, 'DELETE').then(stat.checkNoContentResponse);
  };

  /**
   * Creates an authentication token for the user
   * @param token {object} the token to be created. Requires name
   * @param user {object} the user creating the token. Requires username, token or password
   * @returns {Promise<object>} the new token object
   */
  _this.createToken = function (token, user) {
    return request('users/' + user.username + '/tokens', user, {name: token.name}).then(stat.checkCreatedResponse);
  };

  /**
   * Returns an array of tokens the user has
   * @param user {object} the user who's tokens will be listed. Requires username, password
   * @returns {Promise<array>} an array of token objects
   */
  _this.listTokens = function (user) {
    return request('users/' + user.username + '/tokens', user).then(stat.checkStandardResponse);
  };

  /**
   * Creates a public key for the user
   * @param key {object} the key to be created. Requires title, key
   * @param user {object} the user creating the key. Requires token or username and password
   * @returns {Promise<object>} the new public key object
   */
  _this.createPublicKey = function (key, user) {
    return request('user/keys', user, {
      title: key.title,
      key: key.key
    }).then(stat.checkCreatedResponse);
  };

  /**
   * Returns an array of public keys that belong to the user
   * @param user {object} the user who's public keys will be listed. Requires username, token or password
   * @returns {Promise<array>} an array of public key objects
   */
  _this.listPublicKeys = function (user) {
    return request('users/' + user.username + '/keys', user).then(stat.checkStandardResponse);
  };

  /**
   * Returns the full details for a public key
   * @param key {object} the key that will be retrieved. Requires id
   * @param user {object} the user who's key will be retrieved. Requires token or username and password
   * @returns {Promise<object>} the public key object
   */
  _this.getPublicKey = function (key, user) {
    return request('user/keys/' + key.id, user).then(stat.checkStandardResponse);
  };

  /**
   * Deletes a public key from the user
   * @param key {object} the key to be deleted. Requires id
   * @param user {object} the user who's key will be deleted. Requires token or username and password
   * @returns {Promise} resolves if successful
     */
  _this.deletePublicKey = function (key, user) {
    return request('user/keys/' + key.id, user, null, 'DELETE').then(stat.checkNoContentResponse);
  };

  return _this;
}


// # bionode-seq
// > Module for DNA, RNA and protein sequences manipulation.
// >
// > doi: [?](?)
// > author: [Bruno Vieira](http://bmpvieira.com)
// > email: <mail@bmpvieira.com>
// > license: [MIT](https://raw.githubusercontent.com/bionode/bionode-seq/master/LICENSE)
//
// ---
//
// ## Usage
// See the methods below.

var seq = module.exports

var _baseMatrix = { A: 'T', C: 'G', W: 'S', M: 'K', R: 'Y', B: 'V', D: 'H' }

var _dnaComplementBasesMatrix = Object.create(_baseMatrix)
var _rnaComplementBasesMatrix = Object.create(_baseMatrix)
var _transcribeBasesMatrix = Object.create(_baseMatrix)
var _translateCodonsMatrix = {
  'GCU': 'A', 'GCC': 'A', 'GCA': 'A', 'GCG': 'A', 'CGU': 'R', 'CGC': 'R',
  'CGA': 'R', 'CGG': 'R', 'AGA': 'R', 'AGG': 'R', 'AAU': 'N', 'AAC': 'N',
  'GAU': 'D', 'GAC': 'D', 'UGU': 'C', 'UGC': 'C', 'CAA': 'Q', 'CAG': 'Q',
  'GAA': 'E', 'GAG': 'E', 'GGU': 'G', 'GGC': 'G', 'GGA': 'G', 'GGG': 'G',
  'CAU': 'H', 'CAC': 'H', 'AUU': 'I', 'AUC': 'I', 'AUA': 'I', 'UUA': 'L',
  'UUG': 'L', 'CUU': 'L', 'CUC': 'L', 'CUA': 'L', 'CUG': 'L', 'AAA': 'K',
  'AAG': 'K', 'AUG': 'M', 'UUU': 'F', 'UUC': 'F', 'CCU': 'P', 'CCC': 'P',
  'CCA': 'P', 'CCG': 'P', 'UCU': 'S', 'UCC': 'S', 'UCA': 'S', 'UCG': 'S',
  'AGU': 'S', 'AGC': 'S', 'ACU': 'T', 'ACC': 'T', 'ACA': 'T', 'ACG': 'T',
  'UGG': 'W', 'UAU': 'Y', 'UAC': 'Y', 'GUU': 'V', 'GUC': 'V', 'GUA': 'V',
  'GUG': 'V', 'UAA': '*', 'UGA': '*', 'UAG': '*', 'XAA': 'X', 'XAC': 'X',
  'XAG': 'X', 'XAU': 'X', 'XCA': 'X', 'XCC': 'X', 'XCG': 'X', 'XCU': 'X',
  'XGA': 'X', 'XGC': 'X', 'XGG': 'X', 'XGU': 'X', 'XUA': 'X', 'XUC': 'X',
  'XUG': 'X', 'XUU': 'X', 'XAX': 'X', 'XCX': 'X', 'XGX': 'X', 'XUX': 'X',
  'XXA': 'X', 'XXC': 'X', 'XXG': 'X', 'XXU': 'X', 'XXX': 'X',
  'gcu': 'a', 'gcc': 'a', 'gca': 'a', 'gcg': 'a', 'cgu': 'r', 'cgc': 'r',
  'cga': 'r', 'cgg': 'r', 'aga': 'r', 'agg': 'r', 'aau': 'n', 'aac': 'n',
  'gau': 'd', 'gac': 'd', 'ugu': 'c', 'ugc': 'c', 'caa': 'q', 'cag': 'q',
  'gaa': 'e', 'gag': 'e', 'ggu': 'g', 'ggc': 'g', 'gga': 'g', 'ggg': 'g',
  'cau': 'h', 'cac': 'h', 'auu': 'i', 'auc': 'i', 'aua': 'i', 'uua': 'l',
  'uug': 'l', 'cuu': 'l', 'cuc': 'l', 'cua': 'l', 'cug': 'l', 'aaa': 'k',
  'aag': 'k', 'aug': 'm', 'uuu': 'f', 'uuc': 'f', 'ccu': 'p', 'ccc': 'p',
  'cca': 'p', 'ccg': 'p', 'ucu': 's', 'ucc': 's', 'uca': 's', 'ucg': 's',
  'agu': 's', 'agc': 's', 'acu': 't', 'acc': 't', 'aca': 't', 'acg': 't',
  'ugg': 'w', 'uau': 'y', 'uac': 'y', 'guu': 'v', 'guc': 'v', 'gua': 'v',
  'gug': 'v', 'uaa': '*', 'uga': '*', 'uag': '*', 'xaa': 'x', 'xac': 'x',
  'xag': 'x', 'xau': 'x', 'xca': 'x', 'xcc': 'x', 'xcg': 'x', 'xcu': 'x',
  'xga': 'x', 'xgc': 'x', 'xgg': 'x', 'xgu': 'x', 'xua': 'x', 'xuc': 'x',
  'xug': 'x', 'xuu': 'x', 'xax': 'x', 'xcx': 'x', 'xgx': 'x', 'xux': 'x',
  'xxa': 'x', 'xxc': 'x', 'xxg': 'x', 'xxu': 'x', 'xxx': 'x'
}

_rnaComplementBasesMatrix['A'] = 'U'
delete _rnaComplementBasesMatrix['T']

_transcribeBasesMatrix['A'] = 'U'

mirrorAndLowerCaseMatrices([
  _dnaComplementBasesMatrix,
  _rnaComplementBasesMatrix,
  _transcribeBasesMatrix
])

_transcribeBasesMatrix['T'] = 'A'
_transcribeBasesMatrix['t'] = 'a'

function mirrorAndLowerCaseMatrices(matricesArray) {
  matricesArray.forEach(function(matrix) {
    for (var k in matrix) {
      var v = matrix[k]
      matrix[k.toLowerCase()] = v.toLowerCase()
      matrix[v] = k
      matrix[v.toLowerCase()] = k.toLowerCase()
    }
  })
}

// ### Check sequence type

// Takes a sequence string and checks if it's DNA, RNA or protein (returns 'dna', 'rna', 'protein' or undefined). Other optional arguments include threshold, length and index (see below).
//
//     seq.checkType("ATGACCCTGAGAAGAGCACCG");
//     => "dna"
//     seq.checkType("AUGACCCUGAAGGUGAAUGAA");
//     => "rna"
//     seq.checkType("MAYKSGKRPTFFEVFKAHCSDS");
//     => "protein"
//     seq.checkType("1234567891234567ATGACC");
//     => undefined
//
// By default, the method has a 90% threshold, however, this can be altered as required.
//
//     seq.checkType("1234567891234567ATGACC", 0.8);
//     => undefined
//     seq.checkType("--------MAYKSGKRPTFFEV", 0.7);
//     => "protein"
//
// The length value specifies the length of the sequence to be analyse (default 10000). If your sequence is extremely long, you may want to analyse a shorter sub-section to reduce the computational burden.
//
//     seq.checkType('A Very Long Sequence', 0.9, 1000);
//     => Type based on the first 1000 characters
//
// The index value specifies the point on the sequence from which the sequence is to be analysed. Perhaps you know that there are lot of gaps at the start of the sequence.
//
//     seq.checkType("--------MAYKSGKRPTFFEV", 0.9, 10000, 8);
//     => "protein"
//
seq.checkType = function (sequence, threshold, length, index) {
  if (threshold === undefined) {
    threshold = 0.9
  }
  if (length === undefined) {
    length = 10000
  }
  if (index === undefined) {
    index = 1
  }
  var seq = sequence.slice(index - 1, length)

  var dnaSeq = seq.replace(/N/gi,'')
  var dnaTotal = dnaSeq.length
  var acgMatch = ((dnaSeq.match(/[ACG]/gi) || []).length) / dnaTotal
  var tMatch = ((dnaSeq.match(/[T]/gi) || []).length) / dnaTotal
  var uMatch = ((dnaSeq.match(/[U]/gi) || []).length) / dnaTotal

  var proteinSeq = seq.replace(/X/gi,'')
  var proteinTotal = proteinSeq.length
  var proteinMatch = ((seq.match(/[ARNDCQEGHILKMFPSTWYV\*]/gi) || []).length) / proteinTotal

  if (((acgMatch + tMatch) >= threshold) || ((acgMatch + uMatch) >= threshold)) {
    if (tMatch >= uMatch) {
      return 'dna'
    } else if (uMatch >= tMatch) {
      return 'rna'
    } else {
      return 'dna'
    }
  } else if (proteinMatch >= threshold) {
    return 'protein'
  }
}

// Takes a sequence type argument and returns a function to complement bases.
seq.createComplementBase = function(sequenceType) {
  var complementBasesMatrix = (sequenceType === 'rna' || sequenceType === 'ambiguousRna') ? _rnaComplementBasesMatrix : _dnaComplementBasesMatrix
  var getComplementBase = function(base) {
    var complement = complementBasesMatrix[base]
    return complement || base
  }
  return getComplementBase
}

// ### Reverse sequence

// Takes sequence string and returns the reverse sequence.
//
//     seq.reverse("ATGACCCTGAAGGTGAA");
//     => "AAGTGGAAGTCCCAGTA"
seq.reverse = function(sequence) {
  return sequence.split('').reverse().join('')
}

// ### (Reverse) complement sequence

// Takes a sequence string and optional boolean for reverse, and returns its complement.
//
//     seq.complement("ATGACCCTGAAGGTGAA");
//     => "TACTGGGACTTCCACTT"
//     seq.complement("ATGACCCTGAAGGTGAA", true);
//     => "TTCACCTTCAGGGTCAT"
//     //Alias
//     seq.reverseComplement("ATGACCCTGAAGGTGAA");
//     => "TTCACCTTCAGGGTCAT"
seq.complement = function(sequence, reverse) {
  var reverse = reverse || false
  var sequenceType = seq.checkType(sequence)
  var getComplementBase = seq.createComplementBase(sequenceType)
  if (reverse) {
    return sequence.split('').reverse().map(getComplementBase).join('')
  }
  else {
    return sequence.split('').map(getComplementBase).join('')
  }
}

// Takes a sequence string and returns the reverse complement (syntax sugar).
seq.reverseComplement = function(sequence) {
  return seq.complement(sequence, true)
}

// ### Transcribe base

// Takes a base character and returns the transcript base.
//
//     seq.getTranscribedBase("A");
//     => "U"
//     seq.getTranscribedBase("T");
//     => "A"
//     seq.getTranscribedBase("t");
//     => "a"
//     seq.getTranscribedBase("C");
//     => "G"
seq.getTranscribedBase = function(base) {
  return _transcribeBasesMatrix[base] || base
}

// ### Get codon amino acid

// Takes an RNA codon and returns the translated amino acid.
//
//     seq.getTranslatedAA("AUG");
//     => "M"
//     seq.getTranslatedAA("GCU");
//     => "A"
//     seq.getTranslatedAA("CUU");
//     => "L"
seq.getTranslatedAA = function(codon) {
  return _translateCodonsMatrix[codon]
}

// ### Remove introns

// Take a sequence and an array of exonsRanges and removes them.
//
//     seq.removeIntrons("ATGACCCTGAAGGTGAATGACAG", [[1, 8]]);
//     => "TGACCCT"
//     seq.removeIntrons("ATGACCCTGAAGGTGAATGACAG", [[2, 9], [12, 20]]);
//     => "GACCCTGGTGAATGA"
seq.removeIntrons = function(sequence, exonsRanges) {
  var sequenceWithoutIntrons = ''
  var exonsRangesSorted = exonsRanges.sort(function(a, b) { return a[0] - b[0] })
  exonsRangesSorted.forEach(function(exonRange) {
    sequenceWithoutIntrons += sequence.substring(exonRange[0], exonRange[1])
  })
  return sequenceWithoutIntrons
}

// ### Transcribe sequence

// Takes a sequence string and returns the transcribed sequence (dna <-> rna).
// If an array of exons is given, the introns will be removed from the sequence.
//
//     seq.transcribe("ATGACCCTGAAGGTGAA");
//     => "AUGACCCUGAAGGUGAA"
//     seq.transcribe("AUGACCCUGAAGGUGAA"); //reverse
//     => "ATGACCCTGAAGGTGAA"
seq.transcribe = function(sequence, exonsRanges) {
  if (exonsRanges) {
    var sequenceWithoutIntrons = seq.removeIntrons(sequence, exonsRanges)
    sequence = sequenceWithoutIntrons
  }
  var sequenceType = seq.checkType(sequence)
  if (sequenceType === 'dna' || sequenceType === 'ambiguousDna') {
    return sequence.replace(/t/g, 'u').replace(/T/g, 'U')
  }
  else if (sequenceType === 'rna' || sequenceType === 'ambiguousRna') {
    return sequence.replace(/u/g, 't').replace(/U/g, 'T')
  }
}

// ### Translate sequence

// Takes a DNA or RNA sequence and translates it to protein
// If an array of exons is given, the introns will be removed from the sequence.
//
//     seq.translate("ATGACCCTGAAGGTGAATGACAGGAAGCCCAAC"); //dna
//     => "MTLKVNDRKPN"
//     seq.translate("AUGACCCUGAAGGUGAAUGACAGGAAGCCCAAC"); //rna
//     => "MTLKVNDRKPN"
//     seq.translate("ATGACCCTGAAGGTGAATGACAGGAAGCC", [[3, 21]]);
//     => "LKVND"
seq.translate = function(sequence, exonsRanges) {
  if (exonsRanges) {
    var sequenceWithoutIntrons = seq.removeIntrons(sequence, exonsRanges)
    sequence = sequenceWithoutIntrons
  }
  var sequenceType = seq.checkType(sequence)
  var rna
  if (sequenceType === 'protein') {
    return sequence
  }
  else if (sequenceType === 'dna' || sequenceType === 'ambiguousDna') {
    if (sequenceType === 'ambiguousDna') sequence.replace(/[wsmkrybdhv]/g, 'x').replace(/[WSMKRYBDHV]/g, 'X')
    rna = seq.transcribe(sequence, exonsRanges)
  }
  else if (sequenceType === 'rna' || sequenceType === 'ambiguousRna') {
    if (sequenceType === 'ambiguousRna') sequence.replace(/[wsmkrybdhv]/g, 'x').replace(/[WSMKRYBDHV]/g, 'X')
    rna = sequence
  }
  return rna.match(/.{1,3}/g).map(seq.getTranslatedAA).join('')
}

// ### Reverse exons

// Takes an array of exons and the length of the reference and returns inverted coordinates.
//
//     seq.reverseExons([[2,8]], 20);
//     => [ [ 12, 18 ] ]
//     seq.reverseExons([[10,45], [65,105]], 180);
//     => [ [ 135, 170 ], [ 75, 115 ] ]
seq.reverseExons = function(exonsRanges, referenceLength) {
  var reversedExonsRanges = []
  exonsRanges.forEach(function(exonRange) {
    var start = referenceLength - exonRange[1]
    var stop = referenceLength - exonRange[0]
    reversedExonsRanges.push([start, stop])
  })
  return reversedExonsRanges
}

// ### Find non-canonical splice sites

// Takes a sequence and exons ranges and returns an array of non canonical splice sites.
//
//     seq.findNonCanonicalSplices("GGCGGCGGCGGTGAGGTGGACCTGCGCGAATACGTGGTCGCCCTGT", [[0, 10], [20, 30]]);
//     => [ 20 ]
//     seq.findNonCanonicalSplices("GGCGGCGGCGGTGAGGTGAGCCTGCGCGAATACGTGGTCGCCCTGT", [[0, 10], [20, 30]]);
//     => []
seq.findNonCanonicalSplices = function(sequence, exonsRanges) {
  var nonCanonicalSplices = []
  var exonsRangesSorted = exonsRanges.sort(function(a, b) { return a[0] - b[0] })
  exonsRangesSorted.forEach(checkNonCanonicalIntron)
  function checkNonCanonicalIntron(exonRange, i) {
    var donor = exonRange
    var acceptor = exonsRangesSorted[i + 1]
    if (!acceptor) return null
    var intronRange = [donor[1], acceptor[0]]
    var intronStartBases = sequence.slice(intronRange[0], intronRange[0] + 2).toLowerCase().replace('t', 'u')
    var intronStopBases = sequence.slice(intronRange[1] - 2, intronRange[1]).toLowerCase()
    if (intronStartBases !== 'gu')
      nonCanonicalSplices.push(intronRange[0])
    if (intronStopBases !== 'ag')
      nonCanonicalSplices.push(intronRange[1])
  }
  return nonCanonicalSplices
}

// ### Check canonical translation start site

// Takes a sequence and returns boolean for canonical translation start site.
//
//     seq.checkCanonicalTranslationStartSite("ATGACCCTGAAGGT");
//     => true
//     seq.checkCanonicalTranslationStartSite("AATGACCCTGAAGGT");
//     => false
seq.checkCanonicalTranslationStartSite = function(sequence) {
  return sequence.substring(0, 3).toLowerCase().replace('t', 'u') === 'aug'
}

// ### Get reading frames

// Takes a sequence and returns an array with the six possible Reading Frames (+1, +2, +3, -1, -2, -3).
//
//     seq.getReadingFrames("ATGACCCTGAAGGTGAATGACAGGAAGCCCAAC");
//     => [ 'ATGACCCTGAAGGTGAATGACAGGAAGCCCAAC',
//          'TGACCCTGAAGGTGAATGACAGGAAGCCCAAC',
//          'GACCCTGAAGGTGAATGACAGGAAGCCCAAC',
//          'GTTGGGCTTCCTGTCATTCACCTTCAGGGTCAT',
//          'TTGGGCTTCCTGTCATTCACCTTCAGGGTCAT',
//          'TGGGCTTCCTGTCATTCACCTTCAGGGTCAT' ]
seq.getReadingFrames = function(sequence) {
  var reverse = seq.reverseComplement(sequence)
  return [sequence, sequence.substring(1), sequence.substring(2), reverse, reverse.substring(1), reverse.substring(2)]
}

// ### Get open reading frames

// Takes a Reading Frame sequence and returns an array of Open Reading Frames.
//
//     seq.getOpenReadingFrames("ATGACCCTGAAGGTGAATGACAGGAAGCCCAAC");
//     => [ 'ATGACCCTGAAGGTGAATGACAGGAAGCCCAAC' ]
//     seq.getOpenReadingFrames("AUGACCCUGAAGGUGAAUGACAGGAAGCCCAAC");
//     => [ 'AUGACCCUGAAGGUGAAUGACAGGAAGCCCAAC' ]
//     seq.getOpenReadingFrames("ATGAGAAGCCCAACATGAGGACTGA");
//     => [ 'ATGAGAAGCCCAACATGA', 'GGACTGA' ]
seq.getOpenReadingFrames = function(sequence) {
  var sequenceType = seq.checkType(sequence)
  var stopCodons
  if (sequenceType === 'dna' || sequenceType === 'ambiguousDna') {
    stopCodons = ['TAA', 'TGA', 'TAG', 'taa', 'tga', 'tag']
  }
  else if (sequenceType === 'rna' || sequenceType === 'ambiguousRna') {
    stopCodons = ['UAA', 'UGA', 'UAG', 'uaa', 'uga', 'uag']
  }
  var openReadingFrames = []
  var openReadingFrame = ''
  sequence.match(/.{1,3}/g).forEach(function(codon) {
    openReadingFrame += codon
    if (stopCodons.indexOf(codon) !== -1 && openReadingFrame.length > 0) {
      openReadingFrames.push(openReadingFrame)
      openReadingFrame = ''
    }
  })
  openReadingFrames.push(openReadingFrame)
  return openReadingFrames
}

// ### Get all open reading frames

// Takes a sequence and returns all Open Reading Frames in the six Reading Frames.
//
//     seq.getAllOpenReadingFrames("ATGACCCTGAAGGTGAATGACA");
//     => [ [ 'ATGACCCTGAAGGTGAATGACA' ],
//          [ 'TGA', 'CCCTGA', 'AGGTGA', 'ATGACA' ],
//          [ 'GACCCTGAAGGTGAATGA', 'CA' ],
//          [ 'TGTCATTCACCTTCAGGGTCAT' ],
//          [ 'GTCATTCACCTTCAGGGTCAT' ],
//          [ 'TCATTCACCTTCAGGGTCAT' ] ]
seq.getAllOpenReadingFrames = function(sequence) {
  var readingFrames = seq.getReadingFrames(sequence)
  var allOpenReadingFrames = readingFrames.map(seq.getOpenReadingFrames)
  return allOpenReadingFrames
}

// ### Find longest open reading frame

// Takes a sequence and returns the longest ORF from all six reading frames and
// corresponding frame symbol (+1, +2, +3, -1, -2, -3). If a frame symbol is specified,
// only look for longest ORF on that frame.
// When sorting ORFs, if there's a tie, choose the one that starts with start codon Methionine.
// If there's still a tie, return one randomly.
//
//     seq.findLongestOpenReadingFrame("ATGACCCTGAAGGTGAATGACA");
//     => [ 'ATGACCCTGAAGGTGAATGACA', '+1' ]
//     seq.findLongestOpenReadingFrame("ATGACCCTGAAGGTGAATGACA", "-1");
//     => "TGTCATTCACCTTCAGGGTCAT"
seq.findLongestOpenReadingFrame = function(sequence, frameSymbol) {
  var frameSymbols = ['+1', '+2', '+3', '-1', '-2', '-3']
  if (frameSymbol) {
    var framePosition = frameSymbols.indexOf(frameSymbol)
    var readingFrame = seq.getReadingFrames(sequence)[framePosition]
    var openReadingFrames = seq.getOpenReadingFrames(readingFrame)
    var longestOpenReadingFrame = getLongestOpenReadingFrame(openReadingFrames)
    return longestOpenReadingFrame
  }
  else {
    // Get longest ORFs for all six possible reading frames
    var longestOpenReadingFrames = seq.getAllOpenReadingFrames(sequence).map(getLongestOpenReadingFrame)
    // Get longest ORF
    var longestOpenReadingFrame = getLongestOpenReadingFrame(longestOpenReadingFrames.slice())
    var framePosition = longestOpenReadingFrames.indexOf(longestOpenReadingFrame)
    var frameSymbol = frameSymbols[framePosition]
    return [longestOpenReadingFrame, frameSymbol]
  }

  // Helper that sorts by length, giving priority to ones that start with ATG/AUG
  function sortReadingFrames(a, b) {
    var aSort = a.length
    var bSort = b.length
    if (bSort - aSort === 0) {
      var aStartCodon = a.slice(0, 3).toUpperCase().replace('T', 'U')
      var bStartCodon = b.slice(0, 3).toUpperCase().replace('T', 'U')
      if (aStartCodon === 'AUG') { aSort++ }
      if (bStartCodon === 'AUG') { bSort++ }
    }
    return bSort - aSort
  }

  // Helper that takes an array and returns longest Reading Frame
  function getLongestOpenReadingFrame(array) {
    return array.sort(sortReadingFrames)[0]
  }

}