var sl = require('sugarlisp-core/sl-types'),
    reader = require('sugarlisp-core/reader'),
    plusrfuncs = require('sugarlisp-plus/readfuncs');

// parse an html element (when the parser has just encountered the opening "<")
exports.read_html_element = function(lexer) {

  var openingBracket = lexer.next_token("<");  // skip the angle bracket

  var list = sl.list(sl.atom("tag", {token: openingBracket}));
  var tagNameToken = lexer.next_token(/([a-zA-Z\-\_]+[a-zA-Z0-9\-\_\.\#]*)/g);
  list.push(sl.str(tagNameToken.text, {token: tagNameToken}));

  var tagAttributes = exports.read_html_attributes(lexer);
  if(tagAttributes && tagAttributes.length > 0) {
    list.push(tagAttributes);
  }

  var tagBody;
  if(lexer.on('>')) {
      var expectedEndTag = '</' + tagNameToken.text + '>';
      // in an lsml page, we may encounter:
      //   <script type="text/sugarlisp">
      // in which case we read the content as lispy code not html
      if(tagNameToken.text === 'script' &&
        ((t = find_attr_value(tagAttributes, 'type')) &&
        t.value === 'text/sugarlisp') &&
        !find_attr_value(tagAttributes, 'src')) {
          // yup it's an inline script tag containing lispy code
          // little hacky - but the output is javascript!
          t.value = 'text/javascript';
          lexer.skip_token('>'); // skip token also gets following spaces

          // we allow them to use html in this inline sugarlisp,
          // but we need a new dialect that uses the "dynamic" html
          // generation mode (i.e. creating strings of html at run
          // time)
          var dyndialect = reader.use_dialect_module("html", lexer, {
                rootform: list,
                filelevel: false
          });
// ORIGINAL          dyndialect.html_keyword_mode = "dynamic";
dyndialect.html_keyword_mode = "vdom";  // FOR TESTING NEW VDOM STUFF
          tagBody = sl.list();
          // we're missing a return after the script tag:
          tagBody.prelude = "\n\n";
          lexer.new_dialect(dyndialect, tagBody);

          while (!lexer.eos() && !lexer.on(expectedEndTag)) {
            var nextform = reader.read(lexer);

            // some "directives" don't return an actual form:
            if(!reader.isignorableform(nextform)) {
              tagBody.push(nextform);
            }
          }

          if (lexer.eos()) {
              lexer.error('Missing "' + expectedEndTag + '" + tag?', tagNameToken);
          }

          // the top level delimits subexpressions differently
          tagBody.__toplevel = true;
          lexer.skip_text(expectedEndTag);
      }
      else {
        // the tag's body is treated as if it were an automatically quoted
        // template string, except that other html tags may be there too...

        // not yet sure if the ability to read html tags belongs as an option
        // passed to read_template_string or if it's just a separate function of
        // it's own e.g. read_html_body.  Note that we don't want the body to
        // be an (str...) if the only child is another html tag (despite what
        // the line below suggests)
        var concat = sl.atom('tagjoiner');
        concat.noparens = true;
        tagBody = exports.read_html_body(lexer, '>', expectedEndTag, [concat]);
      }
  } else if(lexer.on('/>')) {
    lexer.skip_text('/>');
    tagBody = [] // empty tag
  }
  else {
    reader.error("Malformed html tag...", lexer);
  }

  if(tagBody && !(Array.isArray(tagBody) && tagBody.length === 0)) {
    list.push(tagBody);
  }
  return list;
}

// note that attributes are read as a lispy (attr name value ...) list,
// yet we set the names as actual object properties so it can also be
// used with simple js dot notation.  Note though the values are atoms
// not simple primitives, so they are wrapped with line/col meta data.
exports.read_html_attributes = function(lexer) {

  // they can use a lispy expression to derive their tag attributes
  // e.g. <div (attr id "mydiv")/>
  // this can be useful in macros, etc.
  if(lexer.on("(")) {
    return reader.read(lexer);
  }

  // "attr" is really just a synonym for "object"
  var list = sl.list("attr");
  list.setOpening(lexer);

  var ch;
  var atLeastOneAttribute = false;
  // the attributes end at either ">" or "/>" ...
  while (!lexer.eos() && (ch = lexer.peek_char()) && (ch !== ">" && ch !== "/")) {
    // read the attribute name and add the lispy form:
    list.push(lexer.next_word_token());
    lexer.skip_whitespace();

    if(lexer.on("=")) {
      lexer.skip_text("=");

      // read the attribute value (which may include ${} placeholders)
      var attrVal = plusrfuncs.read_template_string(lexer, undefined, undefined, ['+']);
      list.push(attrVal);
    }
    else {
      // it's not common but sometimes in html there's no equals e.g. "<option selected/>"
      list.push(sl.atom(true));
    }

    atLeastOneAttribute = true;
  }
  if (ch !== ">" && ch !== "/") {
      reader.error("Missing end to tag attributes", lexer);
  }
  if(!atLeastOneAttribute) {
    list.shift();  // return just () not (attr...)
  }
  list.setClosing(lexer);

  list = plusrfuncs.even_keys_to_list_properties(list);

  // note we have intentionally *not* read the ending ">" or "/>"
  return list;
}

// find the value form with the given name in the attribute forms,
// otherwise undefined
function find_attr_value(attrForms, attrName) {
  var attrVal;
  var attrPos = -1;
  if(attrForms && Array.isArray(attrForms)) {
    attrKey = attrForms.find(function (form, i) {
      attrPos = i;
      return (i % 2 === 1) && form.value === attrName;
    })
    if(attrKey && attrPos+1 < attrForms.length) {
      attrVal = attrForms[attrPos+1];
    }
  }
  return attrVal;
}

// Text in html tags default to being a string, unlike in lispy code
// where they default to being symbols (and strings must be quoted).
// Otherwise lispy expressions must be wrapped in ${}, but html tags
// can be nested normally.
exports.read_html_body = function(lexer, start, end, initial) {

  var openingBracket = lexer.next_token(start);
  var strlist = sl.listFromArray(initial || []);
  strlist.setOpening(openingBracket);

  // note we're no longer tokenizing delimited "words"
  // (an html body is text i.e. like a string just without the quotes)
  var text = "";
  lexer.mark_token_start();

  // scan looking for the ending
  while(!lexer.eos() && !lexer.on(end)) {

    // have we hit the start of an ${} escape?
    if(lexer.on('${')) {
      // add the preceding text
      if(text !== "") {
        text = sl.addQuotes(sl.escapeNewlines(text));
        strlist.push(sl.str(text, {token: lexer.create_token(text)}));
        text = "";
      }
      lexer.skip_text("${");
      strlist.push(reader.read(lexer))
      lexer.skip_filler();
      lexer.assert('}',"Missing end for ${");
      lexer.skip_char("}");
      lexer.mark_token_start();
    }
    // a tag name starts '<' then alphabetical after that...
    else if(lexer.on(/<[a-zA-Z\-\_]/g, true)) {
      // add the preceding text
      if(text !== "") {
        text = sl.addQuotes(sl.escapeNewlines(text));
        strlist.push(sl.str(text, {token: lexer.create_token(text)}));
        text = "";
      }
      // add the child html tag(s):
      strlist.push(exports.read_html_element(lexer));
    }
    else {
      // it was just normal text (accumulate it till some delimiter is found)
      text += lexer.next_char();
    }
  }

  // we should be at the end of the template string now
  lexer.assert(end, "Missing end of html body");
  lexer.skip_text(end);

  // add any text between last ${} or html tag and the end
  if(text !== "") {
    text = sl.addQuotes(sl.escapeNewlines(text));
    strlist.push(sl.str(text, {token: lexer.create_token(text)}));
  }

  if(strlist.length === 1) {
    strlist.shift(); // the body was empty - it's *not* a (str...)
  }
  else if(strlist.length === 2) {
    strlist = strlist[1]; // the body was just a single string or html tag
  }

  return strlist;
}


/* eslint new-cap: [0] */
/* eslint no-console: [0] */
'use strict'

const Model = require('trails/model')
const _ = require('lodash')
const queryDefaults = require('../utils/queryDefaults')
const helpers = require('proxy-engine-helpers')
const Errors = require('proxy-engine-errors')
const shortId = require('shortid')

/**
 * @module User
 * @description User model for basic auth
 */
module.exports = class User extends Model {

  static config(app, Sequelize) {
    return {
      //More information about supported models options here : http://docs.sequelizejs.com/en/latest/docs/models-definition/#configuration
      options: {
        underscored: true,
        hooks: {
          beforeCreate: (values, options) => {
            // If not token was already created, create it
            if (!values.token) {
              values.token = `user_${shortId.generate()}`
            }
          }
        },
        classMethods: {
          //If you need associations, put them here
          associate: (models) => {
            //More information about associations here : http://docs.sequelizejs.com/en/latest/docs/associations/
            models.User.hasMany(models.Passport, {
              as: 'passports',
              onDelete: 'CASCADE',
              foreignKey: {
                allowNull: false
              }
            })
          },
          findByIdDefault: function(criteria, options) {
            if (!options) {
              options = {}
            }
            options = _.defaultsDeep(options, queryDefaults.User.default(app))
            return this.findById(criteria, options)
          },
          findOneDefault: function(options) {
            if (!options) {
              options = {}
            }
            options = _.defaultsDeep(options, queryDefaults.User.default(app))
            return this.findOne(options)
          },
          resolve: function(user, options){
            options = options || {}
            const User = this
            if (user instanceof User){
              return Promise.resolve(user)
            }
            else if (user && _.isObject(user) && user.id) {
              return User.findById(user.id, options)
                .then(resUser => {
                  if (!resUser) {
                    throw new Errors.FoundError(Error(`User ${user.id} not found`))
                  }
                  return resUser
                })
            }
            else if (user && _.isObject(user) && user.email) {
              return User.findOne(_.defaultsDeep({
                where: {
                  email: user.email
                }
              }, options))
                .then(resUser => {
                  if (!resUser) {
                    throw new Errors.FoundError(Error(`User ${user.email} not found`))
                  }
                  return resUser
                })
            }
            else if (user && _.isNumber(user)) {
              return User.findById(user, options)
                .then(resUser => {
                  if (!resUser) {
                    throw new Errors.FoundError(Error(`User ${user} not found`))
                  }
                  return resUser
                })
            }
            else if (user && _.isString(user)) {
              return User.findOne(_.defaultsDeep({
                where: {
                  email: user
                }
              }, options))
                .then(resUser => {
                  if (!resUser) {
                    throw new Errors.FoundError(Error(`User ${user} not found`))
                  }
                  return resUser
                })
            }
            else {
              throw new Errors.FoundError(Error(`User ${user} not found`))
            }
          }
        },
        instanceMethods: {
          /**
           *
           * @param options
           * @returns {string}
           */
          getSalutation: function(options) {
            options = options || {}

            let salutation = 'User'

            if (this.username) {
              salutation = this.username
            }
            else if (this.email) {
              salutation = this.email
            }
            else {
              salutation = this.id
            }
            return salutation
          },
          /**
           *
           * @param val
           * @returns {Promise.<TResult>}
           */
          generateRecovery: function(val) {
            return app.config.proxyPassport.bcrypt.hash(
              val,
              app.config.proxyPassport.bcrypt.genSaltSync(10)
            )
              .then(hash => {
                this.recovery = hash
                return this
              })
          },
          /**
           *
           * @param options
           */
          sendResetEmail: function(options) {
            options = options || {}
            return app.emails.User.recovery(this, {
              send_email: app.config.proxyPassport.emails.userRecovery
            }, {
              transaction: options.transaction || null
            })
              .then(email => {
                return app.services.NotificationService.create(
                  email,
                  [this],
                  {transaction: options.transaction || null}
                )
                  .then(notes => {
                    app.log.debug('NOTIFY', this.id, this.email, this.users.map(u => u.id), email.send_email, notes.users.map(u => u.id))
                    return notes
                  })
              })
              .catch(err => {
                app.log.error(err)
                return
              })
          },
          /**
           * Get's user's passports if not on DAO
           * @param options
           */
          resolvePassports: function(options) {
            options = options || {}
            if (
              this.passports
              && this.passports.every(t => t instanceof app.orm['Passport'])
              && options.reload !== true
            ) {
              return Promise.resolve(this)
            }
            else {
              return this.getPassports({transaction: options.transaction || null})
                .then(passports => {
                  passports = passports || []
                  this.passports = passports
                  this.setDataValue('passports', passports)
                  this.set('passports', passports)
                  return this
                })
            }
          }
        }
      }
    }
  }

  static schema(app, Sequelize) {
    return {
      token: {
        type: Sequelize.STRING,
        unique: true
      },
      // Username
      username: {
        type: Sequelize.STRING,
        unique: true
      },
      // Unique Email address for user
      email: {
        type: Sequelize.STRING,
        unique: true,
        validate: {
          isEmail: true
        }
      },
      // Recovery string in the event of a password reset
      recovery: {
        type: Sequelize.STRING,
        allowNull: true
      },
      // An object of user preferences
      preferences: helpers.JSONB('User', app, Sequelize, 'preferences', {
        defaultValue: {}
      })
    }
  }

}


//@tag foundation,core
//@define Ext

/**
 * @class Ext
 * @singleton
 */
(function() {
    var global = this,
        objectPrototype = Object.prototype,
        toString = objectPrototype.toString,
        enumerables = true,
        enumerablesTest = { toString: 1 },
        emptyFn = function(){},
        i;

    if (typeof Ext === 'undefined') {
        global.Ext = {};
    }

    Ext.global = global;

    for (i in enumerablesTest) {
        enumerables = null;
    }

    if (enumerables) {
        enumerables = ['hasOwnProperty', 'valueOf', 'isPrototypeOf', 'propertyIsEnumerable',
                       'toLocaleString', 'toString', 'constructor'];
    }

    /**
     * An array containing extra enumerables for old browsers.
     * @property {String[]}
     */
    Ext.enumerables = enumerables;

    /**
     * Copies all the properties of config to the specified object.
     * Note that if recursive merging and cloning without referencing the original objects / arrays is needed, use
     * {@link Ext.Object#merge} instead.
     * @param {Object} object The receiver of the properties.
     * @param {Object} config The source of the properties.
     * @param {Object} [defaults] A different object that will also be applied for default values.
     * @return {Object} returns obj
     */
    Ext.apply = function(object, config, defaults) {
        if (defaults) {
            Ext.apply(object, defaults);
        }

        if (object && config && typeof config === 'object') {
            var i, j, k;

            for (i in config) {
                object[i] = config[i];
            }

            if (enumerables) {
                for (j = enumerables.length; j--;) {
                    k = enumerables[j];
                    if (config.hasOwnProperty(k)) {
                        object[k] = config[k];
                    }
                }
            }
        }

        return object;
    };

    Ext.buildSettings = Ext.apply({
        baseCSSPrefix: 'x-',
        scopeResetCSS: false
    }, Ext.buildSettings || {});

    Ext.apply(Ext, {
        /**
         * @property {Function}
         * A reusable empty function
         */
        emptyFn: emptyFn,

        baseCSSPrefix: Ext.buildSettings.baseCSSPrefix,

        /**
         * Copies all the properties of config to object if they don't already exist.
         * @param {Object} object The receiver of the properties.
         * @param {Object} config The source of the properties.
         * @return {Object} returns obj
         */
        applyIf: function(object, config) {
            var property;

            if (object) {
                for (property in config) {
                    if (object[property] === undefined) {
                        object[property] = config[property];
                    }
                }
            }

            return object;
        },

        /**
         * Iterates either an array or an object. This method delegates to
         * {@link Ext.Array#each Ext.Array.each} if the given value is iterable, and {@link Ext.Object#each Ext.Object.each} otherwise.
         *
         * @param {Object/Array} object The object or array to be iterated.
         * @param {Function} fn The function to be called for each iteration. See and {@link Ext.Array#each Ext.Array.each} and
         * {@link Ext.Object#each Ext.Object.each} for detailed lists of arguments passed to this function depending on the given object
         * type that is being iterated.
         * @param {Object} scope (Optional) The scope (`this` reference) in which the specified function is executed.
         * Defaults to the object being iterated itself.
         */
        iterate: function(object, fn, scope) {
            if (Ext.isEmpty(object)) {
                return;
            }

            if (scope === undefined) {
                scope = object;
            }

            if (Ext.isIterable(object)) {
                Ext.Array.each.call(Ext.Array, object, fn, scope);
            }
            else {
                Ext.Object.each.call(Ext.Object, object, fn, scope);
            }
        }
    });

    Ext.apply(Ext, {

        /**
         * This method deprecated. Use {@link Ext#define Ext.define} instead.
         * @method
         * @param {Function} superclass
         * @param {Object} overrides
         * @return {Function} The subclass constructor from the `overrides` parameter, or a generated one if not provided.
         * @deprecated 2.0.0 Please use {@link Ext#define Ext.define} instead
         */
        extend: function() {
            // inline overrides
            var objectConstructor = objectPrototype.constructor,
                inlineOverrides = function(o) {
                for (var m in o) {
                    if (!o.hasOwnProperty(m)) {
                        continue;
                    }
                    this[m] = o[m];
                }
            };

            return function(subclass, superclass, overrides) {
                // First we check if the user passed in just the superClass with overrides
                if (Ext.isObject(superclass)) {
                    overrides = superclass;
                    superclass = subclass;
                    subclass = overrides.constructor !== objectConstructor ? overrides.constructor : function() {
                        superclass.apply(this, arguments);
                    };
                }

                //<debug>
                if (!superclass) {
                    Ext.Error.raise({
                        sourceClass: 'Ext',
                        sourceMethod: 'extend',
                        msg: 'Attempting to extend from a class which has not been loaded on the page.'
                    });
                }
                //</debug>

                // We create a new temporary class
                var F = function() {},
                    subclassProto, superclassProto = superclass.prototype;

                F.prototype = superclassProto;
                subclassProto = subclass.prototype = new F();
                subclassProto.constructor = subclass;
                subclass.superclass = superclassProto;

                if (superclassProto.constructor === objectConstructor) {
                    superclassProto.constructor = superclass;
                }

                subclass.override = function(overrides) {
                    Ext.override(subclass, overrides);
                };

                subclassProto.override = inlineOverrides;
                subclassProto.proto = subclassProto;

                subclass.override(overrides);
                subclass.extend = function(o) {
                    return Ext.extend(subclass, o);
                };

                return subclass;
            };
        }(),

        /**
         * Proxy to {@link Ext.Base#override}. Please refer {@link Ext.Base#override} for further details.
         *
         * @param {Object} cls The class to override
         * @param {Object} overrides The properties to add to `origClass`. This should be specified as an object literal
         * containing one or more properties.
         * @method override
         * @deprecated 2.0.0 Please use {@link Ext#define Ext.define} instead.
         */
        override: function(cls, overrides) {
            if (cls.$isClass) {
                return cls.override(overrides);
            }
            else {
                Ext.apply(cls.prototype, overrides);
            }
        }
    });

    // A full set of static methods to do type checking
    Ext.apply(Ext, {

        /**
         * Returns the given value itself if it's not empty, as described in {@link Ext#isEmpty}; returns the default
         * value (second argument) otherwise.
         *
         * @param {Object} value The value to test.
         * @param {Object} defaultValue The value to return if the original value is empty.
         * @param {Boolean} [allowBlank=false] (optional) `true` to allow zero length strings to qualify as non-empty.
         * @return {Object} `value`, if non-empty, else `defaultValue`.
         */
        valueFrom: function(value, defaultValue, allowBlank){
            return Ext.isEmpty(value, allowBlank) ? defaultValue : value;
        },

        /**
         * Returns the type of the given variable in string format. List of possible values are:
         *
         * - `undefined`: If the given value is `undefined`
         * - `null`: If the given value is `null`
         * - `string`: If the given value is a string
         * - `number`: If the given value is a number
         * - `boolean`: If the given value is a boolean value
         * - `date`: If the given value is a `Date` object
         * - `function`: If the given value is a function reference
         * - `object`: If the given value is an object
         * - `array`: If the given value is an array
         * - `regexp`: If the given value is a regular expression
         * - `element`: If the given value is a DOM Element
         * - `textnode`: If the given value is a DOM text node and contains something other than whitespace
         * - `whitespace`: If the given value is a DOM text node and contains only whitespace
         *
         * @param {Object} value
         * @return {String}
         */
        typeOf: function(value) {
            if (value === null) {
                return 'null';
            }

            var type = typeof value;

            if (type === 'undefined' || type === 'string' || type === 'number' || type === 'boolean') {
                return type;
            }

            var typeToString = toString.call(value);

            switch(typeToString) {
                case '[object Array]':
                    return 'array';
                case '[object Date]':
                    return 'date';
                case '[object Boolean]':
                    return 'boolean';
                case '[object Number]':
                    return 'number';
                case '[object RegExp]':
                    return 'regexp';
            }

            if (type === 'function') {
                return 'function';
            }

            if (type === 'object') {
                if (value.nodeType !== undefined) {
                    if (value.nodeType === 3) {
                        return (/\S/).test(value.nodeValue) ? 'textnode' : 'whitespace';
                    }
                    else {
                        return 'element';
                    }
                }

                return 'object';
            }

            //<debug error>
            Ext.Error.raise({
                sourceClass: 'Ext',
                sourceMethod: 'typeOf',
                msg: 'Failed to determine the type of the specified value "' + value + '". This is most likely a bug.'
            });
            //</debug>
        },

        /**
         * Returns `true` if the passed value is empty, `false` otherwise. The value is deemed to be empty if it is either:
         *
         * - `null`
         * - `undefined`
         * - a zero-length array.
         * - a zero-length string (Unless the `allowEmptyString` parameter is set to `true`).
         *
         * @param {Object} value The value to test.
         * @param {Boolean} [allowEmptyString=false] (optional) `true` to allow empty strings.
         * @return {Boolean}
         */
        isEmpty: function(value, allowEmptyString) {
            return (value === null) || (value === undefined) || (!allowEmptyString ? value === '' : false) || (Ext.isArray(value) && value.length === 0);
        },

        /**
         * Returns `true` if the passed value is a JavaScript Array, `false` otherwise.
         *
         * @param {Object} target The target to test.
         * @return {Boolean}
         * @method
         */
        isArray: ('isArray' in Array) ? Array.isArray : function(value) {
            return toString.call(value) === '[object Array]';
        },

        /**
         * Returns `true` if the passed value is a JavaScript Date object, `false` otherwise.
         * @param {Object} object The object to test.
         * @return {Boolean}
         */
        isDate: function(value) {
            return toString.call(value) === '[object Date]';
        },

        /**
         * Returns 'true' if the passed value is a String that matches the MS Date JSON encoding format
         * @param {String} value The string to test
         * @return {Boolean}
         */
        isMSDate: function(value) {
            if (!Ext.isString(value)) {
                return false;
            } else {
                return value.match("\\\\?/Date\\(([-+])?(\\d+)(?:[+-]\\d{4})?\\)\\\\?/") !== null;
            }
        },

        /**
         * Returns `true` if the passed value is a JavaScript Object, `false` otherwise.
         * @param {Object} value The value to test.
         * @return {Boolean}
         * @method
         */
        isObject: (toString.call(null) === '[object Object]') ?
        function(value) {
            // check ownerDocument here as well to exclude DOM nodes
            return value !== null && value !== undefined && toString.call(value) === '[object Object]' && value.ownerDocument === undefined;
        } :
        function(value) {
            return toString.call(value) === '[object Object]';
        },

        /**
         * @private
         */
        isSimpleObject: function(value) {
            return value instanceof Object && value.constructor === Object;
        },
        /**
         * Returns `true` if the passed value is a JavaScript 'primitive', a string, number or Boolean.
         * @param {Object} value The value to test.
         * @return {Boolean}
         */
        isPrimitive: function(value) {
            var type = typeof value;

            return type === 'string' || type === 'number' || type === 'boolean';
        },

        /**
         * Returns `true` if the passed value is a JavaScript Function, `false` otherwise.
         * @param {Object} value The value to test.
         * @return {Boolean}
         * @method
         */
        isFunction:
        // Safari 3.x and 4.x returns 'function' for typeof <NodeList>, hence we need to fall back to using
        // Object.prorotype.toString (slower)
        (typeof document !== 'undefined' && typeof document.getElementsByTagName('body') === 'function') ? function(value) {
            return toString.call(value) === '[object Function]';
        } : function(value) {
            return typeof value === 'function';
        },

        /**
         * Returns `true` if the passed value is a number. Returns `false` for non-finite numbers.
         * @param {Object} value The value to test.
         * @return {Boolean}
         */
        isNumber: function(value) {
            return typeof value === 'number' && isFinite(value);
        },

        /**
         * Validates that a value is numeric.
         * @param {Object} value Examples: 1, '1', '2.34'
         * @return {Boolean} `true` if numeric, `false` otherwise.
         */
        isNumeric: function(value) {
            return !isNaN(parseFloat(value)) && isFinite(value);
        },

        /**
         * Returns `true` if the passed value is a string.
         * @param {Object} value The value to test.
         * @return {Boolean}
         */
        isString: function(value) {
            return typeof value === 'string';
        },

        /**
         * Returns `true` if the passed value is a Boolean.
         *
         * @param {Object} value The value to test.
         * @return {Boolean}
         */
        isBoolean: function(value) {
            return typeof value === 'boolean';
        },

        /**
         * Returns `true` if the passed value is an HTMLElement.
         * @param {Object} value The value to test.
         * @return {Boolean}
         */
        isElement: function(value) {
            return value ? value.nodeType === 1 : false;
        },

        /**
         * Returns `true` if the passed value is a TextNode.
         * @param {Object} value The value to test.
         * @return {Boolean}
         */
        isTextNode: function(value) {
            return value ? value.nodeName === "#text" : false;
        },

        /**
         * Returns `true` if the passed value is defined.
         * @param {Object} value The value to test.
         * @return {Boolean}
         */
        isDefined: function(value) {
            return typeof value !== 'undefined';
        },

        /**
         * Returns `true` if the passed value is iterable, `false` otherwise.
         * @param {Object} value The value to test.
         * @return {Boolean}
         */
        isIterable: function(value) {
            return (value && typeof value !== 'string') ? value.length !== undefined : false;
        }
    });

    Ext.apply(Ext, {

        /**
         * Clone almost any type of variable including array, object, DOM nodes and Date without keeping the old reference.
         * @param {Object} item The variable to clone.
         * @return {Object} clone
         */
        clone: function(item) {
            if (item === null || item === undefined) {
                return item;
            }

            // DOM nodes
            if (item.nodeType && item.cloneNode) {
                return item.cloneNode(true);
            }

            // Strings
            var type = toString.call(item);

            // Dates
            if (type === '[object Date]') {
                return new Date(item.getTime());
            }

            var i, j, k, clone, key;

            // Arrays
            if (type === '[object Array]') {
                i = item.length;

                clone = [];

                while (i--) {
                    clone[i] = Ext.clone(item[i]);
                }
            }
            // Objects
            else if (type === '[object Object]' && item.constructor === Object) {
                clone = {};

                for (key in item) {
                    clone[key] = Ext.clone(item[key]);
                }

                if (enumerables) {
                    for (j = enumerables.length; j--;) {
                        k = enumerables[j];
                        clone[k] = item[k];
                    }
                }
            }

            return clone || item;
        },

        /**
         * @private
         * Generate a unique reference of Ext in the global scope, useful for sandboxing.
         */
        getUniqueGlobalNamespace: function() {
            var uniqueGlobalNamespace = this.uniqueGlobalNamespace;

            if (uniqueGlobalNamespace === undefined) {
                var i = 0;

                do {
                    uniqueGlobalNamespace = 'ExtBox' + (++i);
                } while (Ext.global[uniqueGlobalNamespace] !== undefined);

                Ext.global[uniqueGlobalNamespace] = Ext;
                this.uniqueGlobalNamespace = uniqueGlobalNamespace;
            }

            return uniqueGlobalNamespace;
        },

        /**
         * @private
         */
        functionFactory: function() {
            var args = Array.prototype.slice.call(arguments),
                ln = args.length;

            if (ln > 0) {
                args[ln - 1] = 'var Ext=window.' + this.getUniqueGlobalNamespace() + ';' + args[ln - 1];
            }

            return Function.prototype.constructor.apply(Function.prototype, args);
        },

        /**
         * @private
         */
        globalEval: ('execScript' in global) ? function(code) {
            global.execScript(code)
        } : function(code) {
            (function(){
                eval(code);
            })();
        }

        //<feature logger>
        /**
         * @private
         * @property
         */
        ,Logger: {
            log: function(message, priority) {
                if ('console' in global) {
                    if (!priority || !(priority in global.console)) {
                        priority = 'log';
                    }
                    message = '[' + priority.toUpperCase() + '] ' + message;
                    global.console[priority](message);
                }
            },
            verbose: function(message) {
                this.log(message, 'verbose');
            },
            info: function(message) {
                this.log(message, 'info');
            },
            warn: function(message) {
                this.log(message, 'warn');
            },
            error: function(message) {
                throw new Error(message);
            },
            deprecate: function(message) {
                this.log(message, 'warn');
            }
        }
        //</feature>
    });

    /**
     * Old alias to {@link Ext#typeOf}.
     * @deprecated 2.0.0 Please use {@link Ext#typeOf} instead.
     * @method
     * @alias Ext#typeOf
     */
    Ext.type = Ext.typeOf;

})();


Ext.data.DatabaseDefinition = Ext.extend(Ext.data.Config, {

	key: undefined, // the developer's api key
	database_name: undefined,
	generation: undefined, // of the database
	system_name: undefined, // this system
	system_names: {}, // other systems
	replica_number: undefined,
	idProperty: undefined,
	idPropertyDefaultValue: undefined,
	version: 1, // of the storage scheme

	// JCM include the epoch of the clock here?

	constructor: function(config,callback,scope) {
		//
		Ext.data.utilities.check('DatabaseDefinition', 'constructor', 'config', config, ['key','database_name','generation','system_name','replica_number']);
		//
		this.set(config);
		config.config_id= 'definition';
		Ext.data.DatabaseDefinition.superclass.constructor.call(this, config);
	},

	setReplicaNumber: function(replica_number,callback,scope) {
		var changed= (this.replica_number!=replica_number);
		this.replica_number= replica_number;
		this.writeAndCallback(changed,callback,scope);
	},

	addSystemName: function(system_name) {
		this.system_names[system_name]= true;
		// JCM this.writeAndCallback(changed,callback,scope);
	},

	isKnownOf: function(system_name) {
		return this.system_name===system_name || Ext.data.array.includes(this.system_names,system_name);
	},

	set: function(config,callback,scope) {
		var changed= Ext.data.utilities.copy(config,this,[
			'key',
			'database_name',
			'generation',
			'system_name',
			'system_names',
			'replica_number',
			'idProperty',
			'idPropertyDefaultValue',
			'version',
			'_id']);
		this.writeAndCallback(changed,callback,scope);
	},

	as_data: function() { // to store on the disk
		var data= {
			key: this.key,
			database_name: this.database_name,
			generation: this.generation,
			system_name: this.system_name,
			system_names: this.system_names,
			replica_number: this.replica_number,
			idProperty: this.idProperty,
			idPropertyDefaultValue: this.idPropertyDefaultValue,
			version: this.version
		};
		data[Ext.data.SyncModel.MODEL]= 'Ext.data.DatabaseDefinition';
		return Ext.data.DatabaseDefinition.superclass.as_data.call(this, data);
	},

	encode: function() { // to send over the wire
		return {
			key: this.key,
			database_name: this.database_name,
			generation: this.generation,
			system_name: this.system_name,
			replica_number: this.replica_number,
			idProperty: this.idProperty,
			idPropertyDefaultValue: this.idPropertyDefaultValue
		};
	}

	// JCM perhaps an explicit decode would be better than the constructor?

});


/**
 * @private
 * @extends Object
 * DOM event. This class really extends {@link Ext.event.Event}, but for documentation
 * purposes it's members are listed inside {@link Ext.event.Event}.
 */
Ext.define('Ext.event.Dom', {
    extend: 'Ext.event.Event',

    constructor: function(event) {
        var target = event.target,
            touches;

        if (target && target.nodeType !== 1) {
            target = target.parentNode;
        }
        touches = event.changedTouches;
        if (touches) {
            touches = touches[0];
            this.pageX = touches.pageX;
            this.pageY = touches.pageY;
        }
        else {
            this.pageX = event.pageX;
            this.pageY = event.pageY;
        }

        this.browserEvent = this.event = event;
        this.target = this.delegatedTarget = target;
        this.type = event.type;

        this.timeStamp = this.time = +event.timeStamp;

        return this;
    },

    /**
     * @property {Number} distance
     * The distance of the event.
     *
     * **This is only available when the event type is `swipe` and `pinch`.**
     */

    /**
     * @property {HTMLElement} target
     * The target HTMLElement for this event. For example; if you are listening to a tap event and you tap on a `<div>` element,
     * this will return that `<div>` element.
     */

    /**
     * @property {Number} pageX The browsers x coordinate of the event.
     */

    /**
     * @property {Number} pageY The browsers y coordinate of the event.
     */

    stopEvent: function() {
        this.preventDefault();

        return this.callParent();
    },

    /**
     * Prevents the browsers default handling of the event.
     */
    preventDefault: function() {
        this.browserEvent.preventDefault();
    },

    /**
     * Gets the x coordinate of the event.
     * @deprecated 2.0 Please use {@link #pageX} property directly.
     * @return {Number}
     */
    getPageX: function() {
        return this.pageX || this.browserEvent.pageX;
    },

    /**
     * Gets the y coordinate of the event.
     * @deprecated 2.0 Please use {@link #pageX} property directly.
     * @return {Number}
     */
    getPageY: function() {
        return this.pageY || this.browserEvent.pageY;
    },

    /**
     * Gets the X and Y coordinates of the event.
     * @deprecated 2.0 Please use the {@link #pageX} and {@link #pageY} properties directly.
     * @return {Array}
     */
    getXY: function() {
        if (!this.xy) {
            this.xy = [this.getPageX(), this.getPageY()];
        }

        return this.xy;
    },

    /**
     * Gets the target for the event. Unlike {@link #target}, this returns the main element for your event. So if you are
     * listening to a tap event on Ext.Viewport.element, and you tap on an inner element of Ext.Viewport.element, this will
     * return Ext.Viewport.element.
     *
     * If you want the element you tapped on, then use {@link #target}.
     *
     * @param {String} selector (optional) A simple selector to filter the target or look for an ancestor of the target
     * @param {Number/Mixed} [maxDepth=10||document.body] (optional) The max depth to
     * search as a number or element (defaults to 10 || document.body)
     * @param {Boolean} returnEl (optional) `true` to return a Ext.Element object instead of DOM node.
     * @return {HTMLElement}
     */
    getTarget: function(selector, maxDepth, returnEl) {
        if (arguments.length === 0) {
            return this.delegatedTarget;
        }

        return selector ? Ext.fly(this.target).findParent(selector, maxDepth, returnEl) : (returnEl ? Ext.get(this.target) : this.target);
    },

    /**
     * Returns the time of the event.
     * @return {Date}
     */
    getTime: function() {
        return this.time;
    },

    setDelegatedTarget: function(target) {
        this.delegatedTarget = target;
    },

    makeUnpreventable: function() {
        this.browserEvent.preventDefault = Ext.emptyFn;
    }
});


/**
 * @author Ed Spencer
 * @private
 *
 * Manages the stack of {@link Ext.app.Action} instances that have been decoded, pushes new urls into the browser's
 * location object and listens for changes in url, firing the {@link #change} event when a change is detected.
 *
 * This is tied to an {@link Ext.app.Application Application} instance. The Application performs all of the
 * interactions with the History object, no additional integration should be required.
 */
Ext.define('Ext.app.History', {
    mixins: ['Ext.mixin.Observable'],

    /**
     * @event change
     * Fires when a change in browser url is detected
     * @param {String} url The new url, after the hash (e.g. http://myapp.com/#someUrl returns 'someUrl')
     */

    config: {
        /**
         * @cfg {Array} actions The stack of {@link Ext.app.Action action} instances that have occurred so far
         */
        actions: [],

        /**
         * @cfg {Boolean} updateUrl `true` to automatically update the browser's url when {@link #add} is called.
         */
        updateUrl: true,

        /**
         * @cfg {String} token The current token as read from the browser's location object.
         */
        token: ''
    },

    constructor: function(config) {
        if (Ext.feature.has.History) {
            window.addEventListener('hashchange', Ext.bind(this.detectStateChange, this));
        }
        else {
            setInterval(Ext.bind(this.detectStateChange, this), 100);
        }

        this.initConfig(config);
        if (config && Ext.isEmpty(config.token)) {
            this.setToken(window.location.hash.substr(1));
        }
    },

    /**
     * Adds an {@link Ext.app.Action Action} to the stack, optionally updating the browser's url and firing the
     * {@link #change} event.
     * @param {Ext.app.Action} action The Action to add to the stack.
     * @param {Boolean} silent Cancels the firing of the {@link #change} event if `true`.
     */
    add: function(action, silent) {
        action = Ext.factory(action, Ext.app.Action);

        this.getActions().push(action);

        var url = action.getUrl();

        if (this.getUpdateUrl()) {
            // history.pushState({}, action.getTitle(), "#" + action.getUrl());
            this.setToken(url);
            window.location.hash = url;
        }

        if (silent !== true) {
            this.fireEvent('change', url);
        }

        this.setToken(url);
    },

    /**
     * Navigate to the previous active action. This changes the page url.
     */
    back: function() {
        var actions = this.getActions(),
            previousAction = actions[actions.length - 2];

        if (previousAction) {
            actions.pop();

            previousAction.getController().getApplication().redirectTo(previousAction.getUrl());
        }
        else {
            actions[actions.length - 1].getController().getApplication().redirectTo('');
        }
    },

    /**
     * @private
     */
    applyToken: function(token) {
        return token[0] == '#' ? token.substr(1) : token;
    },

    /**
     * @private
     */
    detectStateChange: function() {
        var newToken = this.applyToken(window.location.hash),
            oldToken = this.getToken();

        if (newToken != oldToken) {
            this.onStateChange();
            this.setToken(newToken);
        }
    },

    /**
     * @private
     */
    onStateChange: function() {
        this.fireEvent('change', window.location.hash.substr(1));
    }
});


// TODO - remove "callback && callback.call(...)", maybe use "noop"
// TODO - 'release' is just the JSON object we get from GitHub. Create a class when/if needed

var https = require('https'),
    fs = require('fs'),
    path = require('path'),
    url_parser = require('url'),

    // Constants
    RELEASES_URL = 'https://api.github.com/repos/{owner}/{repo}/releases',
    RELEASE_URL = 'https://api.github.com/repos/{owner}/{repo}/releases/{id}';

/**
 * Basic method that copies all the keys and values from the specified object
 * into the current object.
 *
 * @param  {Object} obj The source object from which we want to borrow properties
 * @return {null} No return value
 */

function borrow(obj) {
    for (var key in obj) {
        this[key] = obj[key];
    }
}

/**
 * Format a string by replacing all keys between { and } with values from the given dictionary
 * @param  {String} string Format string
 * @param  {Object} dict   Object containing values for the keys specified in the format string
 * @return {String}        Formatted string
 */

function strformat(string, dict) {
    var formatted = string;
    for (var prop in dict) {
        var regexp = new RegExp('\\{' + prop + '\\}', 'gi');
        formatted = formatted.replace(regexp, dict[prop]);
    }
    return formatted;
}

/**
 * Creates a new instance of the GrelRequest class.
 *
 * This class is used when sending HTTP requests to GitHub and it automatically handles
 * user authentication and a few other things.
 *
 * @param {Grel} grel Grel object that is used for authentication
 */

function GrelRequest(grel) {
    var authString;

    if (grel.token) {
        authString = grel.token + ':';
    } else {
        authString = grel.user + ':' + grel.password;
    }

    this.headers = {
        'Authorization': 'Basic ' + new Buffer(authString).toString('base64'),
        'Accept': 'application/vnd.github.manifold-preview',
        'User-Agent': 'Grel'
    };

    this.grel = grel;
    this.content = null;
}

/**
 * Sets a header on the current request object
 * @param  {String} name  Name of the HTTP header
 * @param  {String} value Value for the HTTP header
 * @return {null} No return value
 */
GrelRequest.prototype.header = function(name, value) {
    this.headers[name] = value;
};

/**
 * Sets the content body for the HTTP requests.
 *
 * @param  {String|Buffer} content Content body
 * @return {null} No return value
 */
GrelRequest.prototype.data = function(content) {
    this.content = content;
};

/**
 * Fire an HTTP request to GitHub.
 * @param  {String}   method   HTTP verb
 * @param  {String}   url      URL for the request
 * @param  {Function} callback Callback function
 * @return {null} No return value
 */
GrelRequest.prototype.send = function(method, url, callback) {
    var self = this,
        location = url_parser.parse(url),
        args = {
            // rejectUnauthorized: false,
            method: method,
            host: location.host,
            path: location.path,
            port: 443,
            headers: this.headers
        },
        req;

    req = https.request(args, function(res) {
        var body = "";
        res.on('data', function(data) {
            body += data;
        });
        res.on('end', function() {
            handleResponse.call(self, res, body, callback);
        })
        res.on('error', function(e) {
            callback && callback.call(self, e);
        });
    });

    // Write the content body data, if any
    this.content && req.write(this.content);

    req.end();
};

/**
 * Private method that handles HTTP responses we get from GitHub.
 * This method will always be executed in the context of a GrelRequest.
 *
 * @param  {String}   response HTTP response
 * @param  {Function} callback Callback function
 * @return {null} No return value
 */

function handleResponse(res, data, callback) {
    // HTTP 204 doesn't have a response
    var json = data && JSON.parse(data) || {};

    if ((res.statusCode >= 200) && (res.statusCode <= 206)) {
        // Handle a few known responses
        switch (json.message) {
            case 'Bad credentials':
                callback.call(this, json);
                break;

            default:
                callback.call(this, null, json);
        }
    } else {
        callback.call(this, json);
    }
}

/**
 * Creates a new instance of the Grel class.
 * The config object should contain the following keys: [user, pass, owner, repo]
 *
 * @param {Object} config GitHub credentials and repo information.
 */

function Grel(config) {
    // copy all of the config keys and values in our new object
    borrow.call(this, config);
}

/**
 * Creates a new release.
 *
 * @constructor
 * @param  {String}   name     Release name
 * @param  {String}   message  Release description
 * @param  {Array}    files    List of files to be attached to the release
 * @param  {Function} callback Callback function called whenever an error occurs
 *                             or when release is created and all attachments are uploaded.
 * @return {null} No return value
 */
Grel.prototype.create = function(name, message, files, callback) {
    var self = this,
        data = JSON.stringify({
            "tag_name": name,
            "name": name,
            "body": message,
            "draft": false,
            "prerelease": false
        }),
        req = new GrelRequest(this),
        url = strformat(RELEASES_URL, {
            "owner": this.owner,
            "repo": this.repo
        }),
        release;

    req.data(data);
    req.send('POST', url, function(error, release) {
        if (error) {
            callback && callback(error);
            return;
        }

        self.attach(release, files, callback);
    });
};

/**
 * Attaches files to the given release.
 * @param  {Object}   release  Release object
 * @param  {Array}    files    Array of files to be attached
 * @param  {Function} callback Callback function
 * @return {null} No return value
 */
Grel.prototype.attach = function(release, files, callback) {
    var self = this,
        bytes, filename,
        i, l = files.length,
        content_types = {
            'zip': 'application/zip',
            'default': 'text/html'
        },
        // very rudimentary implementation of what we usually handle with promises
        in_progress = files.length,
        uploadComplete = function(error, response) {
            if (error) {
                callback && callback.call(self, error);
                return;
            }

            // if all the files were attached, execute the callback
            if (--in_progress === 0) {
                callback && callback.call(self, null, release);
            }
        };

    // if there are no files to attach, execute the callback
    if (!files.length) {
        callback && callback.call(self, null, release);
        return;
    }

    files.forEach(function(filename) {
        fs.readFile(filename, function(err, file) {
            if (err) {
                callback && callback.call(self, err);
                return;
            }

            var req = new GrelRequest(self),
                url = strformat(release.upload_url, {
                    "\\?name": "?name=" + path.basename(filename)
                }),
                ext = path.extname(filename).replace(/^\.+/, '');

            req.header('Content-Type', content_types[ext] || content_types['default']);
            req.header('Content-Length', file.length);
            req.data(file);
            req.send('POST', url, uploadComplete);
        });
    });
};

/**
 * Find a release by name
 * @param  {String}   name     Release name
 * @param  {Function} callback Callback function
 * @return {null} No return value
 */
Grel.prototype.find = function(name, callback) {
    var self = this,
        req = new GrelRequest(this),
        url = strformat(RELEASES_URL, {
            "owner": this.owner,
            "repo": this.repo
        }),
        i, l, release;

    req.send('GET', url, function(error, json) {
        if (error) {
            callback && callback.call(self, error);
            return;
        }

        for (i = 0, l = json.length; i < l; ++i) {
            release = json[i];
            if (release.tag_name === name) {
                callback && callback.call(self, null, release);
                return;
            }
        };

        // if we got here, we didn't find the release we're after
        callback && callback.call(self, {
            'message': 'Not found'
        });
    });
};

/**
 * Removes the specified release.
 * @param  {Grel}   release  Release to be removed
 * @param  {Function} callback Callback function
 * @return {null} No return value
 */
Grel.prototype.remove = function(release, callback) {
    var self = this,
        req = new GrelRequest(this),
        url = strformat(RELEASE_URL, {
            "owner": this.owner,
            "repo": this.repo,
            "id": release.id
        });

    req.send('DELETE', url, function(error, json) {
        if (error) {
            callback && callback.call(self, error);
            return;
        }

        callback && callback.call(self, null, json);
    });
};

/**
 * Export the Grel class
 */
module.exports = Grel;


//
//  Original implementation from https://github.com/facebook/Docusaurus
//
//  MIT License
//  Copyright (c) 2017-present, Facebook, Inc.

// Extract markdown metadata header
export default function (content) {
    const metadata = {};
    const both = splitHeader(content);
    if (Object.keys(both).length === 0) {
        return {
            metadata, content
        };
    }
    const lines = both.header.split('\n');
    for (let i = 0; i < lines.length - 1; ++i) {
        const keyvalue = lines[i].split(':');
        const key = keyvalue[0].trim();
        let value = keyvalue.slice(1)
            .join(':')
            .trim();
        try {
            value = JSON.parse(value);
        } catch (e) {
            // do nothing
        }
        metadata[key] = value;
    }
    return {
        metadata, content: both.content
    };
}

// split markdown header
function splitHeader(content) {
    // New line characters need to handle all operating systems.
    const lines = content.split(/\r?\n/);
    if (lines[0] !== '---') {
        return {};
    }
    let i = 1;
    for (; i < lines.length - 1; ++i) {
        if (lines[i] === '---') {
            break;
        }
    }
    return {
        header: lines.slice(1, i + 1).join('\n'),
        content: lines.slice(i + 1).join('\n'),
    };
}


/**
 * @class Ext.scroll.Scroller
 * @author Jacky Nguyen <jacky@sencha.com>
 *
 * Momentum scrolling is one of the most important part of the framework's UI layer. In Sencha Touch there are
 * several scroller implementations so we can have the best performance on all mobile devices and browsers.
 *
 * Scroller settings can be changed using the {@link Ext.Container#scrollable scrollable} configuration in
 * {@link Ext.Container}. Anything you pass to that method will be passed to the scroller when it is
 * instantiated in your container.
 *
 * Please note that the {@link Ext.Container#getScrollable} method returns an instance of {@link Ext.scroll.View}.
 * So if you need to get access to the scroller after your container has been instantiated, you must use the
 * {@link Ext.scroll.View#getScroller} method.
 *
 *     // lets assume container is a container you have
 *     // created which is scrollable
 *     container.getScrollable().getScroller().setFps(10);
 *
 * ## Example
 *
 * Here is a simple example of how to adjust the scroller settings when using a {@link Ext.Container} (or anything
 * that extends it).
 *
 *      @example
 *      Ext.create('Ext.Container', {
 *           fullscreen: true,
 *           html: "Macaroni cheese roquefort
" +
 *                "port-salut. The big cheese
" +
 *                "fondue camembert de normandie
" +
 *                "cow boursin cheese swiss stinking
" +
 *                "bishop. Fromage feta edam fromage
" +
 *                "frais bavarian bergkase paneer
" +
 *                "paneer cheese and wine. Cow danish
" +
 *                "fontina roquefort bocconcini
" +
 *                "jarlsberg parmesan cheesecake
" +
 *                "danish fontina. Mascarpone
" +
 *                "bishop. Fromage feta edam fromage
" +
 *                "frais bavarian bergkase paneer
" +
 *                "paneer cheese and wine. Cow danish
" +
 *                "fontina roquefort bocconcini
" +
 *                "jarlsberg parmesan cheesecake
" +
 *                "emmental fromage frais cheesy
" +
 *                "grin say cheese squirty cheese
" +
 *                "parmesan queso. Cheese triangles
" +
 *                "st. agur blue cheese chalk and cheese
" +
 *                "cream cheese lancashire manchego
" +
 *                "taleggio blue castello. Port-salut
" +
 *                "paneer monterey jack
" +
 *                "say cheese fondue.",
 *           scrollable: {
 *              direction: 'vertical'
 *           }
 *       });
 *
 * As you can see, we are passing the {@link #direction} configuration into the scroller instance in our container.
 *
 * You can pass any of the configs below in that {@link Ext.Container#scrollable scrollable} configuration and it will
 * just work.
 *
 * Go ahead and try it in the live code editor above!
 */
Ext.define('Ext.scroll.Scroller', {

    extend: 'Ext.Evented',

    requires: [
        'Ext.fx.easing.BoundMomentum',
        'Ext.fx.easing.EaseOut',
        'Ext.util.Translatable'
    ],

    /**
     * @event maxpositionchange
     * Fires whenever the maximum position has changed.
     * @param {Ext.scroll.Scroller} this
     * @param {Number} maxPosition The new maximum position.
     */

    /**
     * @event refresh
     * Fires whenever the Scroller is refreshed.
     * @param {Ext.scroll.Scroller} this
     */

    /**
     * @event scrollstart
     * Fires whenever the scrolling is started.
     * @param {Ext.scroll.Scroller} this
     * @param {Number} x The current x position.
     * @param {Number} y The current y position.
     */

    /**
     * @event scrollend
     * Fires whenever the scrolling is ended.
     * @param {Ext.scroll.Scroller} this
     * @param {Number} x The current x position.
     * @param {Number} y The current y position.
     */

    /**
     * @event scroll
     * Fires whenever the Scroller is scrolled.
     * @param {Ext.scroll.Scroller} this
     * @param {Number} x The new x position.
     * @param {Number} y The new y position.
     */

    config: {
        /**
         * @cfg element
         * @private
         */
        element: null,

        /**
         * @cfg {String} direction
         * Possible values: 'auto', 'vertical', 'horizontal', or 'both'.
         * @accessor
         */
        direction: 'auto',

        /**
         * @cfg fps
         * @private
         */
        fps: 'auto',

        /**
         * @cfg {Boolean} disabled
         * Whether or not this component is disabled.
         * @accessor
         */
        disabled: null,

        /**
         * @cfg {Boolean} directionLock
         * `true` to lock the direction of the scroller when the user starts scrolling.
         * This is useful when putting a scroller inside a scroller or a {@link Ext.Carousel}.
         * @accessor
         */
        directionLock: false,

        /**
         * @cfg {Object} momentumEasing
         * A valid config for {@link Ext.fx.easing.BoundMomentum}. The default value is:
         *
         *     {
         *         momentum: {
         *             acceleration: 30,
         *             friction: 0.5
         *         },
         *         bounce: {
         *             acceleration: 30,
         *             springTension: 0.3
         *         }
         *     }
         *
         * Note that supplied object will be recursively merged with the default object. For example, you can simply
         * pass this to change the momentum acceleration only:
         *
         *     {
         *         momentum: {
         *             acceleration: 10
         *         }
         *     }
         *
         * @accessor
         */
        momentumEasing: {
            momentum: {
                acceleration: 30,
                friction: 0.5
            },

            bounce: {
                acceleration: 30,
                springTension: 0.3
            },

            minVelocity: 1
        },

        /**
         * @cfg bounceEasing
         * @private
         */
        bounceEasing: {
            duration: 400
        },

        /**
         * @cfg outOfBoundRestrictFactor
         * @private
         */
        outOfBoundRestrictFactor: 0.5,

        /**
         * @cfg startMomentumResetTime
         * @private
         */
        startMomentumResetTime: 300,

        /**
         * @cfg maxAbsoluteVelocity
         * @private
         */
        maxAbsoluteVelocity: 6,

        /**
         * @cfg containerSize
         * @private
         */
        containerSize: 'auto',

        /**
         * @cfg size
         * @private
         */
        size: 'auto',

        /**
         * @cfg autoRefresh
         * @private
         */
        autoRefresh: true,

        /**
         * @cfg {Object/Number} initialOffset
         * The initial scroller position.  When specified as Number,
         * both x and y will be set to that value.
         */
        initialOffset: {
            x: 0,
            y: 0
        },

        /**
         * @cfg {Number/Object} slotSnapSize
         * The size of each slot to snap to in 'px', can be either an object with `x` and `y` values, i.e:
         *
         *      {
         *          x: 50,
         *          y: 100
         *      }
         *
         * or a number value to be used for both directions. For example, a value of `50` will be treated as:
         *
         *      {
         *          x: 50,
         *          y: 50
         *      }
         *
         * @accessor
         */
        slotSnapSize: {
            x: 0,
            y: 0
        },

        /**
         * @cfg slotSnapOffset
         * @private
         */
        slotSnapOffset: {
            x: 0,
            y: 0
        },

        slotSnapEasing: {
            duration: 150
        },

        translatable: {
            translationMethod: 'auto',
            useWrapper: false
        }
    },

    cls: Ext.baseCSSPrefix + 'scroll-scroller',

    containerCls: Ext.baseCSSPrefix + 'scroll-container',

    dragStartTime: 0,

    dragEndTime: 0,

    isDragging: false,

    isAnimating: false,

    /**
     * @private
     * @constructor
     * @chainable
     */
    constructor: function(config) {
        var element = config && config.element;

        this.listeners = {
            scope: this,
            touchstart: 'onTouchStart',
            touchend: 'onTouchEnd',
            dragstart: 'onDragStart',
            drag: 'onDrag',
            dragend: 'onDragEnd'
        };

        this.minPosition = { x: 0, y: 0 };

        this.startPosition = { x: 0, y: 0 };

        this.position = { x: 0, y: 0 };

        this.velocity = { x: 0, y: 0 };

        this.isAxisEnabledFlags = { x: false, y: false };

        this.flickStartPosition = { x: 0, y: 0 };

        this.flickStartTime = { x: 0, y: 0 };

        this.lastDragPosition = { x: 0, y: 0 };

        this.dragDirection = { x: 0, y: 0};

        this.initialConfig = config;

        if (element) {
            this.setElement(element);
        }

        return this;
    },

    /**
     * @private
     */
    applyElement: function(element) {
        if (!element) {
            return;
        }

        return Ext.get(element);
    },

    /**
     * @private
     * @chainable
     */
    updateElement: function(element) {
        this.initialize();

        if (!this.FixedHBoxStretching) {
            element.addCls(this.cls);
        }

        if (!this.getDisabled()) {
            this.attachListeneners();
        }

        this.onConfigUpdate(['containerSize', 'size'], 'refreshMaxPosition');

        this.on('maxpositionchange', 'snapToBoundary');
        this.on('minpositionchange', 'snapToBoundary');

        return this;
    },

    applyTranslatable: function(config, translatable) {
        return Ext.factory(config, Ext.util.Translatable, translatable);
    },

    updateTranslatable: function(translatable) {
        translatable.setConfig({
            element: this.getElement(),
            listeners: {
                animationframe: 'onAnimationFrame',
                animationend: 'onAnimationEnd',
                scope: this
            }
        });
    },

    updateFps: function(fps) {
        if (fps !== 'auto') {
            this.getTranslatable().setFps(fps);
        }
    },

    /**
     * @private
     */
    attachListeneners: function() {
        this.getContainer().on(this.listeners);
    },

    /**
     * @private
     */
    detachListeners: function() {
        this.getContainer().un(this.listeners);
    },

    /**
     * @private
     */
    updateDisabled: function(disabled) {
        if (disabled) {
            this.detachListeners();
        }
        else {
            this.attachListeneners();
        }
    },

    updateInitialOffset: function(initialOffset) {
        if (typeof initialOffset == 'number') {
            initialOffset = {
                x: initialOffset,
                y: initialOffset
            };
        }

        var position = this.position,
            x, y;

        position.x = x = initialOffset.x;
        position.y = y = initialOffset.y;

        this.getTranslatable().translate(-x, -y);
    },

    /**
     * @private
     * @return {String}
     */
    applyDirection: function(direction) {
        var minPosition = this.getMinPosition(),
            maxPosition = this.getMaxPosition(),
            isHorizontal, isVertical;

        this.givenDirection = direction;

        if (direction === 'auto') {
            isHorizontal = maxPosition.x > minPosition.x;
            isVertical = maxPosition.y > minPosition.y;

            if (isHorizontal && isVertical) {
                direction = 'both';
            }
            else if (isHorizontal) {
                direction = 'horizontal';
            }
            else {
                direction = 'vertical';
            }
        }

        return direction;
    },

    /**
     * @private
     */
    updateDirection: function(direction, oldDirection) {
        var isAxisEnabledFlags = this.isAxisEnabledFlags,
            verticalCls = this.cls + '-vertical',
            horizontalCls = this.cls + '-horizontal',
            element = this.getElement();

        if (oldDirection === 'both' || oldDirection === 'horizontal') {
            element.removeCls(horizontalCls);
        }

        if (oldDirection === 'both' || oldDirection === 'vertical') {
            element.removeCls(verticalCls);
        }

        isAxisEnabledFlags.x = isAxisEnabledFlags.y = false;
        if (direction === 'both' || direction === 'horizontal') {
            isAxisEnabledFlags.x = true;
            element.addCls(horizontalCls);
        }

        if (direction === 'both' || direction === 'vertical') {
            isAxisEnabledFlags.y = true;
            element.addCls(verticalCls);
        }
    },

    /**
     * Returns `true` if a specified axis is enabled.
     * @param {String} axis The axis to check (`x` or `y`).
     * @return {Boolean} `true` if the axis is enabled.
     */
    isAxisEnabled: function(axis) {
        this.getDirection();

        return this.isAxisEnabledFlags[axis];
    },

    /**
     * @private
     * @return {Object}
     */
    applyMomentumEasing: function(easing) {
        var defaultClass = Ext.fx.easing.BoundMomentum;

        return {
            x: Ext.factory(easing, defaultClass),
            y: Ext.factory(easing, defaultClass)
        };
    },

    /**
     * @private
     * @return {Object}
     */
    applyBounceEasing: function(easing) {
        var defaultClass = Ext.fx.easing.EaseOut;

        return {
            x: Ext.factory(easing, defaultClass),
            y: Ext.factory(easing, defaultClass)
        };
    },

    updateBounceEasing: function(easing) {
        this.getTranslatable().setEasingX(easing.x).setEasingY(easing.y);
    },

    /**
     * @private
     * @return {Object}
     */
    applySlotSnapEasing: function(easing) {
        var defaultClass = Ext.fx.easing.EaseOut;

        return {
            x: Ext.factory(easing, defaultClass),
            y: Ext.factory(easing, defaultClass)
        };
    },

    /**
     * @private
     * @return {Object}
     */
    getMinPosition: function() {
        var minPosition = this.minPosition;

        if (!minPosition) {
            this.minPosition = minPosition = {
                x: 0,
                y: 0
            };

            this.fireEvent('minpositionchange', this, minPosition);
        }

        return minPosition;
    },

    /**
     * @private
     * @return {Object}
     */
    getMaxPosition: function() {
        var maxPosition = this.maxPosition,
            size, containerSize;

        if (!maxPosition) {
            size = this.getSize();
            containerSize = this.getContainerSize();

            this.maxPosition = maxPosition = {
                x: Math.max(0, size.x - containerSize.x),
                y: Math.max(0, size.y - containerSize.y)
            };

            this.fireEvent('maxpositionchange', this, maxPosition);
        }

        return maxPosition;
    },

    /**
     * @private
     */
    refreshMaxPosition: function() {
        this.maxPosition = null;
        this.getMaxPosition();
    },

    /**
     * @private
     * @return {Object}
     */
    applyContainerSize: function(size) {
        var containerDom = this.getContainer().dom,
            x, y;

        if (!containerDom) {
            return;
        }

        this.givenContainerSize = size;

        if (size === 'auto') {
            x = containerDom.offsetWidth;
            y = containerDom.offsetHeight;
        }
        else {
            x = size.x;
            y = size.y;
        }

        return {
            x: x,
            y: y
        };
    },

    /**
     * @private
     * @param {String/Object} size
     * @return {Object}
     */
    applySize: function(size) {
        var dom = this.getElement().dom,
            x, y;

        if (!dom) {
            return;
        }

        this.givenSize = size;

        if (size === 'auto') {
            x = dom.offsetWidth;
            y = dom.offsetHeight;
        }
        else if (typeof size == 'number') {
            x = size;
            y = size;
        }
        else {
            x = size.x;
            y = size.y;
        }

        return {
            x: x,
            y: y
        };
    },

    /**
     * @private
     */
    updateAutoRefresh: function(autoRefresh) {
        this.getElement().toggleListener(autoRefresh, 'resize', 'onElementResize', this);
        this.getContainer().toggleListener(autoRefresh, 'resize', 'onContainerResize', this);
    },

    applySlotSnapSize: function(snapSize) {
        if (typeof snapSize == 'number') {
            return {
                x: snapSize,
                y: snapSize
            };
        }

        return snapSize;
    },

    applySlotSnapOffset: function(snapOffset) {
        if (typeof snapOffset == 'number') {
            return {
                x: snapOffset,
                y: snapOffset
            };
        }

        return snapOffset;
    },

    /**
     * @private
     * Returns the container for this scroller
     */
    getContainer: function() {
        var container = this.container,
            element;

        if (!container) {
            element = this.getElement().getParent();
            this.container = container = this.FixedHBoxStretching ? element.getParent() : element;
            //<debug error>
            if (!container) {
                Ext.Logger.error("Making an element scrollable that doesn't have any container");
            }
            //</debug>
            container.addCls(this.containerCls);
        }

        return container;
    },

    /**
     * @private
     * @return {Ext.scroll.Scroller} this
     * @chainable
     */
    refresh: function() {
        this.stopAnimation();

        this.getTranslatable().refresh();
        this.setSize(this.givenSize);
        this.setContainerSize(this.givenContainerSize);
        this.setDirection(this.givenDirection);

        this.fireEvent('refresh', this);

        return this;
    },

    onElementResize: function(element, info) {
        this.setSize({
            x: info.width,
            y: info.height
        });

        this.refresh();
    },

    onContainerResize: function(container, info) {
        this.setContainerSize({
            x: info.width,
            y: info.height
        });

        this.refresh();
    },

    /**
     * Scrolls to the given location.
     *
     * @param {Number} x The scroll position on the x axis.
     * @param {Number} y The scroll position on the y axis.
     * @param {Boolean/Object} animation (optional) Whether or not to animate the scrolling to the new position.
     *
     * @return {Ext.scroll.Scroller} this
     * @chainable
     */
    scrollTo: function(x, y, animation) {
        if (this.isDestroyed) {
            return this;
        }

        //<deprecated product=touch since=2.0>
        if (typeof x != 'number' && arguments.length === 1) {
            //<debug warn>
            Ext.Logger.deprecate("Calling scrollTo() with an object argument is deprecated, " +
                "please pass x and y arguments instead", this);
            //</debug>

            y = x.y;
            x = x.x;
        }
        //</deprecated>

        var translatable = this.getTranslatable(),
            position = this.position,
            positionChanged = false,
            translationX, translationY;

        if (this.isAxisEnabled('x')) {
            if (isNaN(x) || typeof x != 'number') {
                x = position.x;
            }
            else {
                if (position.x !== x) {
                    position.x = x;
                    positionChanged = true;
                }
            }

            translationX = -x;
        }

        if (this.isAxisEnabled('y')) {
            if (isNaN(y) || typeof y != 'number') {
                y = position.y;
            }
            else {
                if (position.y !== y) {
                    position.y = y;
                    positionChanged = true;
                }
            }

            translationY = -y;
        }

        if (positionChanged) {
            if (animation !== undefined && animation !== false) {
                translatable.translateAnimated(translationX, translationY, animation);
            }
            else {
                this.fireEvent('scroll', this, position.x, position.y);
                translatable.translate(translationX, translationY);
            }
        }

        return this;
    },

    /**
     * @private
     * @return {Ext.scroll.Scroller} this
     * @chainable
     */
    scrollToTop: function(animation) {
        var initialOffset = this.getInitialOffset();

        return this.scrollTo(initialOffset.x, initialOffset.y, animation);
    },

    /**
     * Scrolls to the end of the scrollable view.
     * @return {Ext.scroll.Scroller} this
     * @chainable
     */
    scrollToEnd: function(animation) {
        var size    = this.getSize(),
            cntSize = this.getContainerSize();

        return this.scrollTo(size.x - cntSize.x, size.y - cntSize.y, animation);
    },

    /**
     * Change the scroll offset by the given amount.
     * @param {Number} x The offset to scroll by on the x axis.
     * @param {Number} y The offset to scroll by on the y axis.
     * @param {Boolean/Object} animation (optional) Whether or not to animate the scrolling to the new position.
     * @return {Ext.scroll.Scroller} this
     * @chainable
     */
    scrollBy: function(x, y, animation) {
        var position = this.position;

        x = (typeof x == 'number') ? x + position.x : null;
        y = (typeof y == 'number') ? y + position.y : null;

        return this.scrollTo(x, y, animation);
    },

    /**
     * @private
     */
    onTouchStart: function() {
        this.isTouching = true;
        this.stopAnimation();
    },

    /**
     * @private
     */
    onTouchEnd: function() {
        var position = this.position;

        this.isTouching = false;

        if (!this.isDragging && this.snapToSlot()) {
            this.fireEvent('scrollstart', this, position.x, position.y);
        }
    },

    /**
     * @private
     */
    onDragStart: function(e) {
        var direction = this.getDirection(),
            absDeltaX = e.absDeltaX,
            absDeltaY = e.absDeltaY,
            directionLock = this.getDirectionLock(),
            startPosition = this.startPosition,
            flickStartPosition = this.flickStartPosition,
            flickStartTime = this.flickStartTime,
            lastDragPosition = this.lastDragPosition,
            currentPosition = this.position,
            dragDirection = this.dragDirection,
            x = currentPosition.x,
            y = currentPosition.y,
            now = Ext.Date.now();

        this.isDragging = true;

        if (directionLock && direction !== 'both') {
            if ((direction === 'horizontal' && absDeltaX > absDeltaY)
                    || (direction === 'vertical' && absDeltaY > absDeltaX)) {
                e.stopPropagation();
            }
            else {
                this.isDragging = false;
                return;
            }
        }

        lastDragPosition.x = x;
        lastDragPosition.y = y;

        flickStartPosition.x = x;
        flickStartPosition.y = y;

        startPosition.x = x;
        startPosition.y = y;

        flickStartTime.x = now;
        flickStartTime.y = now;

        dragDirection.x = 0;
        dragDirection.y = 0;

        this.dragStartTime = now;

        this.isDragging = true;

        this.fireEvent('scrollstart', this, x, y);
    },

    /**
     * @private
     */
    onAxisDrag: function(axis, delta) {
        if (!this.isAxisEnabled(axis)) {
            return;
        }

        var flickStartPosition = this.flickStartPosition,
            flickStartTime = this.flickStartTime,
            lastDragPosition = this.lastDragPosition,
            dragDirection = this.dragDirection,
            old = this.position[axis],
            min = this.getMinPosition()[axis],
            max = this.getMaxPosition()[axis],
            start = this.startPosition[axis],
            last = lastDragPosition[axis],
            current = start - delta,
            lastDirection = dragDirection[axis],
            restrictFactor = this.getOutOfBoundRestrictFactor(),
            startMomentumResetTime = this.getStartMomentumResetTime(),
            now = Ext.Date.now(),
            distance;

        if (current < min) {
            current *= restrictFactor;
        }
        else if (current > max) {
            distance = current - max;
            current = max + distance * restrictFactor;
        }

        if (current > last) {
            dragDirection[axis] = 1;
        }
        else if (current < last) {
            dragDirection[axis] = -1;
        }

        if ((lastDirection !== 0 && (dragDirection[axis] !== lastDirection))
                || (now - flickStartTime[axis]) > startMomentumResetTime) {
            flickStartPosition[axis] = old;
            flickStartTime[axis] = now;
        }

        lastDragPosition[axis] = current;
    },

    /**
     * @private
     */
    onDrag: function(e) {
        if (!this.isDragging) {
            return;
        }

        var lastDragPosition = this.lastDragPosition;

        this.onAxisDrag('x', e.deltaX);
        this.onAxisDrag('y', e.deltaY);

        this.scrollTo(lastDragPosition.x, lastDragPosition.y);
    },

    /**
     * @private
     */
    onDragEnd: function(e) {
        var easingX, easingY;

        if (!this.isDragging) {
            return;
        }

        this.dragEndTime = Ext.Date.now();

        this.onDrag(e);

        this.isDragging = false;

        easingX = this.getAnimationEasing('x', e);
        easingY = this.getAnimationEasing('y', e);

        if (easingX || easingY) {
            this.getTranslatable().animate(easingX, easingY);
        }
        else {
            this.onScrollEnd();
        }
    },

    /**
     * @private
     */
    getAnimationEasing: function(axis, e) {
        if (!this.isAxisEnabled(axis)) {
            return null;
        }

        var currentPosition = this.position[axis],
            minPosition = this.getMinPosition()[axis],
            maxPosition = this.getMaxPosition()[axis],
            maxAbsVelocity = this.getMaxAbsoluteVelocity(),
            boundValue = null,
            dragEndTime = this.dragEndTime,
            velocity = e.flick.velocity[axis],
            easing;

        if (currentPosition < minPosition) {
            boundValue = minPosition;
        }
        else if (currentPosition > maxPosition) {
            boundValue = maxPosition;
        }

        // Out of bound, to be pulled back
        if (boundValue !== null) {
            easing = this.getBounceEasing()[axis];
            easing.setConfig({
                startTime: dragEndTime,
                startValue: -currentPosition,
                endValue: -boundValue
            });

            return easing;
        }

        if (velocity === 0) {
            return null;
        }

        if (velocity < -maxAbsVelocity) {
            velocity = -maxAbsVelocity;
        }
        else if (velocity > maxAbsVelocity) {
            velocity = maxAbsVelocity;
        }

        if (Ext.browser.is.IE) {
            velocity *= 2;
        }

        easing = this.getMomentumEasing()[axis];
        easing.setConfig({
            startTime: dragEndTime,
            startValue: -currentPosition,
            startVelocity: velocity * 1.5,
            minMomentumValue: -maxPosition,
            maxMomentumValue: 0
        });

        return easing;
    },

    /**
     * @private
     */
    onAnimationFrame: function(translatable, x, y) {
        var position = this.position;

        position.x = -x;
        position.y = -y;

        this.fireEvent('scroll', this, position.x, position.y);
    },

    /**
     * @private
     */
    onAnimationEnd: function() {
        this.snapToBoundary();
        this.onScrollEnd();
    },

    /**
     * @private
     * Stops the animation of the scroller at any time.
     */
    stopAnimation: function() {
        this.getTranslatable().stopAnimation();
    },

    /**
     * @private
     */
    onScrollEnd: function() {
        var position = this.position;

        if (this.isTouching || !this.snapToSlot()) {
            this.fireEvent('scrollend', this, position.x, position.y);
        }
    },

    /**
     * @private
     * @return {Boolean}
     */
    snapToSlot: function() {
        var snapX = this.getSnapPosition('x'),
            snapY = this.getSnapPosition('y'),
            easing = this.getSlotSnapEasing();

        if (snapX !== null || snapY !== null) {
            this.scrollTo(snapX, snapY, {
                easingX: easing.x,
                easingY: easing.y
            });

            return true;
        }

        return false;
    },

    /**
     * @private
     * @return {Number/null}
     */
    getSnapPosition: function(axis) {
        var snapSize = this.getSlotSnapSize()[axis],
            snapPosition = null,
            position, snapOffset, maxPosition, mod;

        if (snapSize !== 0 && this.isAxisEnabled(axis)) {
            position = this.position[axis];
            snapOffset = this.getSlotSnapOffset()[axis];
            maxPosition = this.getMaxPosition()[axis];

            mod = Math.floor((position - snapOffset) % snapSize);

            if (mod !== 0) {
                if (position !== maxPosition) {
                    if (Math.abs(mod) > snapSize / 2) {
                        snapPosition = Math.min(maxPosition, position + ((mod > 0) ? snapSize - mod : mod - snapSize));
                    }
                    else {
                        snapPosition = position - mod;
                    }
                }
                else {
                    snapPosition = position - mod;
                }
            }
        }

        return snapPosition;
    },

    /**
     * @private
     */
    snapToBoundary: function() {
        var position = this.position,
            minPosition = this.getMinPosition(),
            maxPosition = this.getMaxPosition(),
            minX = minPosition.x,
            minY = minPosition.y,
            maxX = maxPosition.x,
            maxY = maxPosition.y,
            x = Math.round(position.x),
            y = Math.round(position.y);

        if (x < minX) {
            x = minX;
        }
        else if (x > maxX) {
            x = maxX;
        }

        if (y < minY) {
            y = minY;
        }
        else if (y > maxY) {
            y = maxY;
        }

        this.scrollTo(x, y);
    },

    destroy: function() {
        var element = this.getElement(),
            sizeMonitors = this.sizeMonitors,
            container;

        if (sizeMonitors) {
            sizeMonitors.element.destroy();
            sizeMonitors.container.destroy();
        }

        if (element && !element.isDestroyed) {
            element.removeCls(this.cls);
            container = this.getContainer();
            if (container && !container.isDestroyed) {
                container.removeCls(this.containerCls);
            }
        }

        Ext.destroy(this.getTranslatable());

        this.callParent(arguments);
    }

}, function() {
    //<deprecated product=touch since=2.0>
    this.override({
        constructor: function(config) {
            var element, acceleration, slotSnapOffset, friction, springTension, minVelocity;

            if (!config) {
                config = {};
            }

            if (typeof config == 'string') {
                config = {
                    direction: config
                };
            }

            if (arguments.length == 2) {
                //<debug warn>
                Ext.Logger.deprecate("Passing element as the first argument is deprecated, pass it as the " +
                    "'element' property of the config object instead");
                //</debug>
                element = config;
                config = arguments[1];

                if (!config) {
                    config = {};
                }

                config.element = element;
            }

            /**
             * @cfg {Number} acceleration A higher acceleration gives the scroller more initial velocity.
             * @deprecated 2.0.0 Please use {@link #momentumEasing}.momentum.acceleration and {@link #momentumEasing}.bounce.acceleration instead.
             */
            if (config.hasOwnProperty('acceleration')) {
                acceleration = config.acceleration;
                delete config.acceleration;
                //<debug warn>
                Ext.Logger.deprecate("'acceleration' config is deprecated, set momentumEasing.momentum.acceleration and momentumEasing.bounce.acceleration configs instead");
                //</debug>

                Ext.merge(config, {
                    momentumEasing: {
                        momentum: { acceleration: acceleration },
                        bounce: { acceleration: acceleration }
                    }
                });
            }

            if (config.hasOwnProperty('snap')) {
                config.slotSnapOffset = config.snap;
                //<debug warn>
                Ext.Logger.deprecate("'snap' config is deprecated, please use the 'slotSnapOffset' config instead");
                //</debug>
            }

            /**
             * @cfg {Number} friction The friction of the scroller. By raising this value the length that momentum scrolls
             * becomes shorter. This value is best kept between 0 and 1.
             * @deprecated 2.0.0 Please set the {@link #momentumEasing}.momentum.friction configuration instead
             */
            if (config.hasOwnProperty('friction')) {
                friction = config.friction;
                delete config.friction;
                //<debug warn>
                Ext.Logger.deprecate("'friction' config is deprecated, set momentumEasing.momentum.friction config instead");
                //</debug>

                Ext.merge(config, {
                    momentumEasing: {
                        momentum: { friction: friction }
                    }
                });
            }

            if (config.hasOwnProperty('springTension')) {
                springTension = config.springTension;
                delete config.springTension;
                //<debug warn>
                Ext.Logger.deprecate("'springTension' config is deprecated, set momentumEasing.momentum.springTension config instead");
                //</debug>

                Ext.merge(config, {
                    momentumEasing: {
                        momentum: { springTension: springTension }
                    }
                });
            }

            if (config.hasOwnProperty('minVelocityForAnimation')) {
                minVelocity = config.minVelocityForAnimation;
                delete config.minVelocityForAnimation;
                //<debug warn>
                Ext.Logger.deprecate("'minVelocityForAnimation' config is deprecated, set momentumEasing.minVelocity config instead");
                //</debug>

                Ext.merge(config, {
                    momentumEasing: {
                        minVelocity: minVelocity
                    }
                });
            }

            this.callOverridden(arguments);
        },

        scrollToAnimated: function(x, y, animation) {
            //<debug warn>
            Ext.Logger.deprecate("scrollToAnimated() is deprecated, please use `scrollTo()` and pass 'animation' as " +
                "the third argument instead");
            //</debug>

            return this.scrollTo.apply(this, arguments);
        },

        scrollBy: function(x, y, animation) {
            if (Ext.isObject(x)) {
                //<debug warn>
                Ext.Logger.deprecate("calling `scrollBy()` with an object of `x` and `y` properties is no longer supported. " +
                        "Please pass `x` and `y` values as two separate arguments instead");
                //</debug>
                y = x.y;
                x = x.x;
            }

            return this.callOverridden([x, y, animation]);
        },

        /**
         * Sets the offset of this scroller.
         * @param {Object} offset The offset to move to.
         * @param {Number} offset.x The x-axis offset.
         * @param {Number} offset.y The y-axis offset.
         * @deprecated 2.0.0 Please use `{@link #scrollTo}` instead.
         * @return {Ext.scroll.Scroller} this
         * @chainable
         */
        setOffset: function(offset) {
            return this.scrollToAnimated(-offset.x, -offset.y);
        }
    });

    /**
     * @method updateBoundary
     * Updates the boundary information for this scroller.
     * @return {Ext.scroll.Scroller} this
     * @removed 2.0.0 Please use {@link #method-refresh} instead.
     * @chainable
     */
//    Ext.deprecateClassMethod('updateBoundary', 'refresh');
    //</deprecated>
});