/**
 * Javascript
 * .JS
 */
'use strict'

const _ = require('lodash')
const util = require('util')
const settings = require('../settings')
const jsonFile = require('./json')

const REGX_KEY_VAL = /^"/
const REGX_KEY = /:$/
const REGX_BOOLEAN = /true|false/
const REGX_NULL = /null/
const REGX_UNDEFINED = /undefined/
const REGX_SPACES = /^\s+/

const REGX_JSON = /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null|undefined)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g
//const REGX_BRACES = /[{}]/g
const REGX_BRACES = /\{([^{\}]*)\}/g
const REGX_BRACKETS = /[^\(]*(\(.*\))[^\)]*/g
const REGX_COMMAS = /\"\,/g

const REGX_ARG_NAMES = /([^\s,]+)/g
//const REGX_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg
const REGX_COMMENTS = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/mg

const REGX_REMOVE_ANSI_COLOR = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g

const DEF_CLS = settings('js.colors.cls')
const DEF_COLOR = settings('js.colors.color')

function _colorfy(str, cls) {
  cls = cls || DEF_CLS
  let setting = settings('js.colors.' + cls)
  setting = setting || DEF_COLOR
  return setting ? setting(str) : str
}

function _uncolorfy(str) {
  return str && _.isString(str) ? str.replace(REGX_REMOVE_ANSI_COLOR, '') : ''
}

function _argNames(src) {
  if(!_.isFunction(src)) return []

  let ret = _.replace(src.toString(), REGX_COMMENTS, '')
  let result = ret.slice(ret.indexOf('(') + 1, ret.indexOf(')')).match(REGX_ARG_NAMES)
  return _.isArray(result) ? result : []
}

// returns function signature name
function _sigName(src) {
  if (!_.isFunction(src)) return ''
  let ret = _.trim(_.replace(src.toString(), 'function', ''))
  ret = ret.substr(0, ret.indexOf('('))
  return ret || ''
}

module.exports = {
  "fromString": fromString,
  "toString": toString,
  "toPretty": toPretty,
  "uncolorfy": _uncolorfy
}

function fromString(val) {
  return val
}

function strwrap(item) {
  return `"${item}"`
}

function toString(val, opts) {
  return util.inspect(val, opts)
}

function toPretty(val, opts) {
  let _CACHE = []

  let wrap = _.get(opts, 'wrap', settings('js.wrap'))
  let sort = _.get(opts, 'sort', settings('js.sort'))
  let align = _.get(opts, 'align', settings('js.align'))
  let indent = _.get(opts, 'indent', settings('js.indent'))
  let decimals = _.get(opts, 'decimals', settings('js.decimals'))
  let arrayPadding = _.get(opts, 'arrayPadding', settings('js.arrayPadding'))
  let objectPadding = _.get(opts, 'objectPadding', settings('js.objectPadding'))
  let afterComma = _.get(opts, 'afterComma', settings('js.afterComma'))
  let beforeComma = _.get(opts, 'beforeComma', settings('js.beforeComma'))
  let afterColon = _.get(opts, 'afterColon', settings('js.afterColon'))
  let beforeColon = _.get(opts, 'beforeColon', settings('js.beforeColon'))

  if (wrap===true) wrap = -1

  let oComma = _colorfy(',', 'comma')
  let oColon = _colorfy(':', 'colon')
  let oBrace = _colorfy('{', 'brace')
  let eBrace = _colorfy('}', 'brace')
  let oBracket = _colorfy('[', 'bracket')
  let eBracket = _colorfy(']', 'bracket')
  let oNull = _colorfy('null', 'null')
  let oUndefined = _colorfy('undefined', 'undefined')

  let apad  = _.repeat(' ', arrayPadding)
  let opad  = _.repeat(' ', objectPadding)
  let comma = _.repeat(' ', beforeComma) + oComma + _.repeat(' ', afterComma)
  let colon = _.repeat(' ', beforeColon) + oColon + _.repeat(' ', afterColon)

  let fin = build(val,'')
  _CACHE = null
  return fin

  function safeObj(obj) {
    let res = {}
    _.forIn(obj, function(v, k) {
      if (_.includes(_CACHE, v)) return true
        _CACHE.push(v)
        res[k] = v
    })
    return res
  }

  function build(o,dent) {

    function buildFunction() {
      let okeys = _.keys(o)

      if(okeys.length) {
        o = safeObj(_.pick(o, okeys))
        //return buildObject()

        return build(o, '')
      } else {
        let signame = _sigName(o)
        var sigval = _.compact([signame || 'function', "(", _argNames(o).join(", "), ")"]).join("")
        return _colorfy(`"${sigval}"`, 'string')
      }
    }

    function buildArray() {
      if(_.isEmpty(o)) return oBracket+eBracket
      let pieces  = o.map((v) => { return build(v,'') })
      let arrLine = `${dent}${oBracket}${apad}${pieces.join(comma)}${apad}${eBracket}`
      if (wrap === false || arrLine.length <= wrap) return arrLine
      return `${dent}${oBracket}\n${ o.map((v) => { return build(v, dent + indent) }).join(`${oComma}\n`) }\n${dent}${eBracket}`
    }

    function buildObject() {
      let keyvals=[]
      let i = 0

      if(_.isEmpty(o)) return oBrace+eBrace

      _.forIn(o, (v, k) => {
        let kstr = JSON.stringify(k)
        kstr = _colorfy(kstr, 'key')
        keyvals[i++] = [kstr, build(v, '')]
      })

      if (sort) {
        keyvals = keyvals.sort((kv1,kv2) => {
          kv1 = kv1[0]
          kv2 = kv2[0]
          return kv1 < kv2 ? -1 : kv1 > kv2 ? 1 : 0
        })
      }

      keyvals = keyvals.map((kv) => { return kv.join(colon) }).join(comma)

      let objLine = `${oBrace}${opad}${keyvals}${opad}${eBrace}`

      if (wrap===false || objLine.length < wrap) return objLine

      keyvals = []
      i = 0

      _.forIn(o, (v, k) => {
        let kstr = JSON.stringify(k)
        kstr = _colorfy(kstr, 'key')
        keyvals[i++] = [`${dent}${indent}${kstr}`, v]
      })

      if (sort) {
        keyvals = keyvals.sort((kv1, kv2) => {
          kv1 = kv1[0]
          kv2=kv2[0]
          return kv1 < kv2 ? -1 : kv1 > kv2 ? 1 : 0
        })
      }

      if (align) {
        let longest = 0;
        for (let i=keyvals.length;i--;) if (keyvals[i][0].length > longest) longest = keyvals[i][0].length
        for (let i=keyvals.length;i--;) keyvals[i][0] = _.padEnd(keyvals[i][0], longest)
      }

      let indent2 = dent + indent;

      for (let i=keyvals.length; i--; ){
        let k = keyvals[i][0]
        let v = keyvals[i][1]
        let oLine = k + colon + build(v,'')
        keyvals[i] = (wrap===false || oLine.length <= wrap || !v || typeof v!="object") ? oLine : ( k + colon + build(v,indent2).replace(REGX_SPACES,''))
      }

      return `${dent}${oBrace}\n${keyvals.join(`${oComma}\n`)}\n${dent}${eBrace}`
    }

    function buildRegExp() { return dent + _colorfy(`"${o}"`, 'regex') }
    function buildString() { return dent + _colorfy(`"${_.trim(o)}"`, 'string') }
    function buildBoolean() { return dent + _colorfy(o, 'boolean') }
    function buildDefault() { return dent + o }
    function buildNumber() {
      let isFloat = Number(o) === o && o % 1 !== 0
      o = isFloat && decimals ? o.toFixed(decimals) : o + ''
      return dent + _colorfy(o, 'number')
    }

    if (o===null) return `${dent}${oNull}`
    if (o===undefined) return `${dent}${oUndefined}`
    if(_.isFunction(o)) return buildFunction()
    if(_.isRegExp(o)) return buildRegExp()
    if(_.isNumber(o)) return buildNumber()
    if(_.isString(o)) return buildString()
    if(_.isBoolean(o)) return buildBoolean()
    if(_.isArray(o)) return buildArray()
    if(_.isObject(o)) return buildObject()
    return buildDefault()
  }
}

/**
 * Highlighting now done within toPretty
 */

// function _mapVal(str) {
//   if (REGX_KEY_VAL.test(str)) return REGX_KEY.test(str) ? 'key' : 'string'
//   if (REGX_BOOLEAN.test(str)) return 'boolean'
//   if (REGX_NULL.test(str)) return 'null'
//   if (REGX_UNDEFINED.test(str)) return 'undefined'
//   return DEF_CLS
// }

// // returns method signature string "doSomething(time, place, whatever)"
// // or the val if not a function
// function sig(v, k) {
//   return _.isFunction(v) ? [k, "(", _argNames(v).join(", "), ")"].join("") : v;
// };

// function _highlight(json) {
//   let jstr = jsonFile.toString(json)

//   // this is for web/html
//   //jstr = jstr.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')

//   jstr = jstr.replace(REGX_JSON, (match) => {
//     let cls = _mapVal(match)
//     return _colorfy(match, cls)
//   })

//   // jstr = jstr.replace(REGX_BRACES, (match) => {
//   //   return _colorfy(match, 'brace')
//   // })

//   // jstr = jstr.replace(REGX_BRACKETS, (match) => {
//   //   return _colorfy(match, 'bracket')
//   // })

//   // jstr = jstr.replace(REGX_COMMAS, (match) => {
//   //   return _colorfy(match, 'comma')
//   // })

//   return jstr
// }


/**
 * @private
 */
Ext.define('Ext.device.filesystem.HTML5', {
    extend: 'Ext.device.filesystem.Abstract',
    /**
     * Requests a {@link Ext.device.filesystem.FileSystem} instance.
     *
     *      var me = this;
     *      var fs = Ext.create("Ext.device.FileSystem", {});
     *      fs.requestFileSystem({
     *          type: window.PERSISTENT,
     *          size: 1024 * 1024,
     *          success: function(fileSystem) {
     *              me.fs = fileSystem;
     *          },
     *          failure: function(err) {
     *              console.log("FileSystem Failure: " + err.code);
     *          }
     *      });
     *
     * @param {Object} config
     * The object which contains the following config options:
     *
     * @param {Number} config.type
     * window.TEMPORARY (0) or window.PERSISTENT (1)
     *
     * @param {Number} config.size
     * Storage space, in Bytes, needed by the application
     *
     * @param {Function} config.success This is required.
     * The callback to be called when the file system has been successfully created.
     *
     * @param {Ext.device.filesystem.FileSystem} config.success.fileSystem
     * The created file system.
     *
     * @param {Function} config.failure This is optional.
     * The callback to be called when an error occurred.
     *
     * @param {Object} config.failure.error
     * The occurred error.
     *
     * @param {Object} config.scope
     * The scope object
     */
    requestFileSystem: function(config) {
        if (!config.success) {
            Ext.Logger.error('Ext.device.filesystem#requestFileSystem: You must specify a `success` callback.');
            return null;
        }

        var me = this;
        var successCallback = function(fs) {
            var fileSystem = Ext.create('Ext.device.filesystem.FileSystem', fs);
            config.success.call(config.scope || me, fileSystem);
        };

        window.requestFileSystem(
            config.type,
            config.size,
            successCallback,
            config.failure || Ext.emptyFn
        );
    }
}, function() {
    /**
     * The FileSystem class which is used to represent a file system.
     */
    Ext.define('Ext.device.filesystem.FileSystem', {
        fs: null,
        root: null,

        constructor: function(fs) {
            this.fs = fs;
            this.root = Ext.create('Ext.device.filesystem.DirectoryEntry', '/', this);
        },

        /**
         * Returns a {@link Ext.device.filesystem.DirectoryEntry} instance for the root of the file system.
         *
         * @return {Ext.device.filesystem.DirectoryEntry}
         * The file system root directory.
         */
        getRoot: function() {
            return this.root;
        }
    }, function() {
        /**
         * The Entry class which is used to represent entries in a file system,
         * each of which may be a {@link Ext.device.filesystem.FileEntry} or a {@link Ext.device.filesystem.DirectoryEntry}.
         *
         * This is an abstract class.
         * @abstract
         */
        Ext.define('Ext.device.filesystem.Entry', {
            directory: false,
            path: 0,
            fileSystem: null,
            entry: null,

            constructor: function(directory, path, fileSystem) {
                this.directory = directory;
                this.path = path;
                this.fileSystem = fileSystem;
            },

            /**
             * Returns whether the entry is a file.
             *
             * @return {Boolean}
             * The entry is a file.
             */
            isFile: function() {
                return !this.directory;
            },

            /**
             * Returns whether the entry is a directory.
             *
             * @return {Boolean}
             * The entry is a directory.
             */
            isDirectory: function() {
                return this.directory;
            },

            /**
             * Returns the name of the entry, excluding the path leading to it.
             *
             * @return {String}
             * The entry name.
             */
            getName: function() {
                var components = this.path.split('/');
                for (var i = components.length - 1; i >= 0; --i) {
                    if (components[i].length > 0) {
                        return components[i];
                    }
                }

                return '/';
            },

            /**
             * Returns the full absolute path from the root to the entry.
             *
             * @return {String}
             * The entry full path.
             */
            getFullPath: function() {
                return this.path;
            },

            /**
             * Returns the file system on which the entry resides.
             *
             * @return {Ext.device.filesystem.FileSystem}
             * The entry file system.
             */
            getFileSystem: function() {
                return this.fileSystem;
            },

            getEntry: function() {
                return null;
            },

            /**
             * Moves the entry to a different location on the file system.
             *
             * @param {Object} config
             * The object which contains the following config options:
             *
             * @param {Ext.device.filesystem.DirectoryEntry} config.parent This is required.
             * The directory to which to move the entry.
             *
             * @param {String} config.newName This is optional.
             * The new name of the entry to move. Defaults to the entry's current name if unspecified.
             *
             * @param {Function} config.success This is optional.
             * The callback to be called when the entry has been successfully moved.
             *
             * @param {Ext.device.filesystem.Entry} config.success.entry
             * The entry for the new location.
             *
             * @param {Function} config.failure This is optional.
             * The callback to be called when an error occurred.
             *
             * @param {Object} config.failure.error
             * The occurred error.
             *
             * @param {Object} config.scope
             * The scope object
             */
            moveTo: function(config) {
                if (config.parent == null) {
                    Ext.Logger.error('Ext.device.filesystem.Entry#moveTo: You must specify a new `parent` of the entry.');
                    return null;
                }

                var me = this;

                this.getEntry(
                    {
                        options: config.options || {},
                        success: function(sourceEntry) {
                            config.parent.getEntry(
                                {
                                    options: config.options || {},
                                    success: function(destinationEntry) {
                                        if (config.copy) {
                                            sourceEntry.copyTo(destinationEntry, config.newName, function(entry) {
                                                config.success.call(
                                                    config.scope || me,
                                                    entry.isDirectory ? Ext.create('Ext.device.filesystem.DirectoryEntry', entry.fullPath, me.fileSystem) : Ext.create('Ext.device.filesystem.FileEntry', entry.fullPath, me.fileSystem)
                                                );
                                            }, config.failure);
                                        } else {
                                            sourceEntry.moveTo(destinationEntry, config.newName, function(entry) {
                                                config.success.call(
                                                    config.scope || me,
                                                    entry.isDirectory ? Ext.create('Ext.device.filesystem.DirectoryEntry', entry.fullPath, me.fileSystem) : Ext.create('Ext.device.filesystem.FileEntry', entry.fullPath, me.fileSystem)
                                                );
                                            }, config.failure);
                                        }
                                    },
                                    failure: config.failure
                                }
                            );
                        },
                        failure: config.failure
                    }
                );
            },

            /**
             * Works the same way as {@link Ext.device.filesystem.Entry#moveTo}, but copies the entry.
             */
            copyTo: function(config) {
                this.moveTo(Ext.apply(config, {
                    copy: true
                }));
            },

            /**
             * Removes the entry from the file system.
             *
             * @param {Object} config
             * The object which contains the following config options:
             *
             * @param {Boolean} config.recursively This is optional
             * Deletes a directory and all of its contents
             *
             * @param {Function} config.success This is optional.
             * The callback to be called when the entry has been successfully removed.
             *
             * @param {Function} config.failure This is optional.
             * The callback to be called when an error occurred.
             *
             * @param {Object} config.failure.error
             * The occurred error.
             *
             * @param {Object} config.scope
             * The scope object
             */
            remove: function(config) {
                this.getEntry(
                    {
                        success: function(entry) {
                            if (config.recursively && this.directory) {
                                entry.removeRecursively(config.success, config.failure)
                            } else {
                                entry.remove(config.success, config.failure)
                            }
                        },
                        failure: config.failure
                    }
                )
            },

            /**
             * Looks up the parent directory containing the entry.
             *
             * @param {Object} config
             * The object which contains the following config options:
             *
             * @param {Function} config.success This is required.
             * The callback to be called when the parent directory has been successfully selected.
             *
             * @param {Ext.device.filesystem.DirectoryEntry} config.success.entry
             * The parent directory of the entry.
             *
             * @param {Function} config.failure This is optional.
             * The callback to be called when an error occurred.
             *
             * @param {Object} config.failure.error
             * The occurred error.
             *
             * @param {Object} config.scope
             * The scope object
             */
            getParent: function(config) {
                if (!config.success) {
                    Ext.Logger.error('Ext.device.filesystem.Entry#getParent: You must specify a `success` callback.');
                    return null;
                }

                var me = this;
                this.getEntry(
                    {
                        options: config.options || {},
                        success: function(entry) {
                            entry.getParent(
                                function(parentEntry) {
                                    config.success.call(
                                        config.scope || me,
                                        parentEntry.isDirectory
                                            ? Ext.create('Ext.device.filesystem.DirectoryEntry', parentEntry.fullPath, me.fileSystem)
                                            : Ext.create('Ext.device.filesystem.FileEntry', parentEntry.fullPath, me.fileSystem)
                                    )
                                },
                                config.failure
                            )

                        },
                        failure: config.failure
                    }
                )
            }
        });

        /**
         * The DirectoryEntry class which is used to represent a directory on a file system.
         */
        Ext.define('Ext.device.filesystem.DirectoryEntry', {
            extend: 'Ext.device.filesystem.Entry',
            cachedDirectory: null,

            constructor: function(path, fileSystem) {
                this.callParent([true, path, fileSystem]);
            },

            /**
             * Requests a Directory from the Local File System
             *
             * @param {Object} config
             *
             * @param {Object} config.options
             * File creation options {create:true, exclusive:false}
             *
             * @param {Boolean} config.options.create
             * Indicates if the directory should be created if it doesn't exist
             *
             * @param {Boolean} config.options.exclusive
             * Used with the create option only indicates whether a creation causes an error if the directory already exists
             *
             * @param {Function} config.success
             * The function called when the Directory is returned successfully
             *
             * @param {Ext.device.filesystem.DirectoryEntry} config.success.directory
             * DirectoryEntry Object
             *
             * @param {Function} config.failure
             * The function called when the Directory request causes an error
             *
             * @param {FileError} config.failure.error
             */
            getEntry: function(config) {
                var me = this;
                var callback = config.success;

                if ((config.options && config.options.create) && this.path) {
                    var folders = this.path.split("/");
                    if (folders[0] == '.' || folders[0] == '') {
                        folders = folders.slice(1);
                    }

                    var recursiveCreation = function(dirEntry) {
                        if (folders.length) {
                            dirEntry.getDirectory(folders.shift(), config.options, recursiveCreation, config.failure);
                        } else {
                            callback(dirEntry);
                        }
                    };

                    recursiveCreation(this.fileSystem.fs.root);
                } else {
                    this.fileSystem.fs.root.getDirectory(this.path, config.options,
                        function(directory) {
                            config.success.call(config.scope || me, directory);
                        },
                        config.failure
                    );
                }
            },

            /**
             * Lists all the entries in the directory.
             *
             * @param {Object} config
             * The object which contains the following config options:
             *
             * @param {Function} config.success This is required.
             * The callback to be called when the entries has been successfully read.
             *
             * @param {Ext.device.filesystem.Entry[]} config.success.entries
             * The array of entries of the directory.
             *
             * @param {Function} config.failure This is optional.
             * The callback to be called when an error occurred.
             *
             * @param {Object} config.failure.error
             * The occurred error.
             *
             * @param {Object} config.scope
             * The scope object
             */
            readEntries: function(config) {
                if (!config.success) {
                    Ext.Logger.error('Ext.device.filesystem.DirectoryEntry#readEntries: You must specify a `success` callback.');
                    return null;
                }

                var me = this;
                this.getEntry(
                    {
                        success: function(dirEntry) {
                            var directoryReader = dirEntry.createReader();
                            directoryReader.readEntries(
                                function(entryInfos) {
                                    var entries = [],
                                        i = 0,
                                        len = entryInfos.length;

                                    for (; i < len; i++) {
                                        entryInfo = entryInfos[i];
                                        entries[i] = entryInfo.isDirectory
                                            ? Ext.create('Ext.device.filesystem.DirectoryEntry', entryInfo.fullPath, me.fileSystem)
                                            : Ext.create('Ext.device.filesystem.FileEntry', entryInfo.fullPath, me.fileSystem);
                                    }
                                    config.success.call(config.scope || this, entries);
                                },
                                function(error) {
                                    if (config.failure) {
                                        config.failure.call(config.scope || this, error);
                                    }
                                }
                            );
                        },
                        failure: config.failure
                    }
                );
            },

            /**
             * Creates or looks up a file.
             *
             * @param {Object} config
             * The object which contains the following config options:
             *
             * @param {String} config.path This is required.
             * The absolute path or relative path from the entry to the file to create or select.
             *
             * @param {Object} config.options This is optional.
             * The object which contains the following options:
             *
             * @param {Boolean} config.options.create This is optional.
             * Indicates whether to create a file, if path does not exist.
             *
             * @param {Boolean} config.options.exclusive This is optional. Used with 'create', by itself has no effect.
             * Indicates that method should fail, if path already exists.
             *
             * @param {Function} config.success This is optional.
             * The callback to be called when the file has been successfully created or selected.
             *
             * @param {Ext.device.filesystem.Entry} config.success.entry
             * The created or selected file.
             *
             * @param {Function} config.failure This is optional.
             * The callback to be called when an error occurred.
             *
             * @param {Object} config.failure.error
             * The occurred error.
             *
             * @param {Object} config.scope
             * The scope object
             */
            getFile: function(config) {
                if (config.path == null) {
                    Ext.Logger.error('Ext.device.filesystem.DirectoryEntry#getFile: You must specify a `path` of the file.');
                    return null;
                }

                var me = this;
                var fullPath = this.path + config.path;
                var fileEntry = Ext.create('Ext.device.filesystem.FileEntry', fullPath, this.fileSystem);
                fileEntry.getEntry(
                    {
                        success: function() {
                            config.success.call(config.scope || me, fileEntry);
                        },
                        options: config.options || {},
                        failure: config.failure
                    }
                )
            },

            /**
             * Works the same way as {@link Ext.device.filesystem.DirectoryEntry#getFile},
             * but creates or looks up a directory.
             */
            getDirectory: function(config) {
                if (config.path == null) {
                    Ext.Logger.error('Ext.device.filesystem.DirectoryEntry#getFile: You must specify a `path` of the file.');
                    return null;
                }

                var me = this;
                var fullPath = this.path + config.path;
                var directoryEntry = Ext.create('Ext.device.filesystem.DirectoryEntry', fullPath, this.fileSystem);
                directoryEntry.getEntry(
                    {
                        success: function() {
                            config.success.call(config.scope || me, directoryEntry);
                        },
                        options: config.options || {},
                        failure: config.failure
                    }
                )
            },

            /**
             * Works the same way as {@link Ext.device.filesystem.Entry#remove},
             * but removes the directory and all of its contents, if any.
             */
            removeRecursively: function(config) {
                this.remove(Ext.apply(config, {
                    recursively: true
                }));
            }
        });

        /**
         * The FileEntry class which is used to represent a file on a file system.
         */
        Ext.define('Ext.device.filesystem.FileEntry', {
            extend: 'Ext.device.filesystem.Entry',

            length: 0,
            offset: 0,

            constructor: function(path, fileSystem) {
                this.callParent([false, path, fileSystem]);
                this.offset = 0;
                this.length = 0;
            },

            /**
             * Requests a File Handle from the Local File System
             *
             * @param {Object} config
             *
             * @param {String} config.file
             * Filename optionally including path in string format '/tmp/debug.txt' or a File Object
             *
             * @param {Object} config.options
             * File creation options {create:true, exclusive:false}
             *
             * @param {Boolean} config.options.create
             * Indicates if the file should be created if it doesn't exist
             *
             * @param {Boolean} config.options.exclusive
             * Used with the create option only indicates whether a creation causes an error if the file already exists
             *
             * @param {Function} config.success
             * The function called when the filesystem is returned successfully
             *
             * @param {FileSystem} config.success.entry
             *
             * @param {Function} config.failure
             * The function called when the filesystem request causes and error
             *
             * @param {FileError} config.failure.error
             *
             */
            getEntry: function(config) {
                var me = this;
                var originalConfig = Ext.applyIf({}, config);
                if (this.fileSystem) {
                    var failure = function(evt) {
                        if ((config.options && config.options.create) && Ext.isString(this.path)) {
                            var folders = this.path.split("/");
                            if (folders[0] == '.' || folders[0] == '') {
                                folders = folders.slice(1);
                            }

                            if (folders.length > 1 && !config.recursive === true) {
                                folders.pop();

                                var dirEntry = Ext.create('Ext.device.filesystem.DirectoryEntry', folders.join("/"), me.fileSystem);
                                dirEntry.getEntry(
                                    {
                                        options: config.options,
                                        success: function() {
                                            originalConfig.recursive = true;
                                            me.getEntry(originalConfig);
                                        },
                                        failure: config.failure
                                    }
                                );
                            } else {
                                if (config.failure) {
                                    config.failure.call(config.scope || me, evt);
                                }
                            }
                        } else {
                            if (config.failure) {
                                config.failure.call(config.scope || me, evt);
                            }
                        }
                    };

                    this.fileSystem.fs.root.getFile(this.path, config.options || null,
                        function(fileEntry) {
                            fileEntry.file(
                                function(file) {
                                    me.length = file.size;
                                    originalConfig.success.call(config.scope || me, fileEntry);
                                },
                                function(error) {
                                    failure.call(config.scope || me, error);
                                }
                            );
                        },
                        function(error) {
                            failure.call(config.scope || me, error);
                        }
                    );
                } else {
                    config.failure({code: -1, message: "FileSystem not Initialized"});
                }
            },

            /**
             * Returns the byte offset into the file at which the next read/write will occur.
             *
             * @return {Number}
             * The file offset.
             */
            getOffset: function() {
                return this.offset;
            },

            /**
             * Sets the byte offset into the file at which the next read/write will occur.
             *
             * @param {Object} config
             * The object which contains the following config options:
             *
             * @param {Number} config.offset This is required.
             * The file offset to set. If negative, the offset back from the end of the file.
             *
             * @param {Function} config.success This is optional.
             * The callback to be called when the file offset has been successfully set.
             *
             * @param {Function} config.failure This is optional.
             * The callback to be called when an error occurred.
             *
             * @param {Object} config.failure.error
             * The occurred error.
             *
             * @param {Object} config.scope
             * The scope object
             */
            seek: function(config) {
                if (config.offset == null) {
                    Ext.Logger.error('Ext.device.filesystem.FileEntry#seek: You must specify an `offset` in the file.');
                    return null;
                }

                this.offset = config.offset || 0;

                if (config.success) {
                    config.success.call(config.scope || this);
                }
            },

            /**
             * Reads the data from the file starting at the file offset.
             *
             * @param {Object} config
             * The object which contains the following config options:
             *
             * @param {Number} config.length This is optional.
             * The length of bytes to read from the file. Defaults to the file's current size if unspecified.
             *
             * @param {String} config.encoding
             * Optional encoding type used only for reading as Text
             *
             * @param {String} config.type
             * Type of reading to use options are "text" (default), "dataURL", "binaryString" and "arrayBuffer"
             *
             * @param {Object} config.reader
             * Optional config params to be applied to a File Reader
             *
             * @param {Function} config.reader.onloadstart
             * @param {Function} config.reader.onloadprogress
             * @param {Function} config.reader.onload
             * @param {Function} config.reader.onabort
             * @param {Function} config.reader.onerror
             * @param {Function} config.reader.onloadend
             *
             * @param {Function} config.success This is optional.
             * The callback to be called when the data has been successfully read.
             *
             * @param {Object} config.success.data
             * The read data.
             *
             * @param {Function} config.failure This is optional.
             * The callback to be called when an error occurred.
             *
             * @param {Object} config.failure.error
             * The occurred error.
             *
             * @param {Object} config.scope
             * The scope object
             */
            read: function(config) {
                var me = this;
                this.getEntry(
                    {
                        success: function(fileEntry) {
                            fileEntry.file(
                                function(file) {
                                    if (Ext.isNumber(config.length)) {
                                        if (Ext.isFunction(file.slice)) {
                                            file = file.slice(me.offset, config.length);
                                        } else {
                                            if (config.failure) {
                                                config.failure.call(config.scope || me, {code: -2, message: "File missing slice functionality"});
                                            }
                                            return;
                                        }
                                    }

                                    var reader = new FileReader();
                                    reader.onloadend = function(evt) {
                                        config.success.call(config.scope || me, evt.target.result);
                                    };

                                    reader.onerror = function(error) {
                                        config.failure.call(config.scope || me, error);
                                    };

                                    if (config.reader) {
                                        reader = Ext.applyIf(reader, config.reader);
                                    }

                                    config.encoding = config.encoding || "UTF8";

                                    switch (config.type) {
                                        default:
                                        case "text":
                                            reader.readAsText(file, config.encoding);
                                            break;
                                        case "dataURL":
                                            reader.readAsDataURL(file);
                                            break;
                                        case "binaryString":
                                            reader.readAsBinaryString(file);
                                            break;
                                        case "arrayBuffer":
                                            reader.readAsArrayBuffer(file);
                                            break;
                                    }
                                },
                                function(error) {
                                    config.failure.call(config.scope || me, error)
                                }
                            );
                        },
                        failure: function(error) {
                            config.failure.call(config.scope || me, error)
                        }
                    }
                )
            },

            /**
             * Writes the data to the file starting at the file offset.
             *
             * @param {Object} config
             * The object which contains the following config options:
             *
             * @param {Object} config.data This is required.
             * The data to write to the file.
             *
             * @param {Boolean} config.append This is optional.
             * Append to the end of the file
             *
             * @param {Object} config.writer
             * Optional config params to be applied to a File Reader
             *
             * @param {Function} config.writer.onwritestart
             * @param {Function} config.writer.onprogress
             * @param {Function} config.writer.onwrite
             * @param {Function} config.writer.onabort
             * @param {Function} config.writer.onerror
             * @param {Function} config.writer.onwriteend
             *
             * @param {Function} config.success This is optional.
             * The callback to be called when the data has been successfully written.
             *
             * @param {Function} config.failure This is optional.
             * The callback to be called when an error occurred.
             *
             * @param {Object} config.failure.error
             * The occurred error.
             *
             * @param {Object} config.scope
             * The scope object
             */
            write: function(config) {
                if (config.data == null) {
                    Ext.Logger.error('Ext.device.filesystem.FileEntry#write: You must specify `data` to write into the file.');
                    return null;
                }

                var me = this;
                this.getEntry(
                    {
                        options: config.options || {},
                        success: function(fileEntry) {
                            fileEntry.createWriter(
                                function(writer) {
                                    writer.onwriteend = function(evt) {
                                        me.length = evt.target.length;
                                        config.success.call(config.scope || me, evt.result);
                                    };

                                    writer.onerror = function(error) {
                                        config.failure.call(config.scope || me, error);
                                    };

                                    if (config.writer) {
                                        writer = Ext.applyIf(writer, config.writer);
                                    }

                                    if (me.offset) {
                                        writer.seek(me.offset);
                                    } else if (config.append) {
                                        writer.seek(me.length);
                                    }

                                    me.writeData (writer, config.data);
                                },
                                function(error) {
                                    config.failure.call(config.scope || me, error)
                                }
                            )
                        },
                        failure: function(error) {
                            config.failure.call(config.scope || me, error)
                        }
                    }
                )
            },

            writeData: function(writer, data) {
                writer.write(new Blob([data]));
            },

            /**
             * Truncates or extends the file to the specified size in bytes.
             * If the file is extended, the added bytes are null bytes.
             *
             * @param {Object} config
             * The object which contains the following config options:
             *
             * @param {Number} config.size This is required.
             * The new file size.
             *
             * @param {Function} config.success This is optional.
             * The callback to be called when the file size has been successfully changed.
             *
             * @param {Function} config.failure This is optional.
             * The callback to be called when an error occurred.
             *
             * @param {Object} config.failure.error
             * The occurred error.
             *
             * @param {Object} config.scope
             * The scope object
             */
            truncate: function(config) {
                if (config.size == null) {
                    Ext.Logger.error('Ext.device.filesystem.FileEntry#write: You must specify a `size` of the file.');
                    return null;
                }

                var me = this;
                //noinspection JSValidateTypes
                this.getEntry(
                    {
                        success: function(fileEntry) {
                            fileEntry.createWriter(
                                function(writer) {
                                    writer.truncate(config.size);
                                    config.success.call(config.scope || me, me);
                                },
                                function(error) {
                                    config.failure.call(config.scope || me, error)
                                }
                            )
                        },
                        failure: function(error) {
                            config.failure.call(config.scope || me, error)
                        }
                    }
                )
            }
        });
    });
});


const _  = require('lodash');
const _M = require('../manager/manager-index.js');

module.exports = function (data) {
  //sorts keys for better readablity
  const sortKeys = function (obj) {
    const keys = _.sortBy(_.keys(obj), function (key) {
      return key;
    });
    return _.zipObject(keys, _.map(keys, function (key) {
      return obj[key];
    }));
  };

  //remove stlye
  data = _.omit(data, ['style', 'buttron']);
  let res = _.defaults(data, {
    height: '60px',
    border: 'initial'
  });
  //@TODO fuck line-height??
  const lineHeight = res.height;

  res = _.defaults(data, {
    background: '#2196f3',
    color: '#fff',
    position: 'relative',
    width: '200px',
    margin: '0rem',
    padding: '0rem',
    'border-radius': '4px',
    'box-shadow': 'initial',
    'font-size': '1rem',
    'font-weight': 'initial',
    'font-family': 'initial',
    'text-align': 'center',
    'text-decoration': 'none',
    'vertical-align': 'middle',
    'line-height': lineHeight,
    cursor: 'pointer',
    overflow: 'hidden',
    'z-index': 0,
    transform: 'translateZ(0)'
  });

  //sort keys
  res = sortKeys(res);

  //add font-smoothing
  if (_M._option.getIn(['global', 'fontSmoothing'])) {
    res = _.defaults(res, {
      'backface-visibility': 'hidden',
      '-webkit-backface-visibility': 'hidden',
      '-moz-osx-font-smoothing': 'grayscale'
    });
  }

  return res;
};


var path = require('path');
var util = require('util');
var semver = require('semver');
var EventEmitter = require('events').EventEmitter;
var fs = require('fs');
var async = require('async');

var SKIP = {};
var BREAK = {};

module.exports = Traveller;

module.exports.Visitor = {
  SKIP: SKIP,
  BREAK: BREAK
};

function Traveller(built_root, options) {
  if (typeof built_root != 'string') {
    return new Error("Must provide built_root");
  }

  EventEmitter.call(this);

  this.built_root = built_root;

  // options
  options = options || {};

  this.options = options;
  this.enableDev = options.dev || options.enableDev;
  this.enableAsync = options.async || options.enableAsync;
  this.stopOnError = options.stop_on_error !== false;

  this.maxDepth = options.max_depth || options.maxDepth || options.depth || Infinity;

  // caches
  this.pkgCache = {};

  this.verCache = {};
}

util.inherits(Traveller, EventEmitter);

Traveller.prototype.topsort = Traveller.prototype.sort = require('./sort');

/**
 * Get tree of dependencies from the root in JSON fromat
 * @param  {Object}   root     package.json object
 * @param  {Function} callback callback function
 */
Traveller.prototype.toJSONTree = function(root, callback) {
  var trees = {};

  this.visit(root, {
      enter: function(node, parent, circular, ref) {
        var key = node.pkg.name + '@' + node.pkg.version + node.from;
        if (!trees[key]) {
          trees[key] = {
            from: node.pkg.name + '@' + node.from,
            version: node.pkg.version
          };
        }

        var n = trees[key];
        if (circular && ref) {
          n = {
            from: node.pkg.name + '@' + node.from,
            version: node.pkg.version,
            circular: true
          };

          var r = trees[ref.pkg.name + '@' + ref.pkg.version + ref.from];
          if (r) {
            r.scoped = true;
          }
        }

        if (parent) {
          var pkey = parent.pkg.name + '@' + parent.pkg.version + parent.from;
          if (!trees[pkey]) {
            trees[pkey] = {
              from: parent.pkg.name + '@' + parent.from,
              version: parent.pkg.version,
              dependencies: {}
            };
          }

          var p = trees[pkey];
          p.dependencies = p.dependencies || {};
          p.dependencies[node.pkg.name] = n;
        }
      }
    },
    function(err) {
      var rootKey = root.name + '@' + root.version + undefined;
      var json = trees[rootKey];
      delete json.from;
      json.name = root.name;
      callback(err, json);
    });
};

/**
 * Visit the dep tree of root with visitor
 * @param  {Object}   root    root of the dep tree
 * @param  {Visitor}  visitor visitor object to travel the dep tree
 * @param  {Object=}  options
 * @param  {Function} done    called when all the node are visited
 */
Traveller.prototype.visit = function(root, visitor, done) {
  if (!root) return done('Root must be provided');
  var self = this;
  process.nextTick(function() {
    self.pkgCache[root.name + "@" + root.version] = root;

    self.__visit({
      pkg: root
    }, null, visitor.enter, visitor.leave, function(err) {
      if (typeof visitor.complete == 'function') {
        visitor.complete.call(this, err, root);
      }
      done && done(err);
    });
  });
};

Traveller.prototype.__visit = function(child, parent, enter, leave, done, depths) {
  depths = depths || [];

  if (depths.length > this.maxDepth) {
    return done();
  }

  var self = this;
  var pkg = child.pkg;
  var from = child.from;
  var pkgDeps = this.options.pkgDeps || defaultPkgDeps;

  // try to find child node in the path if there is a cycle existed
  for (var i = depths.length - 1; i >= 0; --i) {
    var d = depths[i];
    if (d.pkg.name == pkg.name && d.pkg.version == pkg.version) {
      // cycle
      if (typeof enter == 'function') {
        var ret, err;
        try {
          ret = enter.call(this, child, parent, true, d);
          if (typeof leave == 'function')
            leave.call(this, child, parent, true, d);
        } catch (e) {
          err = e;
        } finally {
          return done(err, ret);
        }
      }
    }
  }

  self.pkgCache[pkg.name + '@' + pkg.version] = pkg;

  // enter
  if (typeof enter == 'function') {
    var ret, err;
    try {
      ret = enter.call(this, child, parent);
    } catch (e) {
      err = e;
    } finally {
      if (ret === BREAK || ret === SKIP) {
        if (typeof leave == 'function')
          leave.call(this, child, parent);
        return done(null, ret);
      } else if (err)
        return done(err);
    }
  }

  var dependencies = pkgDeps.bind(this)(pkg);

  var breaked = false;
  async.parallel(Object.keys(dependencies).map(function(name) {
      var range = dependencies[name];
      return function(cb) {
        if (breaked) return cb();
        self.resolvePackage(name, range, function(err, dep) {
          if (err) {
            return cb(err);
          }

          // skip
          if (!dep) {
            self.emit('unmet', name, range, child.pkg.name, child.pkg.version, child.from);
            return cb();
          }

          self.__visit({
            pkg: dep,
            from: range
          }, {
            pkg: pkg,
            from: child.from
          }, enter, leave, function(err, ret) {
            if (ret === BREAK)
              breaked = true;
            cb(err);
          }, depths.concat(child));
        });
      };
    }),
    // all dependencies done
    function(err) {
      if (err) return done(err);

      var ret;
      if (typeof leave == 'function') {
        try {
          ret = leave.call(this, child, parent);
        } catch (err) {
          return done(err);
        }
      }

      done(null, ret);
    });
};

Traveller.prototype.resolvePackage = function(name, range, callback) {
  var self = this;
  self.resolveRange(name, range, function(err, version) {
    if (err) {
      if (self.stopOnError) {
        return callback(err);
      } else {
        self.emit("depErr", name, range, err);
        return callback();
      }
    }

    var key = name + '@' + version;
    if (self.pkgCache[key]) {
      callback(null, self.pkgCache[key]);
    } else {
      var ctx_json = path.join(self.built_root, name, version, 'cortex.json');
      fs.exists(ctx_json, function(exists) {
        // error that can not continue and recover
        if (!exists) return callback(new Error("'" + ctx_json + "' does not exist"));
        var pkg;
        try {
          pkg = require(ctx_json);
        } catch (e) {
          // error that can not continue and recover
          return callback(e);
        }

        self.pkgCache[key] = pkg;
        callback(null, pkg);
      });
    }
  });
};

Traveller.prototype.resolveRange = function(name, range, callback) {
  var self = this;
  (function(cb) {
    if (self.verCache[name]) {
      cb(null, self.verCache[name]);
    } else {

      self.readVersions(name, cb);
    }
  })(function(err, versions) {
    if (err) return callback(err);

    var ver = semver.maxSatisfying(versions, range);

    if (!ver) {
      // TODO: whether go to registry, issue #1
      return callback(new Error("Can not resolve package " + name + " from range: " + range + " in available versions: " + versions));
    }

    callback(null, ver);
  });
};

Traveller.prototype.readVersions = function(name, cb) {
  var self = this,
    verCache = self.verCache || {};
  // no multi process
  if (verCache[name]) {
    cb(null, verCache[name]);
  } else {
    var pkg_root = path.join(self.built_root, name);
    fs.exists(pkg_root, function(exists) {
      if (!exists)
        return cb(new Error("No pacakge '" + name + "' installed, please run 'cortex install' first"));

      fs.readdir(pkg_root, function(err, files) {
        if (err) return cb(err);

        var vers = files.filter(semver.valid);
        cb(null, verCache[name] = vers);
      });
    });
  }
};

function defaultPkgDeps(pkg) {
  var dependencies = {};
  if (pkg.dependencies) {
    for (var d in pkg.dependencies) {
      dependencies[d] = pkg.dependencies[d];
    }
  }

  if (this.enableDev) {
    var devDependencies = pkg.devDependencies || {};
    for (var d in devDependencies) {
      dependencies[d] = devDependencies[d];
    }
  } else if (!isEmptyObject(pkg.devDependencies)) {
    for (var d in pkg.devDependencies) {
      this.emit("ignoreDev", d);
    }
  }

  if (this.enableAsync) {
    var asyncDependencies = pkg.asyncDependencies || {};
    for (var ad in asyncDependencies) {
      dependencies[ad] = asyncDependencies[ad];
    }
  } else if (pkg.asyncDependencies) {
    for (var ad in pkg.asyncDependencies) {
      this.emit("ignoreAsync", ad);
    }
  }
  return dependencies;
}

function isEmptyObject(obj) {
  if (!obj) return true;
  for (var p in obj) {
    if (obj.hasOwnProperty(p))
      return false;
  }

  return true;
}


'use strict';

var stringify = require('json-stable-stringify');
var LRU = require('lru-cache');

/**
 * @module mkc
 * @author Ryan Sandor Richards
 */
module.exports = MultiKeyCache;

/**
 * Multi-Key LRU cache.
 * @class
 * @param {object} options Options for the cache.
 */
function MultiKeyCache(options) {
  options = options || {};

  var self = this;
  var dispose = options.dispose;

  options.dispose = function (key, value) {
    self._dispose(key);
    if (dispose) { dispose(key, value); }
  };

  this.cache = new LRU(options);
  this._keyMap = {};
}

/**
 * Hashes a set of key values into a single key.
 * @param {object} keyValues Key values to hash.
 * @return {string} The hash string for the set of key values.
 */
function hash(keyValues) {
  return stringify(keyValues);
}

/**
 * Parses a hashed key value set.
 * @param {string} key Key to parse.
 * @return {object} Set of key values represented by the key.
 */
function parse(key) {
  return JSON.parse(key);
}

/**
 * Removes mappings when objects are disposed by the LRU.
 * @param {string} keyHash Key hash for the disposed object.
 */
MultiKeyCache.prototype._dispose = function (keyHash) {
  var keyValues = parse(keyHash);
  for (var key in keyValues) {
    var value = keyValues[key];
    if (!this._keyMap[key]) {
      continue;
    }
    if (!Array.isArray(this._keyMap[key][value])) {
      continue;
    }
    var map = this._keyMap[key][value];
    var index = map.indexOf(keyHash);
    if (~index) {
      map.splice(index, 1);
    }
    if (map.length === 0) {
      delete this._keyMap[key][value];
    }
  }
};

/**
 * Puts an object into the cache with the given key values.
 * @param {object} keyValues Key values that identify the object in the cache.
 * @param object Object to store in the cache.
 */
MultiKeyCache.prototype.set = function (keyValues, object) {
  var keyHash = hash(keyValues);

  for (var key in keyValues) {
    var value = keyValues[key];
    if (!this._keyMap[key]) {
      this._keyMap[key] = {};
    }
    if (!this._keyMap[key][value]) {
      this._keyMap[key][value] = [];
    }
    this._keyMap[key][value].push(keyHash);
  }

  this.cache.set(keyHash, object);
};

/**
 * Gets and object from the cache with the given key values.
 * @param {Object} keyValues Key values that identify the object in the cache.
 * @return If found, the object with the given key values. `undefined`
 *   otherwise.
 */
MultiKeyCache.prototype.get = function (keyValues) {
  var keyHash = hash(keyValues);
  return this.cache.get(keyHash);
};

/**
 * Determines if an object exists in the cache for the given key values.
 * @param {Object} keyValues Key values that identify the object in the cache.
 * @return {Boolean} `true` if found, `false` otherwise.
 */
MultiKeyCache.prototype.has = function (keyValues) {
  var keyHash = hash(keyValues);
  return this.cache.has(keyHash);
};

/**
 * Removes all objects with the given key values.
 *
 * @example
 * // Add two objects
 * cache.set({ type: 'string', id: 1 }, 'one');
 * cache.set({ type: 'number', id: 1 }, 1);
 *
 * // Remove only objects with the key `type === 'string'`
 * // (only the object with type number will remain...)
 * cache.purge({ type: 'string' });
 *
 * @param {Object} keyValues Key values to identify the objects to remove.
 */
MultiKeyCache.prototype.purge = function (keyValues) {
  var delKeyHashSet = {};

  for (var key in keyValues) {
    var value = keyValues[key];

    if (!this._keyMap[key]) {
      continue;
    }
    if (!Array.isArray(this._keyMap[key][value])) {
      continue;
    }

    var map = this._keyMap[key][value];
    for (var i = 0; i < map.length; i++) {
      delKeyHashSet[map[i]] = true;
    }
  }

  for (var keyHash in delKeyHashSet) {
    this.cache.del(keyHash);
  }
};

/**
 * Clears the entire cache.
 */
MultiKeyCache.prototype.reset = function () {
  this.cache.reset();
  this._keyMap = {};
};

/**
 * Reports the number of items in the multi-key cache.
 * @return The number of items in the cache.
 */
MultiKeyCache.prototype.itemCount = function () {
  return this.cache.itemCount;
};

/**
 * Reports the total length of the items in the multi-key cache.
 * @return The length of the items in the cache.
 */
MultiKeyCache.prototype.length = function () {
  return this.cache.length;
};


(function (global, factory) {
  if (typeof define === "function" && define.amd) {
    define(['exports'], factory);
  } else if (typeof exports !== "undefined") {
    factory(exports);
  } else {
    var mod = {
      exports: {}
    };
    factory(mod.exports);
    global.decoratorsJs = mod.exports;
  }
})(this, function (exports) {
  'use strict';

  Object.defineProperty(exports, "__esModule", {
    value: true
  });

  var _slicedToArray = function () {
    function sliceIterator(arr, i) {
      var _arr = [];
      var _n = true;
      var _d = false;
      var _e = undefined;

      try {
        for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
          _arr.push(_s.value);

          if (i && _arr.length === i) break;
        }
      } catch (err) {
        _d = true;
        _e = err;
      } finally {
        try {
          if (!_n && _i["return"]) _i["return"]();
        } finally {
          if (_d) throw _e;
        }
      }

      return _arr;
    }

    return function (arr, i) {
      if (Array.isArray(arr)) {
        return arr;
      } else if (Symbol.iterator in Object(arr)) {
        return sliceIterator(arr, i);
      } else {
        throw new TypeError("Invalid attempt to destructure non-iterable instance");
      }
    };
  }();

  function _toConsumableArray(arr) {
    if (Array.isArray(arr)) {
      for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) {
        arr2[i] = arr[i];
      }

      return arr2;
    } else {
      return Array.from(arr);
    }
  }

  /*
   *Common decorators and combinators I use in Javascript
   *@author Jared Smith
   *@copyright Jared Adam Smith, 2015, 2016
   *Licensed under the MIT license. You should have received a copy with this software, otherwise see
   *https://opensource.org/licenses/MIT.
   *
   */

  // Extracts internal [[class]] property of a javascript value
  // _extractHiddenClass :: * -> String
  var _extractHiddenClass = function (r) {
    return function (a) {
      return Object.prototype.toString.call(a).match(r)[1];
    };
  }(/ ([a-z]+)]$/i);

  var _applyConstructor = function _applyConstructor(ctor, args) {
    return new (ctor.bind.apply(ctor, [ctor].concat(_toConsumableArray(args))))();
  };

  // NOTE: the 'this' context will be determined at the time the curried function receives its
  // **final** argument.
  var curry = function curry(len, f) {
    for (var _len = arguments.length, initArgs = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
      initArgs[_key - 2] = arguments[_key];
    }

    var _ref = function () {
      switch (true) {
        case _extractHiddenClass(len) === 'Function':
          var args = f == null ? [] : [f].concat(initArgs);
          return [len, len.length, args];
        case _extractHiddenClass(len) === 'Number':
          return [f, len, initArgs];
        default:
          throw new TypeError('Unrecognized arguments ' + len + ' and ' + f + ' to function curry.');
      }
    }(),
        _ref2 = _slicedToArray(_ref, 3),
        fn = _ref2[0],
        arity = _ref2[1],
        fnArgs = _ref2[2];

    if (!fn) {
      return function (fn) {
        for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
          args[_key2 - 1] = arguments[_key2];
        }

        return curry.apply(this, [arity, fn].concat(args));
      };
    }

    var helper = function helper(args) {
      return function () {
        for (var _len3 = arguments.length, rest = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
          rest[_key3] = arguments[_key3];
        }

        return currier.call(this, arity, fn, [].concat(_toConsumableArray(args), rest));
      };
    };

    var currier = function currier(length, f, args) {
      if (args.length >= length) {

        // ES 6 classes and built-ins, real or polyfilled, throw a TypeError if you try to call them
        // as a function.
        try {
          return f.apply(this, args);
        } catch (e) {
          if (e instanceof TypeError) {
            return _applyConstructor(f, args);
          } else {
            throw e;
          }
        }
      } else {
        return helper(args);
      }
    };

    return currier.call(this, arity, fn, fnArgs);
  };

  var apply = curry(function (f, args) {
    return f.apply(this, args);
  });

  //debounce :: Number -> (* -> Null) -> Number
  //Delay in milliseconds. Returns the timer ID so caller can cancel
  var debounce = function debounce(n, immed, f) {
    var _ref3 = function () {
      switch (_extractHiddenClass(immed)) {
        case 'Boolean':
          return [f, immed];
        case 'Function':
          return [immed, false];
        default:
          throw new TypeError('Unrecognized arguments ' + immed + ' and ' + f + ' to function debounce.');
      }
    }(),
        _ref4 = _slicedToArray(_ref3, 2),
        fn = _ref4[0],
        now = _ref4[1];

    var timer = null;
    return function () {
      var _this = this;

      for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
        args[_key4] = arguments[_key4];
      }

      if (timer === null && now) {
        fn.apply(this, args);
      }
      clearTimeout(timer);
      timer = setTimeout(function () {
        return fn.apply(_this, args);
      }, n);
      return timer;
    };
  };

  //throttle :: Number -> (* -> Null) -> Number
  //Delay in milliseconds. Returns the timer ID so caller can cancel
  var throttle = curry(function (delay, fn) {
    var timer = null,
        last = null;
    return function () {
      var _this2 = this;

      for (var _len5 = arguments.length, args = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
        args[_key5] = arguments[_key5];
      }

      var now = Date.now();
      if (last != null && now < last + delay) {
        clearTimeout(timer);
        timer = setTimeout(function () {
          last = now;
          fn.apply(_this2, args);
        }, delay);
      } else {
        last = now;
        fn.apply(this, args);
      }
      return timer;
    };
  });

  // pipe
  // Forward function composition.
  var pipe = function pipe() {
    for (var _len6 = arguments.length, fs = Array(_len6), _key6 = 0; _key6 < _len6; _key6++) {
      fs[_key6] = arguments[_key6];
    }

    return function () {
      var _this3 = this;

      var first = fs.shift();

      for (var _len7 = arguments.length, args = Array(_len7), _key7 = 0; _key7 < _len7; _key7++) {
        args[_key7] = arguments[_key7];
      }

      return fs.reduce(function (acc, f) {
        return f.call(_this3, acc);
      }, first.apply(this, args));
    };
  };

  //denodeify :: (* -> *) -> (* -> Promise *)
  //Turns a callback-accepting function into one that returns a Promise.
  var denodeify = function denodeify(fn) {
    var length = fn.length > 0 ? fn.length - 1 : 0;
    var f = function f() {
      var _this4 = this;

      for (var _len8 = arguments.length, args = Array(_len8), _key8 = 0; _key8 < _len8; _key8++) {
        args[_key8] = arguments[_key8];
      }

      return new Promise(function (resolve, reject) {
        fn.apply(_this4, [].concat(args, [function (err) {
          for (var _len9 = arguments.length, rest = Array(_len9 > 1 ? _len9 - 1 : 0), _key9 = 1; _key9 < _len9; _key9++) {
            rest[_key9 - 1] = arguments[_key9];
          }

          if (err) {
            reject(err);
          }
          var result = void 0;
          switch (rest.length) {
            case 0:
              result = true;
              break;
            case 1:
              result = rest[0];
              break;
            default:
              result = rest;
              break;
          }
          resolve(result);
        }]));
      });
    };

    return length ? curry(length, f) : f;
  };

  //trampoline :: (* -> *) -> (* -> *)
  var trampoline = function trampoline(fn) {
    return curry(fn.length, function () {
      for (var _len10 = arguments.length, args = Array(_len10), _key10 = 0; _key10 < _len10; _key10++) {
        args[_key10] = arguments[_key10];
      }

      var result = fn.apply(this, args);
      while (_extractHiddenClass(result) === 'Function') {
        result = result();
      }
      return result;
    });
  };

  var bindArity = curry(function (n, f) {
    return function () {
      for (var _len11 = arguments.length, args = Array(_len11), _key11 = 0; _key11 < _len11; _key11++) {
        args[_key11] = arguments[_key11];
      }

      return f.apply(this, args.slice(0, n));
    };
  });

  var unary = bindArity(1);

  var _memoized = {};
  var memoize = function memoize(f) {
    return function () {
      for (var _len12 = arguments.length, arr = Array(_len12), _key12 = 0; _key12 < _len12; _key12++) {
        arr[_key12] = arguments[_key12];
      }

      var args = arr.map(function (arg) {
        var s = arg.toString();
        if (s === '[object Object]') {
          throw new TypeError('Unhashable argument to function memoize.');
        }
        return s;
      });

      var str = args.join('');
      var m = _memoized[str];
      if (m) {
        return m;
      } else {
        var res = f.apply(this, arr);
        _memoized[str] = res;
        return res;
      }
    };
  };

  var maybe = function maybe(f) {
    var strict = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;

    var method = strict ? 'some' : 'every';
    return function () {
      for (var _len13 = arguments.length, args = Array(_len13), _key13 = 0; _key13 < _len13; _key13++) {
        args[_key13] = arguments[_key13];
      }

      if (f.length && args[method](function (a) {
        return a == null;
      })) return null;
      return f.apply(this, args);
    };
  };

  var applyConstructor = curry(_applyConstructor);

  var bindPromise = function bindPromise(f) {
    return function (p) {
      var _this5 = this;

      return p.then(function (arg) {
        return f.call(_this5, arg);
      });
    };
  };

  exports.trampoline = trampoline;
  exports.pipe = pipe;
  exports.curry = curry;
  exports.bindArity = bindArity;
  exports.unary = unary;
  exports.debounce = debounce;
  exports.throttle = throttle;
  exports.applyConstructor = applyConstructor;
  exports.apply = apply;
  exports.denodeify = denodeify;
  exports.memoize = memoize;
  exports.maybe = maybe;
  exports.bindPromise = bindPromise;
});


/**
 * @author Ed Spencer
 *
 * Proxies are used by {@link Ext.data.Store Stores} to handle the loading and saving of {@link Ext.data.Model Model}
 * data. Usually developers will not need to create or interact with proxies directly.
 *
 * # Types of Proxy
 *
 * There are two main types of Proxy - {@link Ext.data.proxy.Client Client} and {@link Ext.data.proxy.Server Server}.
 * The Client proxies save their data locally and include the following subclasses:
 *
 * - {@link Ext.data.proxy.LocalStorage LocalStorageProxy} - saves its data to localStorage if the browser supports it
 * - {@link Ext.data.proxy.Memory MemoryProxy} - holds data in memory only, any data is lost when the page is refreshed
 *
 * The Server proxies save their data by sending requests to some remote server. These proxies include:
 *
 * - {@link Ext.data.proxy.Ajax Ajax} - sends requests to a server on the same domain
 * - {@link Ext.data.proxy.JsonP JsonP} - uses JSON-P to send requests to a server on a different domain
 *
 * Proxies operate on the principle that all operations performed are either Create, Read, Update or Delete. These four
 * operations are mapped to the methods {@link #create}, {@link #read}, {@link #update} and {@link #destroy}
 * respectively. Each Proxy subclass implements these functions.
 *
 * The CRUD methods each expect an {@link Ext.data.Operation Operation} object as the sole argument. The Operation
 * encapsulates information about the action the Store wishes to perform, the {@link Ext.data.Model model} instances
 * that are to be modified, etc. See the {@link Ext.data.Operation Operation} documentation for more details. Each CRUD
 * method also accepts a callback function to be called asynchronously on completion.
 *
 * Proxies also support batching of Operations via a {@link Ext.data.Batch batch} object, invoked by the {@link #batch}
 * method.
 *
 * ###Further Reading
 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
 */
Ext.define('Ext.data.proxy.Proxy', {
    extend: 'Ext.Evented',

    alias: 'proxy.proxy',

    alternateClassName: ['Ext.data.DataProxy', 'Ext.data.Proxy'],

    requires: [
        'Ext.data.reader.Json',
        'Ext.data.writer.Json',
        'Ext.data.Batch',
        'Ext.data.Operation'
    ],

    config: {
        /**
         * @cfg {String} batchOrder
         * Comma-separated ordering 'create', 'update' and 'destroy' actions when batching. Override this to set a different
         * order for the batched CRUD actions to be executed in.
         * @accessor
         */
        batchOrder: 'create,update,destroy',

        /**
         * @cfg {Boolean} batchActions
         * True to batch actions of a particular type when synchronizing the store.
         * @accessor
         */
        batchActions: true,

        /**
         * @cfg {String/Ext.data.Model} model (required)
         * The name of the Model to tie to this Proxy. Can be either the string name of the Model, or a reference to the
         * Model constructor.
         * @accessor
         */
        model: null,

        /**
         * @cfg {Object/String/Ext.data.reader.Reader} reader
         * The Ext.data.reader.Reader to use to decode the server's response or data read from client. This can either be a
         * Reader instance, a config object or just a valid Reader type name (e.g. 'json', 'xml').
         * @accessor
         */
        reader: {
            type: 'json'
        },

        /**
         * @cfg {Object/String/Ext.data.writer.Writer} writer
         * The Ext.data.writer.Writer to use to encode any request sent to the server or saved to client. This can either be
         * a Writer instance, a config object or just a valid Writer type name (e.g. 'json', 'xml').
         * @accessor
         */
        writer: {
            type: 'json'
        }
    },

    isProxy: true,

    applyModel: function(model) {
        if (typeof model == 'string') {
            model = Ext.data.ModelManager.getModel(model);

            if (!model) {
                Ext.Logger.error('Model with name ' + arguments[0] + ' doesnt exist.');
            }
        }

        if (model && !model.prototype.isModel && Ext.isObject(model)) {
            model = Ext.data.ModelManager.registerType(model.storeId || model.id || Ext.id(), model);
        }

        return model;
    },

    updateModel: function(model) {
        if (model) {
            var reader = this.getReader();
            if (reader && !reader.getModel()) {
                reader.setModel(model);
            }
        }
    },

    applyReader: function(reader, currentReader) {
        return Ext.factory(reader, Ext.data.Reader, currentReader, 'reader');
    },

    updateReader: function(reader) {
        if (reader) {
            var model = this.getModel();
            if (!model) {
                model = reader.getModel();
                if (model) {
                    this.setModel(model);
                }
            } else {
                reader.setModel(model);
            }

            if (reader.onMetaChange) {
                 reader.onMetaChange = Ext.Function.createSequence(reader.onMetaChange, this.onMetaChange, this);
            }
        }
    },

    onMetaChange: function(data) {
        var model = this.getReader().getModel();
        if (!this.getModel() && model) {
            this.setModel(model);
        }

        /**
         * @event metachange
         * Fires whenever the server has sent back new metadata to reconfigure the Reader.
         * @param {Ext.data.Proxy} this
         * @param {Object} data The metadata sent back from the server
         */
        this.fireEvent('metachange', this, data);
    },

    applyWriter: function(writer, currentWriter) {
        return Ext.factory(writer, Ext.data.Writer, currentWriter, 'writer');
    },

    /**
     * Performs the given create operation. If you override this method in a custom Proxy, remember to always call the provided
     * callback method when you are done with your operation.
     * @param {Ext.data.Operation} operation The Operation to perform
     * @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not)
     * @param {Object} scope Scope to execute the callback function in
     * @method
     */
    create: Ext.emptyFn,

    /**
     * Performs the given read operation. If you override this method in a custom Proxy, remember to always call the provided
     * callback method when you are done with your operation.
     * @param {Ext.data.Operation} operation The Operation to perform
     * @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not)
     * @param {Object} scope Scope to execute the callback function in
     * @method
     */
    read: Ext.emptyFn,

    /**
     * Performs the given update operation. If you override this method in a custom Proxy, remember to always call the provided
     * callback method when you are done with your operation.
     * @param {Ext.data.Operation} operation The Operation to perform
     * @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not)
     * @param {Object} scope Scope to execute the callback function in
     * @method
     */
    update: Ext.emptyFn,

    /**
     * Performs the given destroy operation. If you override this method in a custom Proxy, remember to always call the provided
     * callback method when you are done with your operation.
     * @param {Ext.data.Operation} operation The Operation to perform
     * @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not)
     * @param {Object} scope Scope to execute the callback function in
     * @method
     */
    destroy: Ext.emptyFn,

    onDestroy: function() {
        Ext.destroy(this.getReader(), this.getWriter());
        Ext.Evented.prototype.destroy.apply(this, arguments);
    },

    /**
     * Performs a batch of {@link Ext.data.Operation Operations}, in the order specified by {@link #batchOrder}. Used
     * internally by {@link Ext.data.Store}'s {@link Ext.data.Store#sync sync} method. Example usage:
     *
     *     myProxy.batch({
     *         create : [myModel1, myModel2],
     *         update : [myModel3],
     *         destroy: [myModel4, myModel5]
     *     });
     *
     * Where the myModel* above are {@link Ext.data.Model Model} instances - in this case 1 and 2 are new instances and
     * have not been saved before, 3 has been saved previously but needs to be updated, and 4 and 5 have already been
     * saved but should now be destroyed.
     *
     * @param {Object} options Object containing one or more properties supported by the batch method:
     *
     * @param {Object} options.operations Object containing the Model instances to act upon, keyed by action name
     *
     * @param {Object} [options.listeners] Event listeners object passed straight through to the Batch -
     * see {@link Ext.data.Batch} for details
     *
     * @param {Ext.data.Batch/Object} [options.batch] A {@link Ext.data.Batch} object (or batch config to apply
     * to the created batch). If unspecified a default batch will be auto-created.
     *
     * @param {Function} [options.callback] The function to be called upon completion of processing the batch.
     * The callback is called regardless of success or failure and is passed the following parameters:
     * @param {Ext.data.Batch} options.callback.batch The {@link Ext.data.Batch batch} that was processed,
     * containing all operations in their current state after processing
     * @param {Object} options.callback.options The options argument that was originally passed into batch
     *
     * @param {Function} [options.success] The function to be called upon successful completion of the batch. The
     * success function is called only if no exceptions were reported in any operations. If one or more exceptions
     * occurred then the `failure` function will be called instead. The success function is called
     * with the following parameters:
     * @param {Ext.data.Batch} options.success.batch The {@link Ext.data.Batch batch} that was processed,
     * containing all operations in their current state after processing
     * @param {Object} options.success.options The options argument that was originally passed into batch
     *
     * @param {Function} [options.failure] The function to be called upon unsuccessful completion of the batch. The
     * failure function is called when one or more operations returns an exception during processing (even if some
     * operations were also successful). The failure function is called with the following parameters:
     * @param {Ext.data.Batch} options.failure.batch The {@link Ext.data.Batch batch} that was processed,
     * containing all operations in their current state after processing
     * @param {Object} options.failure.options The options argument that was originally passed into batch
     *
     * @param {Object} [options.scope] The scope in which to execute any callbacks (i.e. the `this` object inside
     * the callback, success and/or failure functions). Defaults to the proxy.
     *
     * @param {Object} [options.params] External parameters for proxy fine tuning
     *
     * @return {Ext.data.Batch} The newly created Batch
     */
    batch: function(options, /* deprecated */listeners) {
        var me = this,
            params = options.params,
            useBatch = me.getBatchActions(),
            model = me.getModel(),
            batch,
            records;

        if (options.operations === undefined) {
            // the old-style (operations, listeners) signature was called
            // so convert to the single options argument syntax
            options = {
                operations: options,
                listeners: listeners
            };

            // <debug warn>
            Ext.Logger.deprecate('Passes old-style signature to Proxy.batch (operations, listeners). Please convert to single options argument syntax.');
            // </debug>
        }

        if (options.batch && options.batch.isBatch) {
            batch = options.batch;
        } else {
            batch = new Ext.data.Batch(options.batch || {});
        }

        batch.setProxy(me);

        batch.on('complete', Ext.bind(me.onBatchComplete, me, [options], 0));
        if (options.listeners) {
            batch.on(options.listeners);
        }

        Ext.each(me.getBatchOrder().split(','), function(action) {
             records = options.operations[action];
             if (records) {
                 if (useBatch) {
                     var operationOptions = {
                         action: action,
                         records: records,
                         model: model
                     };
                     if (params) {
                         operationOptions.params = params;
                     }
                     batch.add(new Ext.data.Operation(operationOptions));
                 } else {
                     Ext.each(records, function(record) {
                         var operationOptions = {
                             action: action,
                             records: [record],
                             model: model
                         };
                         if (params) {
                             operationOptions.params = params;
                         }
                         batch.add(new Ext.data.Operation(operationOptions));
                     });
                 }
             }
        }, me);

        batch.start();
        return batch;
    },

    /**
      * @private
      * The internal callback that the proxy uses to call any specified user callbacks after completion of a batch
      */
    onBatchComplete: function(batchOptions, batch) {
         var scope = batchOptions.scope || this;

         if (batch.hasException) {
             if (Ext.isFunction(batchOptions.failure)) {
                 Ext.callback(batchOptions.failure, scope, [batch, batchOptions]);
             }
         } else if (Ext.isFunction(batchOptions.success)) {
             Ext.callback(batchOptions.success, scope, [batch, batchOptions]);
         }

         if (Ext.isFunction(batchOptions.callback)) {
             Ext.callback(batchOptions.callback, scope, [batch, batchOptions]);
         }

         Ext.destroy(batch);
    }

    // <deprecated product=touch since=2.0>
    ,onClassExtended: function(cls, data) {
        var prototype = this.prototype,
            defaultConfig = prototype.config,
            config = data.config || {},
            key;

        // Convert deprecated properties in application into a config object
        for (key in defaultConfig) {
            if (key != "control" && key in data) {
                config[key] = data[key];
                delete data[key];
                // <debug warn>
                Ext.Logger.warn(key + ' is deprecated as a property directly on the ' + this.$className + ' prototype. Please put it inside the config object.');
                // </debug>
            }
        }
        data.config = config;
    }
    // </deprecated>
}, function() {
    // Ext.data2.proxy.ProxyMgr.registerType('proxy', this);

    //backwards compatibility
    // Ext.data.DataProxy = this;
    // Ext.deprecate('platform', '2.0', function() {
    //     Ext.data2.DataProxy = this;
    // }, this);
});


/**
 * Creates a raw HTML string for a template. A comment containing the el configuration can be inserted into it.
 *
 * By default, the comment is inserted at the beginning of the HTML string.
 *
 * @param   {Function} createTemplateFn  the function returning the template HTML, except for the el comment. It  must
 *                                       be possible to pass the el comment as an insertion. Expected signature:
 *                                       createTemplateFn( templateLanguage, insertion )
 * @param   {string} templateLanguage    currently supported: "Handlebars", "EJS" (EJS), "ES6". It is up to the template
 *                                       creation function what to make of it (e.g., can be ignored if there are no
 *                                       template vars)
 * @param   {Object} elCommentConfig
 * @param   {Function} [elCommentConfig.createContent]    a function generating the content of the comment which defines
 *                                                        the el, except for the outer comment tags (not wrapped in
 *                                                        ""). Called with the data attributes (as a hash).
 *                                                        Expected signature: createContent( dataAttributes )
 * @param   {boolean}  [elCommentConfig.noComment=false]  flag: comment should be omitted
 * @param   {boolean}  [elCommentConfig.trailing=false]   flag: comment should be placed at the end of the HTML
 * @param   {boolean}  [elCommentConfig.among=false]      flag: comment should be placed inside the HTML
 * @param   {Object}   [dataAttributes]                   hash containing the data attributes to be set in the comment
 *
 * @returns {string}
 */
function createRawHtml( createTemplateFn, templateLanguage, elCommentConfig, dataAttributes ) {
    // Construct the HTML string
    var comment = elCommentConfig.noComment ? "" : "",

        insertion = elCommentConfig.among ? comment : "",
        isLeading = ! elCommentConfig.trailing && !elCommentConfig.among,
        isTrailing = elCommentConfig.trailing,

        baseTemplate = createTemplateFn( templateLanguage, insertion );

    return isLeading ? comment + baseTemplate : isTrailing ? baseTemplate + comment : baseTemplate;
}

/**
 * Creates the content of a complex template. It should contain as many pitfalls for correct processing as possible.
 *
 * Features:
 *
 * - has a top-level comment (single line)
 * - has a top-level comment (multi-line)
 * - has a comment containing a tag
 * - has numerous top-level tags
 * - has text at top level, not enclosed in another tag
 * - has tags nested in invalid ways, creating invalid HTML (
 inside
)
 * - has significant whitespace which must be preserved
 * - has template vars inside a tag itself (defining attributes)
 * - has a template var define the tag type
 * - has templating instructions (if/else)
 * - has a script tag inside the template, containing Javascript
 * - has a reference to a partial template
 *
 * NB For ES6 templates, only the start and end delimiters used for a variable are supported, other constructs (if, loop,
 * partial) are omitted here.
 *
 * @param   {string} templateLanguage          values are "Handlebars", "EJS" (EJS), "ES6"
 * @param   {object} [options]
 * @param   {string} [options.indentation=""]  a string of whitespace, e.g. "   " or "" (no indentation)
 * @param   {string} [options.insertion=""]    an additional string which is inserted somewhere in the middle of the
 *                                             content (if left undefined, a blank line, plus insertion, appears instead)
 * @returns {string}
 */
function createComplexTemplate ( templateLanguage, options ) {
    var t = getTemplateLanguageConstructs( templateLanguage ),

        indent = options && options.indentation || "",
        insert = options && options.insertion || "",

        lines = [
            '',
            '',

            t.if,
            '
This is a %%paragraph',
            'Some random %%text&& with different line breaks.

',
            t.else,
            '
This is a %%header&&
    ',
            t.endIf,
            t.if,
            '
',
            t.endIf,

            insert,

            'Some top-level %%text&&, not wrapped in a tag.

',
            '',
            "<" + "script>alert( 'foo' );</" + "script>",
            '
',
            '  some text
',
            '<%%tagName&& %%attrs&&>lorem ipsum</%%tagName&&>',
            '

Invalid nesting

',

            t.partial,

            '
',
            '  ' + t.loop,
            '
%%dd_name&&
',
            '
%%dd_content&&
',
            '  ' + t.endLoop,
            '
'
        ],

        innerContent = _.map( lines, function ( line ) {
            return indent + line;
        } ).join( "\n" );

    return innerContent.replace( /%%/g, t.startDelimiter ).replace( /&&/g, t.endDelimiter );
}

/**
 * Returns the language constructs for a given template language, for use in template creation.
 *
 * NB For ES6 templates, only the start and end delimiters used for a variable are supported, other constructs (if, loop,
 * partial) are omitted here. Empty strings are returned for the instead.
 *
 * @param   {string} templateLanguage
 * @returns {Object}
 */
function getTemplateLanguageConstructs ( templateLanguage ) {
    var constructs;

    switch ( templateLanguage.toLowerCase() ) {

        case "handlebars":
            constructs = {
                startDelimiter: "{{",
                endDelimiter: "}}",
                if: "{{#if isActive}}",
                else: "{{else}}",
                endIf: "{{/if}}",
                loop: "{{#each looped as |value index|}}",
                endLoop: "{{/each}}",
                partial: '{{> userMessage tagName="h2" }}'
            };

            break;
        case "ejs":
            constructs = {
                startDelimiter: "<%= ",
                endDelimiter: " %>",
                if: "<% if (isActive) { %>",
                else: "<% } else { %>",
                endIf: "<% } %>",
                loop: "<% looped.forEach(function(item) { %>",
                endLoop: "<% }); %>",
                partial: "<%- include('user/show', {user: user}); %>"
            };
            break;
        case "es6":
            constructs = {
                startDelimiter: "${",
                endDelimiter: "}",
                if: "",
                else: "",
                endIf: "",
                loop: "",
                endLoop: "",
                partial: ""
            };
            break;
        default:
            throw new Error( 'Unsupported template language "' + templateLanguage + '"' );
    }

    return constructs;
}


/**
* @ requires
*/
var Stream            = require('./stream.js'),
    request           = require('./http.js').request,
    makePointOptions  = require('./utils.js').makePointOptions,
    defaultDataFormat = require('../include/meta.js').defaultDataFormat;

/**
* Create an instance of Point
*
* @constructor {Point}
* @this        {Point}
* @param       {string}     masterApikey Your pachube api key.
*/
function Point(masterApikey, feedID, streamID) {
  /** @private */this.masterApiKey  = masterApikey;
  /** @private */this.feedID   = feedID.toString();
  /** @private */this.streamID = streamID.toString();
}

/**
* @inheritance DataStream
*/
Point.prototype = Stream.prototype;

/**
* Create datapoint: POST /v2/feeds/<feed_id>/datastreams/<datastream_id>/datapoints
*
* @this    {Point}
* @param   {function} callback           The callback function.
* @param   {string}   optionalDataFormat Optional data format parameter, json, xml or csv, default to json.
* @returns {string}                      HTTP headers only
*/
Point.prototype.create = function(callback, optionalDataFormat) {
  var options          = makePointOptions('POST', this.feedID, this.streamID);
  var dataFormat       = optionalDataFormat || defaultDataFormat;
  request(this.masterApiKey, options, callback, dataFormat);
};

/**
* Read datapoint: GET /v2/feeds/<feed_id>/datastreams/<datastream_id>/datapoints/<timestamp>
*
* @this    {Point}
* @param   {string}   timestamp          The timestamp for data point.
* @param   {function} callback           The callback function.
* @param   {string}   optionalDataFormat Optional data format parameter, json, xml for csv, default to json.
* @returns {string}                      datapoint
*/
Point.prototype.read = function(timestamp, callback, optionalDataFormat) {
  var options        = makePointOptions('GET', this.feedID, this.streamID, timestamp);
  var dataFormat     = optionalDataFormat || defaultDataFormat;
  request(this.masterApiKey, options, callback, dataFormat);
};

/**
* @ exports
*/
module.exports = Point;