;(function(undefined) {
    'use strict';
    /**
     * BottleJS v1.7.2 - 2019-02-07
     * A powerful dependency injection micro container
     *
     * Copyright (c) 2019 Stephen Young
     * Licensed MIT
     */
    var Bottle;

    /**
     * String constants
     */
    var DELIMITER = '.';
    var FUNCTION_TYPE = 'function';
    var STRING_TYPE = 'string';
    var GLOBAL_NAME = '__global__';
    var PROVIDER_SUFFIX = 'Provider';

    /**
     * Unique id counter;
     *
     * @type Number
     */
    var id = 0;

    /**
     * Local slice alias
     *
     * @type Functions
     */
    var slice = Array.prototype.slice;

    /**
     * Iterator used to walk down a nested object.
     *
     * If Bottle.config.strict is true, this method will throw an exception if it encounters an
     * undefined path
     *
     * @param Object obj
     * @param String prop
     * @return mixed
     * @throws Error if Bottle is unable to resolve the requested service.
     */
    var getNested = function getNested(obj, prop) {
        var service = obj[prop];
        if (service === undefined && Bottle.config.strict) {
            throw new Error('Bottle was unable to resolve a service.  `' + prop + '` is undefined.');
        }
        return service;
    };

    /**
     * Get a nested bottle. Will set and return if not set.
     *
     * @param String name
     * @return Bottle
     */
    var getNestedBottle = function getNestedBottle(name) {
        var bottle;
        if (!this.nested[name]) {
            bottle = Bottle.pop();
            this.nested[name] = bottle;
            this.factory(name, function SubProviderFactory() {
                return bottle.container;
            });
        }
        return this.nested[name];
    };

    /**
     * Get a service stored under a nested key
     *
     * @param String fullname
     * @return Service
     */
    var getNestedService = function getNestedService(fullname) {
        return fullname.split(DELIMITER).reduce(getNested, this);
    };

    /**
     * Function used by provider to set up middleware for each request.
     *
     * @param Number id
     * @param String name
     * @param Object instance
     * @param Object container
     * @return void
     */
    var applyMiddleware = function applyMiddleware(middleware, name, instance, container) {
        var descriptor = {
            configurable : true,
            enumerable : true
        };
        if (middleware.length) {
            descriptor.get = function getWithMiddlewear() {
                var index = 0;
                var next = function nextMiddleware(err) {
                    if (err) {
                        throw err;
                    }
                    if (middleware[index]) {
                        middleware[index++](instance, next);
                    }
                };
                next();
                return instance;
            };
        } else {
            descriptor.value = instance;
            descriptor.writable = true;
        }

        Object.defineProperty(container, name, descriptor);

        return container[name];
    };

    /**
     * Register middleware.
     *
     * @param String name
     * @param Function func
     * @return Bottle
     */
    var middleware = function middleware(fullname, func) {
        var parts, name;
        if (typeof fullname === FUNCTION_TYPE) {
            func = fullname;
            fullname = GLOBAL_NAME;
        }

        parts = fullname.split(DELIMITER);
        name = parts.shift();
        if (parts.length) {
            getNestedBottle.call(this, name).middleware(parts.join(DELIMITER), func);
        } else {
            if (!this.middlewares[name]) {
                this.middlewares[name] = [];
            }
            this.middlewares[name].push(func);
        }
        return this;
    };

    /**
     * Used to process decorators in the provider
     *
     * @param Object instance
     * @param Function func
     * @return Mixed
     */
    var reducer = function reducer(instance, func) {
        return func(instance);
    };

    /**
     * Get decorators and middleware including globals
     *
     * @return array
     */
    var getWithGlobal = function getWithGlobal(collection, name) {
        return (collection[name] || []).concat(collection.__global__ || []);
    };

    /**
     * Create the provider properties on the container
     *
     * @param String name
     * @param Function Provider
     * @return Bottle
     */
    var createProvider = function createProvider(name, Provider) {
        var providerName, properties, container, id, decorators, middlewares;

        id = this.id;
        container = this.container;
        decorators = this.decorators;
        middlewares = this.middlewares;
        providerName = name + PROVIDER_SUFFIX;

        properties = Object.create(null);
        properties[providerName] = {
            configurable : true,
            enumerable : true,
            get : function getProvider() {
                var instance = new Provider();
                delete container[providerName];
                container[providerName] = instance;
                return instance;
            }
        };

        properties[name] = {
            configurable : true,
            enumerable : true,
            get : function getService() {
                var provider = container[providerName];
                var instance;
                if (provider) {
                    // filter through decorators
                    instance = getWithGlobal(decorators, name).reduce(reducer, provider.$get(container));

                    delete container[providerName];
                    delete container[name];
                }
                return instance === undefined ? instance : applyMiddleware(getWithGlobal(middlewares, name),
                    name, instance, container);
            }
        };

        Object.defineProperties(container, properties);
        return this;
    };

    /**
     * Register a provider.
     *
     * @param String fullname
     * @param Function Provider
     * @return Bottle
     */
    var provider = function provider(fullname, Provider) {
        var parts, name;
        parts = fullname.split(DELIMITER);
        if (this.providerMap[fullname] && parts.length === 1 && !this.container[fullname + PROVIDER_SUFFIX]) {
            return console.error(fullname + ' provider already instantiated.');
        }
        this.originalProviders[fullname] = Provider;
        this.providerMap[fullname] = true;

        name = parts.shift();

        if (parts.length) {
            getNestedBottle.call(this, name).provider(parts.join(DELIMITER), Provider);
            return this;
        }
        return createProvider.call(this, name, Provider);
    };

    /**
     * Register a factory inside a generic provider.
     *
     * @param String name
     * @param Function Factory
     * @return Bottle
     */
    var factory = function factory(name, Factory) {
        return provider.call(this, name, function GenericProvider() {
            this.$get = Factory;
        });
    };

    /**
     * Private helper for creating service and service factories.
     *
     * @param String name
     * @param Function Service
     * @return Bottle
     */
    var createService = function createService(name, Service, isClass) {
        var deps = arguments.length > 3 ? slice.call(arguments, 3) : [];
        var bottle = this;
        return factory.call(this, name, function GenericFactory() {
            var serviceFactory = Service; // alias for jshint
            var args = deps.map(getNestedService, bottle.container);

            if (!isClass) {
                return serviceFactory.apply(null, args);
            }
            return new (Service.bind.apply(Service, [null].concat(args)))();
        });
    };

    /**
     * Register a class service
     *
     * @param String name
     * @param Function Service
     * @return Bottle
     */
    var service = function service(name, Service) {
        return createService.apply(this, [name, Service, true].concat(slice.call(arguments, 2)));
    };

    /**
     * Register a function service
     */
    var serviceFactory = function serviceFactory(name, factoryService) {
        return createService.apply(this, [name, factoryService, false].concat(slice.call(arguments, 2)));
    };

    /**
     * Define a mutable property on the container.
     *
     * @param String name
     * @param mixed val
     * @return void
     * @scope container
     */
    var defineValue = function defineValue(name, val) {
        Object.defineProperty(this, name, {
            configurable : true,
            enumerable : true,
            value : val,
            writable : true
        });
    };

    /**
     * Iterator for setting a plain object literal via defineValue
     *
     * @param Object container
     * @param string name
     */
    var setValueObject = function setValueObject(container, name) {
        var nestedContainer = container[name];
        if (!nestedContainer) {
            nestedContainer = {};
            defineValue.call(container, name, nestedContainer);
        }
        return nestedContainer;
    };

    /**
     * Register a value
     *
     * @param String name
     * @param mixed val
     * @return Bottle
     */
    var value = function value(name, val) {
        var parts;
        parts = name.split(DELIMITER);
        name = parts.pop();
        defineValue.call(parts.reduce(setValueObject, this.container), name, val);
        return this;
    };

    /**
     * Define an enumerable, non-configurable, non-writable value.
     *
     * @param String name
     * @param mixed value
     * @return undefined
     */
    var defineConstant = function defineConstant(name, value) {
        Object.defineProperty(this, name, {
            configurable : false,
            enumerable : true,
            value : value,
            writable : false
        });
    };

    /**
     * Register a constant
     *
     * @param String name
     * @param mixed value
     * @return Bottle
     */
    var constant = function constant(name, value) {
        var parts = name.split(DELIMITER);
        name = parts.pop();
        defineConstant.call(parts.reduce(setValueObject, this.container), name, value);
        return this;
    };

    /**
     * Register decorator.
     *
     * @param String fullname
     * @param Function func
     * @return Bottle
     */
    var decorator = function decorator(fullname, func) {
        var parts, name;
        if (typeof fullname === FUNCTION_TYPE) {
            func = fullname;
            fullname = GLOBAL_NAME;
        }

        parts = fullname.split(DELIMITER);
        name = parts.shift();
        if (parts.length) {
            getNestedBottle.call(this, name).decorator(parts.join(DELIMITER), func);
        } else {
            if (!this.decorators[name]) {
                this.decorators[name] = [];
            }
            this.decorators[name].push(func);
        }
        return this;
    };

    /**
     * Register a function that will be executed when Bottle#resolve is called.
     *
     * @param Function func
     * @return Bottle
     */
    var defer = function defer(func) {
        this.deferred.push(func);
        return this;
    };

    /**
     * Immediately instantiates the provided list of services and returns them.
     *
     * @param Array services
     * @return Array Array of instances (in the order they were provided)
     */
    var digest = function digest(services) {
        return (services || []).map(getNestedService, this.container);
    };

    /**
     * Register an instance factory inside a generic factory.
     *
     * @param {String} name - The name of the service
     * @param {Function} Factory - The factory function, matches the signature required for the
     * `factory` method
     * @return Bottle
     */
    var instanceFactory = function instanceFactory(name, Factory) {
        return factory.call(this, name, function GenericInstanceFactory(container) {
            return {
                instance : Factory.bind(Factory, container)
            };
        });
    };

    /**
     * A filter function for removing bottle container methods and providers from a list of keys
     */
    var byMethod = function byMethod(name) {
        return !/^\$(?:decorator|register|list)$|Provider$/.test(name);
    };

    /**
     * List the services registered on the container.
     *
     * @param Object container
     * @return Array
     */
    var list = function list(container) {
        return Object.keys(container || this.container || {}).filter(byMethod);
    };

    /**
     * Named bottle instances
     *
     * @type Object
     */
    var bottles = {};

    /**
     * Get an instance of bottle.
     *
     * If a name is provided the instance will be stored in a local hash.  Calling Bottle.pop multiple
     * times with the same name will return the same instance.
     *
     * @param String name
     * @return Bottle
     */
    var pop = function pop(name) {
        var instance;
        if (typeof name === STRING_TYPE) {
            instance = bottles[name];
            if (!instance) {
                bottles[name] = instance = new Bottle();
                instance.constant('BOTTLE_NAME', name);
            }
            return instance;
        }
        return new Bottle();
    };

    /**
     * Clear all named bottles.
     */
    var clear = function clear(name) {
        if (typeof name === STRING_TYPE) {
            delete bottles[name];
        } else {
            bottles = {};
        }
    };

    /**
     * Register a service, factory, provider, or value based on properties on the object.
     *
     * properties:
     *  * Obj.$name   String required ex: `'Thing'`
     *  * Obj.$type   String optional 'service', 'factory', 'provider', 'value'.  Default: 'service'
     *  * Obj.$inject Mixed  optional only useful with $type 'service' name or array of names
     *  * Obj.$value  Mixed  optional Normally Obj is registered on the container.  However, if this
     *                       property is included, it's value will be registered on the container
     *                       instead of the object itsself.  Useful for registering objects on the
     *                       bottle container without modifying those objects with bottle specific keys.
     *
     * @param Function Obj
     * @return Bottle
     */
    var register = function register(Obj) {
        var value = Obj.$value === undefined ? Obj : Obj.$value;
        return this[Obj.$type || 'service'].apply(this, [Obj.$name, value].concat(Obj.$inject || []));
    };

    /**
     * Deletes providers from the map and container.
     *
     * @param String name
     * @return void
     */
    var removeProviderMap = function resetProvider(name) {
        delete this.providerMap[name];
        delete this.container[name];
        delete this.container[name + PROVIDER_SUFFIX];
    };

    /**
     * Resets providers on a bottle instance. If 'names' array is provided, only the named providers will be reset.
     *
     * @param Array names
     * @return void
     */
    var resetProviders = function resetProviders(names) {
        var tempProviders = this.originalProviders;
        var shouldFilter = Array.isArray(names);

        Object.keys(this.originalProviders).forEach(function resetProvider(originalProviderName) {
            if (shouldFilter && names.indexOf(originalProviderName) === -1) {
                return;
            }
            var parts = originalProviderName.split(DELIMITER);
            if (parts.length > 1) {
                parts.forEach(removeProviderMap, getNestedBottle.call(this, parts[0]));
            }
            removeProviderMap.call(this, originalProviderName);
            this.provider(originalProviderName, tempProviders[originalProviderName]);
        }, this);
    };

    /**
     * Execute any deferred functions
     *
     * @param Mixed data
     * @return Bottle
     */
    var resolve = function resolve(data) {
        this.deferred.forEach(function deferredIterator(func) {
            func(data);
        });

        return this;
    };

    /**
     * Bottle constructor
     *
     * @param String name Optional name for functional construction
     */
    Bottle = function Bottle(name) {
        if (!(this instanceof Bottle)) {
            return Bottle.pop(name);
        }

        this.id = id++;

        this.decorators = {};
        this.middlewares = {};
        this.nested = {};
        this.providerMap = {};
        this.originalProviders = {};
        this.deferred = [];
        this.container = {
            $decorator : decorator.bind(this),
            $register : register.bind(this),
            $list : list.bind(this)
        };
    };

    /**
     * Bottle prototype
     */
    Bottle.prototype = {
        constant : constant,
        decorator : decorator,
        defer : defer,
        digest : digest,
        factory : factory,
        instanceFactory: instanceFactory,
        list : list,
        middleware : middleware,
        provider : provider,
        resetProviders : resetProviders,
        register : register,
        resolve : resolve,
        service : service,
        serviceFactory : serviceFactory,
        value : value
    };

    /**
     * Bottle static
     */
    Bottle.pop = pop;
    Bottle.clear = clear;
    Bottle.list = list;

    /**
     * Global config
     */
    Bottle.config = {
        strict : false
    };

    /**
     * Exports script adapted from lodash v2.4.1 Modern Build
     *
     * @see http://lodash.com/
     */

    /**
     * Valid object type map
     *
     * @type Object
     */
    var objectTypes = {
        'function' : true,
        'object' : true
    };

    (function exportBottle(root) {

        /**
         * Free variable exports
         *
         * @type Function
         */
        var freeExports = objectTypes[typeof exports] && exports && !exports.nodeType && exports;

        /**
         * Free variable module
         *
         * @type Object
         */
        var freeModule = objectTypes[typeof module] && module && !module.nodeType && module;

        /**
         * CommonJS module.exports
         *
         * @type Function
         */
        var moduleExports = freeModule && freeModule.exports === freeExports && freeExports;

        /**
         * Free variable `global`
         *
         * @type Object
         */
        var freeGlobal = objectTypes[typeof global] && global;
        if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal)) {
            root = freeGlobal;
        }

        /**
         * Export
         */
        if (typeof define === FUNCTION_TYPE && typeof define.amd === 'object' && define.amd) {
            root.Bottle = Bottle;
            define(function() { return Bottle; });
        } else if (freeExports && freeModule) {
            if (moduleExports) {
                (freeModule.exports = Bottle).Bottle = Bottle;
            } else {
                freeExports.Bottle = Bottle;
            }
        } else {
            root.Bottle = Bottle;
        }
    }((objectTypes[typeof window] && window) || this));

}.call(this));


const DEFAULT_OPTIONS = { order: 2, precision: 2, period: null };

/**
* Determine the coefficient of determination (r^2) of a fit from the observations
* and predictions.
*
* @param {Array<Array<number>>} data - Pairs of observed x-y values
* @param {Array<Array<number>>} results - Pairs of observed predicted x-y values
*
* @return {number} - The r^2 value, or NaN if one cannot be calculated.
*/
function determinationCoefficient(data, results) {
  const predictions = [];
  const observations = [];

  data.forEach((d, i) => {
    if (d[1] !== null) {
      observations.push(d);
      predictions.push(results[i]);
    }
  });

  const sum = observations.reduce((a, observation) => a + observation[1], 0);
  const mean = sum / observations.length;

  const ssyy = observations.reduce((a, observation) => {
    const difference = observation[1] - mean;
    return a + (difference * difference);
  }, 0);

  const sse = observations.reduce((accum, observation, index) => {
    const prediction = predictions[index];
    const residual = observation[1] - prediction[1];
    return accum + (residual * residual);
  }, 0);

  return 1 - (sse / ssyy);
}

/**
* Determine the solution of a system of linear equations A * x = b using
* Gaussian elimination.
*
* @param {Array<Array<number>>} input - A 2-d matrix of data in row-major form [ A | b ]
* @param {number} order - How many degrees to solve for
*
* @return {Array<number>} - Vector of normalized solution coefficients matrix (x)
*/
function gaussianElimination(input, order) {
  const matrix = input;
  const n = input.length - 1;
  const coefficients = [order];

  for (let i = 0; i < n; i++) {
    let maxrow = i;
    for (let j = i + 1; j < n; j++) {
      if (Math.abs(matrix[i][j]) > Math.abs(matrix[i][maxrow])) {
        maxrow = j;
      }
    }

    for (let k = i; k < n + 1; k++) {
      const tmp = matrix[k][i];
      matrix[k][i] = matrix[k][maxrow];
      matrix[k][maxrow] = tmp;
    }

    for (let j = i + 1; j < n; j++) {
      for (let k = n; k >= i; k--) {
        matrix[k][j] -= (matrix[k][i] * matrix[i][j]) / matrix[i][i];
      }
    }
  }

  for (let j = n - 1; j >= 0; j--) {
    let total = 0;
    for (let k = j + 1; k < n; k++) {
      total += matrix[k][j] * coefficients[k];
    }

    coefficients[j] = (matrix[n][j] - total) / matrix[j][j];
  }

  return coefficients;
}

/**
* Round a number to a precision, specificed in number of decimal places
*
* @param {number} number - The number to round
* @param {number} precision - The number of decimal places to round to:
*                             > 0 means decimals, < 0 means powers of 10
*
*
* @return {numbr} - The number, rounded
*/
function round(number, precision) {
  const factor = 10 ** precision;
  return Math.round(number * factor) / factor;
}

/**
* The set of all fitting methods
*
* @namespace
*/
const methods = {
  linear(data, options) {
    const sum = [0, 0, 0, 0, 0];
    let len = 0;

    for (let n = 0; n < data.length; n++) {
      if (data[n][1] !== null) {
        len++;
        sum[0] += data[n][0];
        sum[1] += data[n][1];
        sum[2] += data[n][0] * data[n][0];
        sum[3] += data[n][0] * data[n][1];
        sum[4] += data[n][1] * data[n][1];
      }
    }

    const run = ((len * sum[2]) - (sum[0] * sum[0]));
    const rise = ((len * sum[3]) - (sum[0] * sum[1]));
    const gradient = run === 0 ? 0 : round(rise / run, options.precision);
    const intercept = round((sum[1] / len) - ((gradient * sum[0]) / len), options.precision);

    const predict = x => ([
      round(x, options.precision),
      round((gradient * x) + intercept, options.precision)]
    );

    const points = data.map(point => predict(point[0]));

    return {
      points,
      predict,
      equation: [gradient, intercept],
      r2: round(determinationCoefficient(data, points), options.precision),
      string: intercept === 0 ? `y = ${gradient}x` : `y = ${gradient}x + ${intercept}`,
    };
  },

  exponential(data, options) {
    const sum = [0, 0, 0, 0, 0, 0];

    for (let n = 0; n < data.length; n++) {
      if (data[n][1] !== null) {
        sum[0] += data[n][0];
        sum[1] += data[n][1];
        sum[2] += data[n][0] * data[n][0] * data[n][1];
        sum[3] += data[n][1] * Math.log(data[n][1]);
        sum[4] += data[n][0] * data[n][1] * Math.log(data[n][1]);
        sum[5] += data[n][0] * data[n][1];
      }
    }

    const denominator = ((sum[1] * sum[2]) - (sum[5] * sum[5]));
    const a = Math.exp(((sum[2] * sum[3]) - (sum[5] * sum[4])) / denominator);
    const b = ((sum[1] * sum[4]) - (sum[5] * sum[3])) / denominator;
    const coeffA = round(a, options.precision);
    const coeffB = round(b, options.precision);
    const predict = x => ([
      round(x, options.precision),
      round(coeffA * Math.exp(coeffB * x), options.precision),
    ]);

    const points = data.map(point => predict(point[0]));

    return {
      points,
      predict,
      equation: [coeffA, coeffB],
      string: `y = ${coeffA}e^(${coeffB}x)`,
      r2: round(determinationCoefficient(data, points), options.precision),
    };
  },

  logarithmic(data, options) {
    const sum = [0, 0, 0, 0];
    const len = data.length;

    for (let n = 0; n < len; n++) {
      if (data[n][1] !== null) {
        sum[0] += Math.log(data[n][0]);
        sum[1] += data[n][1] * Math.log(data[n][0]);
        sum[2] += data[n][1];
        sum[3] += (Math.log(data[n][0]) ** 2);
      }
    }

    const a = ((len * sum[1]) - (sum[2] * sum[0])) / ((len * sum[3]) - (sum[0] * sum[0]));
    const coeffB = round(a, options.precision);
    const coeffA = round((sum[2] - (coeffB * sum[0])) / len, options.precision);

    const predict = x => ([
      round(x, options.precision),
      round(round(coeffA + (coeffB * Math.log(x)), options.precision), options.precision),
    ]);

    const points = data.map(point => predict(point[0]));

    return {
      points,
      predict,
      equation: [coeffA, coeffB],
      string: `y = ${coeffA} + ${coeffB} ln(x)`,
      r2: round(determinationCoefficient(data, points), options.precision),
    };
  },

  power(data, options) {
    const sum = [0, 0, 0, 0, 0];
    const len = data.length;

    for (let n = 0; n < len; n++) {
      if (data[n][1] !== null) {
        sum[0] += Math.log(data[n][0]);
        sum[1] += Math.log(data[n][1]) * Math.log(data[n][0]);
        sum[2] += Math.log(data[n][1]);
        sum[3] += (Math.log(data[n][0]) ** 2);
      }
    }

    const b = ((len * sum[1]) - (sum[0] * sum[2])) / ((len * sum[3]) - (sum[0] ** 2));
    const a = ((sum[2] - (b * sum[0])) / len);
    const coeffA = round(Math.exp(a), options.precision);
    const coeffB = round(b, options.precision);

    const predict = x => ([
      round(x, options.precision),
      round(round(coeffA * (x ** coeffB), options.precision), options.precision),
    ]);

    const points = data.map(point => predict(point[0]));

    return {
      points,
      predict,
      equation: [coeffA, coeffB],
      string: `y = ${coeffA}x^${coeffB}`,
      r2: round(determinationCoefficient(data, points), options.precision),
    };
  },

  polynomial(data, options) {
    const lhs = [];
    const rhs = [];
    let a = 0;
    let b = 0;
    const len = data.length;
    const k = options.order + 1;

    for (let i = 0; i < k; i++) {
      for (let l = 0; l < len; l++) {
        if (data[l][1] !== null) {
          a += (data[l][0] ** i) * data[l][1];
        }
      }

      lhs.push(a);
      a = 0;

      const c = [];
      for (let j = 0; j < k; j++) {
        for (let l = 0; l < len; l++) {
          if (data[l][1] !== null) {
            b += data[l][0] ** (i + j);
          }
        }
        c.push(b);
        b = 0;
      }
      rhs.push(c);
    }
    rhs.push(lhs);

    const coefficients = gaussianElimination(rhs, k).map(v => round(v, options.precision));

    const predict = x => ([
      round(x, options.precision),
      round(
        coefficients.reduce((sum, coeff, power) => sum + (coeff * (x ** power)), 0),
        options.precision,
      ),
    ]);

    const points = data.map(point => predict(point[0]));

    let string = 'y = ';
    for (let i = coefficients.length - 1; i >= 0; i--) {
      if (i > 1) {
        string += `${coefficients[i]}x^${i} + `;
      } else if (i === 1) {
        string += `${coefficients[i]}x + `;
      } else {
        string += coefficients[i];
      }
    }

    return {
      string,
      points,
      predict,
      equation: [...coefficients].reverse(),
      r2: round(determinationCoefficient(data, points), options.precision),
    };
  },
};

function createWrapper() {
  const reduce = (accumulator, name) => ({
    _round: round,
    ...accumulator,
    [name](data, supplied) {
      return methods[name](data, {
        ...DEFAULT_OPTIONS,
        ...supplied,
      });
    },
  });

  return Object.keys(methods).reduce(reduce, {});
}

module.exports = createWrapper();


/**
 * @fileoverview Utility functions to help efficiently explore the Abstract Syntax Tree
 * @author Raghav Dua <duaraghav8@gmail.com>
 */

"use strict";

let Ajv = require("ajv"),
    util = require("util"),
    astNodeSchema = require("../../config/schemas/ast-node");

let nodeSchemaValidator = new Ajv({ allErrors: true }), sourceCodeText = "";

// For internal use. Throws if is passed an invalid AST node, else does nothing.
function throwIfInvalidNode(node, functionName) {
    if (!exports.isASTNode(node)) {
        throw new Error(functionName + "(): " + util.inspect(node) + " is not a valid AST node.");
    }
}

/**
 * Initialization method - provide all the necessary information astUtils functions could require in order to work
 * @param {String} sourceCode The source code being linted
 */
exports.init = function(sourceCode) {
    sourceCodeText = sourceCode;
};

/**
 * Check if given argument is an AST Node
 * @param {Object} possibleObject Argument to check for validity
 * @returns {Boolean} isAValidASTNode true if given argument is an AST node, false otherwise
 */
exports.isASTNode = nodeSchemaValidator.compile(astNodeSchema);

/**
 * @param {Object} node The node to check
 * @param {String} name The type of the node
 * @returns {Boolean} true if the given node is the right type
 */
function isNodeType(node, name) {
    throwIfInvalidNode(node, "isNodeType");
    return node["type"] === name;
}

/**
  * @param {Object} node The node to check
  * @returns {Boolean} true if the given node is an BlockStatement
  */
exports.isBlockStatement = function(node) {
    return isNodeType(node, "BlockStatement");
};

/**
 * @param {Object} node The node to check
 * @returns {Boolean} true if the given node is an BreakStatement
 */
exports.isBreakStatement = function(node) {
    return isNodeType(node, "BreakStatement");
};

/**
 * @param {Object} node The node to check
 * @returns {Boolean} true if the given node is an ExpressionStatement
 */
exports.isExpression = function(node) {
    return isNodeType(node, "ExpressionStatement");
};

/**
 * @param {Object} node The node to check
 * @returns {Boolean} true if the given node is an AssignmentStatement
 */
exports.isAssignment = function(node) {
    return isNodeType(node, "AssignmentExpression");
};

/**
 * @param {Object} node The node to check
 * @returns {Boolean} true if the given node is an UpdateExpression
 */
exports.isUpdate = function(node) {
    return isNodeType(node, "UpdateExpression");
};

/**
 * @param {Object} node The node to check
 * @returns {Boolean} true if the given node is an MemberExpression
 */
exports.isMember = function(node) {
    return isNodeType(node, "MemberExpression");
};

/**
 * @param {Object} node The node to check
 * @returns {Boolean} true if the given node is an IfStatement
 */
exports.isIfStatement = function(node) {
    return isNodeType(node, "IfStatement");
};

/**
 * @param {Object} node The node to check
 * @returns {Boolean} true if the given node is a type of loop statement
 */
exports.isLoopStatement = function(node) {
    return ["ForStatement", "WhileStatement", "DoWhileStatement"].indexOf(node["type"]) >= 0;
};

/**
 * Get the parent node of the specified node
 * @param {Object} node The AST Node to retrieve the parent of
 * @returns {Object} nodeParent Parent node of the given node
 */
exports.getParent = function(node) {
    throwIfInvalidNode(node, "getParent");
    return node.parent;
};

/**
 * Retrieve the line number on which the code for provided node STARTS
 * @param {Object} node The AST Node to retrieve the line number of
 * @returns {Integer} lineNumber Line number of code of the specified node. (LINES BEGIN FROM 1)
 */
exports.getLine = function(node) {
    throwIfInvalidNode(node, "getLine");

    let newLineCharsBefore = sourceCodeText
        .slice(0, node.start)
        .match(/\n/g);

    return (
        (newLineCharsBefore ? newLineCharsBefore.length : 0) + 1
    );
};

/**
 * Retrieve the column number of the first character of the given node
 * @param {Object} node The AST Node to retrieve the column number of
 * @returns {Integer} columnNumber Column number of code of the specified node (COLUMNS BEGIN FROM 0)
 */
exports.getColumn = function(node) {
    throwIfInvalidNode(node, "getColumn");

    //start looking from sourceCodeText [node.start] and stop upon encountering the first linebreak character
    for (let i = node.start; i >= 0; i--) {
        if (sourceCodeText [i] === "\n") {
            return node.start - i - 1;
        }
    }

    return node.start;
};

/**
 * Retrieve the line number on which the code for provided node ENDS
 * @param {Object} node The AST Node to retrieve the line number of
 * @returns {int} lineNumber Line number of code ending of the specified node. (LINES BEGIN FROM 1)
 */
exports.getEndingLine = function(node) {
    throwIfInvalidNode(node, "getEndingLine");

    let newLineCharsBefore = sourceCodeText
        .slice(0, node.end)
        .match(/\n/g);

    return (
        (newLineCharsBefore ? newLineCharsBefore.length : 0) + 1
    );
};

/**
 * Retrieve the column number of the last character that is part of the given node
 * @param {Object} node The AST Node to retrieve the ending column number of
 * @returns {Integer} columnNumber Column number of last char of the specified node (COLUMNS BEGIN FROM 0)
 */
exports.getEndingColumn = function(node) {
    throwIfInvalidNode(node, "getEndingColumn");

    //start looking from 1 character before node.start and stop upon encountering the first linebreak character
    for (let i = node.end - 1; i >= 0; i--) {
        if (sourceCodeText [i] === "\n") {
            return node.end - i - 2;
        }
    }

    return node.end - 1;
};

/**
 * Determine whether a given node is a child of another given node
 * @param {Object} potentialChild AST Node to be tested for child
 * @param {Object} potentialParent AST Node to be tested for parent
 * @returns {Bool} true if potentialChild is indeed a child of potentialParent, false otherwise
 */
exports.isAChildOf = (potentialChild, potentialParent) => {
    throwIfInvalidNode(potentialChild, "isAChildOf");
    throwIfInvalidNode(potentialParent, "isAChildOf");

    // Note that if the start or end pos of both nodes is same,
    // then they're considered to be the same node and DON'T have parent-child relationship.
    return potentialChild.start > potentialParent.start && potentialChild.end < potentialParent.end;
};


/**
 * @fileoverview Use emit statement to trigger a solidity event
 * @author Raghav Dua <duaraghav8@gmail.com>
 */

"use strict";

/**
 * While entering nodes, register all events and all call expressions that are not part of emit statement
 * While exiting, determine which of the registered call expressions is an event trigger and flag them
 * NOTE: This rule currently doesn't flag issues where event declaration & non-"emit"ted event trigger reside in
 * different contracts which are linked through inheritance.
 *
 * TODO: Enhance this rule to also detect event declarations inherited from other contracts
 * and whether they're being triggered using "emit".
 */
function create(context) {
    const events = [], callExpressions = [], sourceCode = context.getSourceCode();

    // Determines whether the given call name refers to an event declaration using scope resolution.
    function isEvent(expr, eventDeclarations) {
        for (let { node, enclosingContract } of eventDeclarations) {
            if (expr.callee.name === node.name && sourceCode.isAChildOf(expr, enclosingContract)) {
                return true;
            }
        }
        return false;
    }

    // Stores each declared event in the file and its corresponding parent contract
    function registerEventName(emitted) {
        const { node } = emitted;
        (!emitted.exit) && events.push({ node, enclosingContract: sourceCode.getParent(node) });
    }

    function registerNonEmittedCallExpression(emitted) {
        const { node } = emitted;

        if (!emitted.exit && sourceCode.getParent(node).type !== "EmitStatement") {
            callExpressions.push(node);
        }
    }

    function reportBadEventTriggers(emitted) {
        if (!emitted.exit) {
            return;
        }

        callExpressions.forEach(node => {
            isEvent(node, events) && context.report({
                node,
                fix(fixer) {
                    return fixer.insertTextBefore(node, "emit ");
                },
                message: "Use emit statements for triggering events."
            });
        });
    }

    return {
        EventDeclaration: registerEventName,
        CallExpression: registerNonEmittedCallExpression,
        Program: reportBadEventTriggers
    };
}

module.exports = {

    meta: {
        docs: {
            recommended: true,
            type: "warning",
            description: "Use emit statement to trigger a solidity event"
        },
        schema: [],
        fixable: "code"
    },

    create

};


/**
 * @fileoverview Flag all the variables that were declared but never used
 * @author Raghav Dua <duaraghav8@gmail.com>
 */

"use strict";

module.exports = {

    meta: {

        docs: {
            recommended: true,
            type: "error",
            description: "Flag all the variables that were declared but never used"
        },

        schema: []

    },

    create: function(context) {

        let allVariableDeclarations = {};

        //collect all variable declarations from VariableDeclarators and DeclarativeExpressions
        function inspectVariableDeclarator(emitted) {
            let node = emitted.node;

            if (!emitted.exit) {
                allVariableDeclarations [node.id.name] = node;
            }
        }

        function inspectDeclarativeExpression(emitted) {
            let node = emitted.node;

            //do not examine if the declaration is part of a Struct definition
            if (!emitted.exit && node.parent.type !== "StructDeclaration") {
                allVariableDeclarations [node.name] = node;
            }
        }

        //While exiting Progam Node, all the vars that haven't been used still exist inside VariableDeclarations. Report them
        function inspectProgram(emitted) {

            if (emitted.exit) {
                Object.keys(allVariableDeclarations).forEach(function(name) {
                    context.report({
                        node: allVariableDeclarations [name],
                        message: "Variable '" + name + "' is declared but never used."
                    });
                });
            }

        }

        //As soon as the first use of a variable is encountered, delete that variable's node from allVariableDeclarations
        function inspectIdentifier(emitted) {
            if (!emitted.exit) {
                let node = emitted.node,
                    sourceCode = context.getSourceCode();

                if (
                    allVariableDeclarations [node.name] &&
					sourceCode.getParent(node).type !== "VariableDeclarator"
                ) {
                    delete allVariableDeclarations [node.name];
                }
            }
        }

        return {
            Identifier: inspectIdentifier,
            Program: inspectProgram,
            DeclarativeExpression: inspectDeclarativeExpression,
            VariableDeclarator: inspectVariableDeclarator
        };

    }

};


/**
 * @fileoverview Ensure that functions in a contract are ordered according to their visibility
 * @author Raghav Dua <duaraghav8@gmail.com>
 */

"use strict";

const functionOrder = ["constructor", "fallback", "external", "public", "internal", "private"],
    errorMessage = `Functions should be in order: ${functionOrder.join(", ")}`;

function isConstructor(funcNode, contractNode) {
    // Account for both ways of defining a contract constructor
    // 1. The old way (function with same name as contract)
    // 2. New way ("constructor() {...}")
    return (funcNode.name === contractNode.name || funcNode.type === "ConstructorDeclaration");
}

function isFallback(funcNode) {
    return funcNode.name === null;
}

function findFuncPosInOrder(contractNode, funcNode) {
    if (isConstructor(funcNode, contractNode)) {
        return functionOrder.indexOf("constructor");
    }

    if (isFallback(funcNode)) {
        return functionOrder.indexOf("fallback");
    }

    // Default visibility of a function is public.
    if (funcNode.modifiers === null) {
        return functionOrder.indexOf("public");
    }

    // If we bypass all above cases, below logic is guranteed to return a valid position
    const modifNames = funcNode.modifiers.map(m => { return m.name; });

    for (let mName of modifNames) {
        const i = functionOrder.indexOf(mName);

        if (i > -1) {
            return i;
        }
    }

    // If we bypass above loop, it means modifiers exist for this func,
    // but none of them was a visibility modif. This is equivalent to public vis. modif.
    return functionOrder.indexOf("public");
}

function isFunctionVisibility(contractNode, funcNode, typeDescriptor) {
    const funcPosInOrder = findFuncPosInOrder(contractNode, funcNode);
    return typeDescriptor === functionOrder[funcPosInOrder];
}

function isIgnored(funcNode, contractNode, ignoreCriteria) {
    if (ignoreCriteria.constructorFunc && isConstructor(funcNode, contractNode)) {
        return true;
    }
    if (ignoreCriteria.fallbackFunc && isFallback(funcNode)) {
        return true;
    }
    if (ignoreCriteria.functions && ignoreCriteria.functions.includes(funcNode.name)) {
        return true;
    }

    for (let vis of (ignoreCriteria.visibilities || [])) {
        if (isFunctionVisibility(contractNode, funcNode, vis)) {
            return true;
        }
    }

    return false;
}

module.exports = {

    meta: {

        docs: {
            recommended: true,
            type: "warning",
            description: "Ensure that functions in a contract are ordered according to their visibility"
        },

        schema: [{
            type: "object",
            properties: {
                ignore: {
                    type: "object",
                    anyOf: [
                        { required: ["constructorFunc"] },
                        { required: ["fallbackFunc"] },
                        { required: ["functions"] },
                        { required: ["visibilities"] }
                    ],
                    properties: {
                        constructorFunc: { type: "boolean" },
                        fallbackFunc: { type: "boolean" },
                        functions: {
                            type: "array",
                            items: { type: "string" }
                        },
                        visibilities: {
                            type: "array",
                            items: { type: "string" }
                        }
                    },
                    additionalProperties: false
                }
            },
            required: ["ignore"],
            additionalProperties: false
        }]

    },

    create(context) {
        /**
         * Set cursor to point to first visibility in order of vis. array.
         * For each function Fi inside contract, if Fi's vis is same as that pointed by cursor, ignore func.
         * If not, check the pos in the order of Fi's vis. If ahead, simply move cursor forward to Fi's vis.'s
         * position in array. If behind, report func.
         */
        function inspectFunctionsOfContract(emitted) {
            if (emitted.exit) {
                return;
            }

            const { node } = emitted, { body } = node;
            let cursor = 0;

            // Filter out non-function nodes
            body.filter(child => {
                return ["FunctionDeclaration", "ConstructorDeclaration"].includes(child.type);
            }).forEach(funcNode => {
                // Return if the function is ignored or in the correct order.
                if (
                    (context.options && isIgnored(funcNode, node, context.options[0].ignore)) ||
                    isFunctionVisibility(node, funcNode, functionOrder[cursor])
                ) {
                    return;
                }

                const funcPosInOrder = findFuncPosInOrder(node, funcNode);

                if (funcPosInOrder > cursor) {
                    cursor = funcPosInOrder;
                    return;
                }

                context.report({ node: funcNode, message: errorMessage });
            });
        }

        return {
            ContractStatement: inspectFunctionsOfContract
        };
    }

};


/**
 * @fileoverview Ensure function calls and declaration have (or don't have) whitespace in appropriate locations
 * @author Raghav Dua <duaraghav8@gmail.com>
 */

"use strict";

module.exports = {

    meta: {

        docs: {
            recommended: true,
            type: "error",
            description: "Ensure function calls and declaration have (or don't have) whitespace in appropriate locations"
        },

        schema: []

    },

    create: function(context) {

        let sourceCode = context.getSourceCode();

        //If parameters are specified, ensure appropriate spacing surrounding commas
        function inspectFunctionDeclaration(emitted) {
            let node = emitted.node;

            if (emitted.exit) {
                return;
            }

            let params = node.params;

            if (params && params.length > 1) {
                params.slice(0, -1).forEach(function(arg) {
                    sourceCode.getNextChar(arg) !== "," && context.report({
                        node: arg,
                        location: {
                            column: sourceCode.getEndingColumn(arg) + 1
                        },
                        message: "All arguments (except the last one) must be immediately followed by a comma."
                    });
                });
            }
        }

        // The InformalParameter nodes (params) in modifier declaration should follow the same spacing rules as function declarations
        function inspectModifierDeclaration(emitted) {
            let node = emitted.node;

            if (emitted.exit) {
                return;
            }

            //If parameters are specified, ensure appropriate spacing surrounding commas
            let params = node.params;

            if (params && params.length > 1) {
                params.slice(0, -1).forEach(function(arg) {
                    sourceCode.getNextChar(arg) !== "," && context.report({
                        node: arg,
                        location: {
                            column: sourceCode.getEndingColumn(arg) + 1
                        },
                        message: "All arguments (except the last one) must be immediately followed by a comma."
                    });
                });
            }
        }

        //same could potentially be applied to FunctionDeclaration
        function inspectCallExpression(emitted) {
            let node = emitted.node,
                callArgs = node.arguments;

            if (emitted.exit) {
                return;
            }

            let nodeCode = sourceCode.getText(node);

            //for a 0-argument call, ensure that name is followed by '()'
            if (!callArgs.length) {
                for (let i = nodeCode.length; i > 0; i--) {
                    if (nodeCode [i] === ")" && nodeCode [i-1] === "(") {
                        return;
                    }
                    if (/[\s\(\)]/.test(nodeCode [i])) {
                        break;
                    }
                }

                return context.report({
                    node: node,
                    message: "\"" + nodeCode + "\": " +
					"A call without arguments should have brackets without any whitespace between them, like 'functionName ()'."
                });
            }

            let lastCallArg = callArgs.slice(-1) [0];

            //if call spans over multiple lines (due to too many arguments), below rules don't apply
            if (sourceCode.getLine(node) !== sourceCode.getEndingLine(lastCallArg)) {
                return;
            }

            let charBeforeFirstArg = sourceCode.getPrevChar(callArgs [0]),
                charAfterLastCallArg = sourceCode.getNextChar(lastCallArg);

            (callArgs [0].type !== "NameValueAssignment" && charBeforeFirstArg !== "(") && context.report({
                node: callArgs [0],
                location: {
                    column: sourceCode.getColumn(callArgs [0]) - 1
                },
                message: "'" + node.callee.name + "': The first argument must not be preceded by any whitespace or comments (only '(')."
            });

            (lastCallArg.type !== "NameValueAssignment" && charAfterLastCallArg !== ")") && context.report({
                node: callArgs [0],
                location: {
                    column: sourceCode.getEndingColumn(lastCallArg) + 1
                },
                message: "'" + node.callee.name + "': The last argument must not be succeeded by any whitespace or comments (only ')')."
            });
        }

        return {
            CallExpression: inspectCallExpression,
            ModifierDeclaration: inspectModifierDeclaration,
            FunctionDeclaration: inspectFunctionDeclaration,
            ConstructorDeclaration: inspectFunctionDeclaration
        };

    }

};


/**
 * @fileoverview Ensure a) A PRAGMA directive exists and b) its on top of the file
 * @author Raghav Dua <duaraghav8@gmail.com>
 */

"use strict";

const { EOL } = require("os");

module.exports = {

    meta: {

        docs: {
            recommended: true,
            type: "warning",
            description: "Ensure a) A PRAGMA directive exists and b) its on top of the file"
        },

        schema: [],

        fixable: "code"

    },

    create: function(context) {

        let missingNodeOnTopErrorReported = false;

        /**
		 * This executes only when we're leaving the Program node. At that time,
		 * we check whether the "missing pragma on top" error has already been reported or not.
		 * If not, we proceed to report it here. This happens when there are no pragma statements in
		 * the entire file. If there is one (but not on top of file), it gets reported by inspectPragmaStatement().
		 * NOTE: A Pragma dir must exist at absolute top, even before pragma experimental.
		 */
        function inspectProgram(emitted) {
            let node = emitted.node, body = node.body;

            if (!emitted.exit || missingNodeOnTopErrorReported) {
                return;
            }

            (body.length > 0) && (body [0].type !== "PragmaStatement") && context.report({
                node: node,
                message: "No Pragma directive found at the top of file."
            });
        }

        function inspectPragmaStatement(emitted) {
            let node = emitted.node,
                sourceCode = context.getSourceCode(), pragmaParent = sourceCode.getParent(node);

            // If pragma statement is on top, exit now. No further checks required.
            if (emitted.exit || node.start === pragmaParent.body [0].start) {
                return;
            }

            const pragmaCode = sourceCode.getText(node);

            context.report({
                node: node,
                message: `"${pragmaCode}" should be at the top of the file.`,
                fix: function(fixer) {
                    return [fixer.remove(node),
                        fixer.insertTextBefore(pragmaParent.body [0], `${pragmaCode}${EOL}`)];
                }
            });

            missingNodeOnTopErrorReported = true;
        }

        // Experimental pragmas, if they exist, must be above everything EXCEPT pragma solidity & other experimental pragmas.
        function inspectExperimentalPragmaStatement(emitted) {
            if (emitted.exit) {
                return;
            }

            const { node } = emitted,
                nodesAllowedAbove = ["ExperimentalPragmaStatement", "PragmaStatement"],
                programNode = context.getSourceCode().getParent(node);

            for (let childNode of programNode.body) {
                // If we've reached this exp. pragma while traversing body, it means its position is fine.
                if (node.start === childNode.start) {
                    return;
                }

                // We found the first node not allowed above experimental pragma, report and exit.
                const pragmaCode = context.getSourceCode().getText(node);

                if (nodesAllowedAbove.indexOf(childNode.type) < 0) {
                    const errObject = {
                        node,
                        fix(fixer) {
                            return [fixer.remove(node),
                                fixer.insertTextBefore(childNode, `${pragmaCode}${EOL}`)];
                        },
                        message: "Experimental Pragma must precede everything except Solidity Pragma."
                    };

                    return context.report(errObject);
                }
            }
        }

        return {
            Program: inspectProgram,
            PragmaStatement: inspectPragmaStatement,
            ExperimentalPragmaStatement: inspectExperimentalPragmaStatement
        };

    }

};


/**
 * @fileoverview Apply fixes described by the received fix packets to the given source code and return the 'fixed' code.
 * @author Raghav Dua <duaraghav8@gmail.com>
 */

"use strict";

let mergeFixes = require("./merge-fixer-packets");

function compareMessagesByLocation(a, b) {
    return (a.line - b.line) || (a.column - b.column);
}

function compareMessagesByFixRange(a, b) {
    return (a.fix.range [0] - b.fix.range [0]) || (a.fix.range [1] - b.fix.range [1]);
}

module.exports = {

    /**
     * Apply fixes to source code depending on whichever errors can be fixed.
     * @param {String} sourceCode Code to fix
     * @param {Array} errorMessages Error objects that describe the error and possibly how to fix it.
     * @returns {Object} fixed Contains fixed code and information about fixes applied & remaining un-fixed errors.
     */
    applyFixes: function(sourceCode, errorMessages) {
        let fixedSourceCode = "", fixes = [], fixesApplied = [], remainingMessages = [];
        let cursor = Number.NEGATIVE_INFINITY;

        function attemptFix(fix) {
            let start = fix.range [0], end = fix.range [1];

            // If this fix overlaps with the previous one or has negaive range, return.
            // Note that when cursor === start, its NOT an overlap since when source code in range
            // [i, j] is being edited, the code covered is actually from i to j-1.
            if (cursor > start || start > end) {
                return false;
            }

            fixedSourceCode += sourceCode.slice(Math.max(0, cursor), Math.max(0, start));
            fixedSourceCode += fix.text;
            cursor = end;
            return true;
        }

        // Segregate errors that can be fixed from those that can't for sure.
        errorMessages.forEach(function(msg) {
            if (msg.fix) {
                // If msg.fix is an Array of fix packets, merge them into a single fix packet.
                try {
                    msg.fix = mergeFixes(msg.fix, sourceCode);
                } catch (e) {
                    throw new Error("An error occured while applying fix of rule \""
						+ msg.ruleName + "\" for error \"" + msg.message + "\": " + e.message);
                }

                return fixes.push(msg);
            }

            remainingMessages.push(msg);
        });

        // Fixes will be applied in top-down approach. The fix that arrives first (line-wise, followed by column-wise)
        // gets applied first. But if current fix is applied successfully & the next one overlaps the current one,
        // then the next one is simply skipped. Hence, it is NOT guranteed that all fixes will be applied.
        fixes.sort(compareMessagesByFixRange).forEach(function(msg) {
            if (attemptFix(msg.fix)) {
                return fixesApplied.push(msg);
            }

            remainingMessages.push(msg);
        });

        fixedSourceCode += sourceCode.slice(Math.max(0, cursor));
        remainingMessages.sort(compareMessagesByLocation);

        return {
            fixesApplied: fixesApplied,
            fixedSourceCode: fixedSourceCode,
            remainingErrorMessages: remainingMessages
        };
    }

};


/**
 * @fileoverview Enforce standardized indentation style
 * @author Raghav Dua <duaraghav8@gmail.com>
 */

"use strict";

// This func will receive either empty str (len = 0), str with N Spaces (N >= 1) or N Tabs (N >= 1)
function getIndentDescription(indentStyle, level) {
    // If either user has specified 0 indent or we're at level 0 (start), totalIndent becomes 0.
    const totalIndent = indentStyle.length * level, s = totalIndent > 1 ? "s" : "";

    // If style is that there should be no indent for any level OR we're at base level
    if (totalIndent === 0) {
        return "0 whitespace";
    }

    if (indentStyle [0] === " ") {
        return `${totalIndent} space${s}`;
    }

    // If above 2 are bypassed, indent style must be tab(s)
    return `${totalIndent} tab${s}`;
}

function getIndentString(indentStyle, level) {
    return Array(level + 1).join(indentStyle);
}

module.exports = {

    meta: {

        docs: {
            recommended: true,
            type: "warning",
            description: "Ensure consistent indentation per level (4 spaces by default)"
        },

        schema: [{
            oneOf: [
                { type: "string", enum: ["tab"] },
                { type: "integer", minimum: 0 }
            ]
        }]

    },

    create: function(context) {

        // default configurations
        let BASE_INDENTATION_STYLE = " ".repeat(4),
            BASE_INDENTATION_STYLE_DESC = "4 spaces";

        if (context.options) {
            if (context.options [0] === "tab") {
                BASE_INDENTATION_STYLE = "\t";
                BASE_INDENTATION_STYLE_DESC = "1 tab";
            } else {
                let spCount = context.options [0];

                BASE_INDENTATION_STYLE = " ".repeat(spCount);
                BASE_INDENTATION_STYLE_DESC = spCount + " space" + (spCount === 1 ? "" : "s");
            }
        }

        let BASE_INDENTATION_STYLE_REGEXP_GLOBAL = new RegExp(BASE_INDENTATION_STYLE, "g");

        let sourceCode = context.getSourceCode();
        let topLevelDeclarations = ["ContractStatement", "LibraryStatement"];

        //Ensure NO indentation exists before top-level declarations (contract, library)
        function inspectProgram(emitted) {
            let node = emitted.node;

            if (emitted.exit) {
                return;
            }

            function inspectProgramChild(programChild) {
                //if node's code starts at index 0, getPrevChar() returns null (meaning no errors), so change it to '\n'
                let prevChar = sourceCode.getPrevChar(programChild) || "\n",
                    childEndingLine = sourceCode.getEndingLine(programChild),
                    childEndingLineText = sourceCode.getTextOnLine(childEndingLine);

                function report(messageText) {
                    context.report({
                        node: programChild,
                        message: (
                            programChild.type.replace("Statement", "").toLowerCase() +
							( programChild.name ? (" '" + programChild.name + "'") : " statement" ) +
							": " + messageText
                        )
                    });
                }

                if (prevChar !== "\n") {
                    //either indentation exists, or some other character - both are not allowed
                    if (/\s/.test(prevChar)) {
                        report("There should be no indentation before top-level declaration.");
                    } else {
                        report("There should be no character(s) before top-level declaration.");
                    }
                }

                //if node starts and ends on different lines and its last line starts with a whitespace or multiline/natspec comment, report
                if (
                    sourceCode.getLine(programChild) !== childEndingLine &&
					/^(\s+)|(\/\*[^*\/]*\*\/)/.test(childEndingLineText)
                ) {
                    context.report({
                        node: programChild,
                        location: {
                            line: childEndingLine,
                            column: 0
                        },
                        message: "Line shouln't have any indentation or comments at the beginning."
                    });
                }

            }

            node.body.forEach(inspectProgramChild);
        }

        //Ensure level 1 indentation before all immediate children of top-level declarations
        function inspectTopLevelDeclaration(emitted) {
            let body = emitted.node.body || [],
                levelOneIndentRegExp = new RegExp("^\\n" + BASE_INDENTATION_STYLE + "$"),
                endingLineRegExp = new RegExp("^" + BASE_INDENTATION_STYLE + "(\\S| \\*)$"),	//either a non-whitespace character or 1 extra whitespace followed by * (closing of multi-line comment)
                endingLineExtraIndentRegExp = new RegExp("^" + BASE_INDENTATION_STYLE.repeat(2) + "(\\S| \\*)$");

            if (emitted.exit) {
                return;
            }

            function inspectChild(child) {
                let prevChars = sourceCode.getPrevChars(child, BASE_INDENTATION_STYLE.length+1),
                    endingLineNum = sourceCode.getEndingLine(child);

                //if the start of node doesn't follow correct indentation
                if (!levelOneIndentRegExp.test(prevChars)) {
                    context.report({
                        node: child,
                        message: `Only use indent of ${BASE_INDENTATION_STYLE_DESC}.`
                    });
                }

                // If the node starts & ends on same line, exit.
                if (sourceCode.getLine(child) === endingLineNum) {
                    return;
                }

                // If node starts & ends on diff lines, the ending line must also follow correct indentation.
                // Exception to this is an abstract function whose declaration spans over multiple lines. Eg-
                // function foo()
                //     payable
                //     returns (uint, string);

                if (child.type === "FunctionDeclaration" && child.is_abstract) {

                    if (!endingLineExtraIndentRegExp.test(
                        sourceCode.getTextOnLine(endingLineNum).slice(0, BASE_INDENTATION_STYLE.repeat(2).length+1))) {
                        context.report({
                            node: child,
                            location: {
                                line: endingLineNum,
                                column: 0
                            },
                            message: `Only use indent of ${BASE_INDENTATION_STYLE_DESC}.`
                        });
                    }

                    return;
                }

                if (!endingLineRegExp.test(
                    sourceCode.getTextOnLine(endingLineNum).slice(0, BASE_INDENTATION_STYLE.length+1))) {
                    context.report({
                        node: child,
                        location: {
                            line: endingLineNum,
                            column: 0
                        },
                        message: `Only use indent of ${BASE_INDENTATION_STYLE_DESC}.`
                    });
                }
            }

            body.forEach(inspectChild);
        }

        //Ensure 1 extra indentation inside Block than before it
        function inspectBlockStatement(emitted) {
            let node = emitted.node;

            //if the complete block resides on the same line, no need to check for indentation
            if (emitted.exit || (sourceCode.getLine(node) === sourceCode.getEndingLine(node))) {
                return;
            }

            let parent = sourceCode.getParent(node),
                parentDeclarationLine = sourceCode.getLine(parent),
                parentDeclarationLineText = sourceCode.getTextOnLine(parentDeclarationLine),
                currentIndent, currentIndentLevel;

            function inspectBlockItem(blockIndent, blockIndentDesc, blockItem) {
                let prevChars = sourceCode.getPrevChars(blockItem, blockIndent.length+1),
                    endingLineNum = sourceCode.getEndingLine(blockItem),
                    endingLineRegExp = new RegExp("^" + blockIndent + "(" + BASE_INDENTATION_STYLE + ")?\\S.*$");

                if (prevChars !== ("\n" + blockIndent)) {
                    context.report({
                        node: blockItem,
                        message: `Only use indent of ${blockIndentDesc}.`
                    });
                }

                /**
				 * If the block item spans over multiple lines, make sure the ending line also follows the indent rule
				 * An exception to this is the if-else statements when they don't have BlockStatement as their body
				 * eg-
				 * if (a)
				 *     foo();
				 * else
				 *     bar();
				 *
				 * Another exception is chaining.
				 * eg-
				 * function() {
				 *   myObject
				 *     .funcA()
				 *     .funcB()
				 *     [0];
				 * }
				 * Ending line has 1 extra indentation but this is acceptable.
				 */
                if (
                    blockItem.type !== "IfStatement" &&
					sourceCode.getLine(blockItem) !== endingLineNum &&
					!endingLineRegExp.test(sourceCode.getTextOnLine(endingLineNum))
                ) {
                    context.report({
                        node: blockItem,
                        location: {
                            line: endingLineNum,
                            column: 0
                        },
                        message: `Only use indent of ${blockIndentDesc}.`
                    });
                }
            }

            currentIndent = parentDeclarationLineText.slice(
                0,
                parentDeclarationLineText.indexOf(parentDeclarationLineText.trim())
            );

            //in case of no match, match() returns null. Return [] instead to avoid crash
            currentIndentLevel = (currentIndent.match(BASE_INDENTATION_STYLE_REGEXP_GLOBAL) || []).length;

            //ensure that there is only whitespace of correct level before the block's parent's code
            if (getIndentString(BASE_INDENTATION_STYLE, currentIndentLevel) !== currentIndent) {
                return;	//exit now, we can' proceed further unless this is fixed
            }

            //indentation of items inside block should be 1 level greater than that of parent
            const blockIndent = getIndentString(BASE_INDENTATION_STYLE, currentIndentLevel + 1);
            const blockIndentDesc = getIndentDescription(BASE_INDENTATION_STYLE, currentIndentLevel + 1);

            node.body.forEach(inspectBlockItem.bind(null, blockIndent, blockIndentDesc));
        }

        function inspectStructDeclaration(emitted) {
            let node = emitted.node,
                body = node.body || [],
                endingLineNum = sourceCode.getEndingLine(node);
            let structDeclarationLineText, currentIndent, currentIndentLevel;

            function inspectStructAttr(structIndent, structIndentDesc, attr) {
                let indentRegExp = new RegExp("^" + structIndent + "[^\\s(\/\*)]"),
                    attrLineText = sourceCode.getTextOnLine(sourceCode.getLine(attr));

                //attribute declaration must be preceded by only correct level of indentation & no comments
                !indentRegExp.test(attrLineText) && context.report({
                    node: attr,
                    message: `Only use indent of ${structIndentDesc}.`
                });
            }

            // No need to lint further if entire struct declaration is on single line
            if (emitted.exit || sourceCode.getLine(node) === endingLineNum) {
                return;
            }

            structDeclarationLineText = sourceCode.getTextOnLine(sourceCode.getLine(node));

            currentIndent = structDeclarationLineText.slice(
                0,
                structDeclarationLineText.indexOf(structDeclarationLineText.trim())
            );

            currentIndentLevel = (currentIndent.match(BASE_INDENTATION_STYLE_REGEXP_GLOBAL) || []).length;

            //ensure that there is only whitespace of correct level on the line containing struct declaration
            if (getIndentString(BASE_INDENTATION_STYLE, currentIndentLevel) !== currentIndent) {
                return;	//exit now, we can' proceed further unless this is fixed
            }

            const structIndent = getIndentString(BASE_INDENTATION_STYLE, currentIndentLevel + 1);
            const structIndentDesc = getIndentDescription(BASE_INDENTATION_STYLE, currentIndentLevel + 1);

            body.forEach(inspectStructAttr.bind(null, structIndent, structIndentDesc));

        }

        function inspectArrayExpression(emitted) {
            let node = emitted.node, elements = node.elements;
            let endingLineNum = sourceCode.getEndingLine(node),
                arrayExpressionLineText, currentIndent, currentIndentLevel;

            function inspectElement(arrayIndent, arrayIndentDesc, elem) {
                let indentRegExp = new RegExp("^" + arrayIndent + "[^\\s(\/\*)]"),
                    elemLineText = sourceCode.getTextOnLine(sourceCode.getLine(elem));

                //element declaration must be preceded by only correct level of indentation & no comments
                !indentRegExp.test(elemLineText) && context.report({
                    node: elem,
                    message: `Only use indent of ${arrayIndentDesc}.`
                });
            }

            // No need to lint further if entire arary declaration is on single line
            if (emitted.exit || sourceCode.getLine(node) === endingLineNum) {
                return;
            }

            arrayExpressionLineText = sourceCode.getTextOnLine(sourceCode.getLine(node));

            currentIndent = arrayExpressionLineText.slice(
                0,
                arrayExpressionLineText.indexOf(arrayExpressionLineText.trim())
            );

            currentIndentLevel = (currentIndent.match(BASE_INDENTATION_STYLE_REGEXP_GLOBAL) || []).length;

            //ensure that there is only whitespace of correct level on the line containing array expression
            if (getIndentString(BASE_INDENTATION_STYLE, currentIndentLevel) !== currentIndent) {
                return;	//exit now, we can' proceed further unless this is fixed
            }

            const arrayIndent = getIndentString(BASE_INDENTATION_STYLE, currentIndentLevel + 1);
            const arrayIndentDesc = getIndentDescription(BASE_INDENTATION_STYLE, currentIndentLevel + 1);

            elements.forEach(inspectElement.bind(null, arrayIndent, arrayIndentDesc));
        }

        //function params (if on multiple lines)
        function inspectFunctionDeclaration(emitted) {
            let node = emitted.node, params = node.params || [];

            let startLine = sourceCode.getLine(node),
                lastArgLine = params.length ? sourceCode.getEndingLine(params.slice(-1) [0]) : startLine,
                functionDeclarationLineText, currentIndent, currentIndentLevel;

            function inspectParam(paramIndent, paramIndentDesc, param) {
                let indentRegExp = new RegExp("^" + paramIndent + "[^\\s(\/\*)]"),
                    paramLineText = sourceCode.getTextOnLine(sourceCode.getLine(param));

                //parameter declaration must be preceded by only correct level of indentation & no comments
                !indentRegExp.test(paramLineText) && context.report({
                    node: param,
                    message: `Only use indent of ${paramIndentDesc}.`
                });
            }

            // If declaration args start & end on same line, exit now
            if (emitted.exit || startLine === lastArgLine) {
                return;
            }

            functionDeclarationLineText = sourceCode.getTextOnLine(startLine);

            currentIndent = functionDeclarationLineText.slice(
                0,
                functionDeclarationLineText.indexOf(functionDeclarationLineText.trim())
            );

            currentIndentLevel = (currentIndent.match(BASE_INDENTATION_STYLE_REGEXP_GLOBAL) || []).length;

            //ensure that there is only whitespace of correct level on the line containing parameter
            if (getIndentString(BASE_INDENTATION_STYLE, currentIndentLevel) !== currentIndent) {
                return;	//exit now, we can' proceed further unless this is fixed
            }

            const paramIndent = getIndentString(BASE_INDENTATION_STYLE, currentIndentLevel + 1);
            const paramIndentDesc = getIndentDescription(BASE_INDENTATION_STYLE, currentIndentLevel + 1);

            params.forEach(inspectParam.bind(null, paramIndent, paramIndentDesc));
        }

        function inspectCallExpression(emitted) {

            let node = emitted.node;
            let endingLineNum = sourceCode.getEndingLine(node),
                callExpressionLineText, currentIndent, currentIndentLevel;

            function inspectArgument(argIndent, argIndentDesc, argument) {
                let indentRegExp = new RegExp(`^${argIndent}[^\\s\/]`),
                    argLineText = sourceCode.getTextOnLine(sourceCode.getLine(argument));

                //parameter declaration must be preceded by only correct level of indentation & no comments
                !indentRegExp.test(argLineText) && context.report({
                    node: argument,
                    message: `Only use indent of ${argIndentDesc}.`
                });
            }

            // Return if the call starts & ends on same line
            // Also return if the callee is NOT an identifier, else rule creates false positives
            // eg-
            // foo()
            //     .bar(10, "hello");
            //
            // baz(10, "hello");
            //
            // In first eg, rule will lint at args for indent because it thinks the call spans over multiple lines
            if (emitted.exit ||
				sourceCode.getLine(node) === endingLineNum || node.callee.type !== "Identifier") {
                return;
            }

            callExpressionLineText = sourceCode.getTextOnLine(sourceCode.getLine(node));

            currentIndent = callExpressionLineText.slice(
                0,
                callExpressionLineText.indexOf(callExpressionLineText.trim())
            );

            currentIndentLevel = (currentIndent.match(BASE_INDENTATION_STYLE_REGEXP_GLOBAL) || []).length;

            //ensure that there is only whitespace of correct level on the line containing parameter
            if (getIndentString(BASE_INDENTATION_STYLE, currentIndentLevel) !== currentIndent) {
                return;	//exit now, we can' proceed further unless this is fixed
            }

            const argIndent = getIndentString(BASE_INDENTATION_STYLE, currentIndentLevel + 1);
            const argIndentDesc = getIndentDescription(BASE_INDENTATION_STYLE, currentIndentLevel + 1);

            node.arguments.forEach(inspectArgument.bind(null, argIndent, argIndentDesc));

        }

        let response = {
            CallExpression: inspectCallExpression,
            FunctionDeclaration: inspectFunctionDeclaration,
            ArrayExpression: inspectArrayExpression,
            StructDeclaration: inspectStructDeclaration,
            BlockStatement: inspectBlockStatement,
            Program: inspectProgram
        };

        topLevelDeclarations.forEach(function(nodeName) {
            response [nodeName] = inspectTopLevelDeclaration;
        });

        return response;

    }

};


/**
 * @fileoverview Ensure that a line of code doesn't exceed the specified number of characters.
 * @author Leo Arias <yo@elopio.net>
 */

"use strict";

module.exports = {

    meta: {

        docs: {
            recommended: true,
            type: "warning",
            description: "Ensure that a line of code doesn't exceed the specified number of characters"
        },

        schema: [{
            type: "integer", minimum: 1
        }]
    },

    create(context) {
        const maxLineLength = context.options ? context.options [0] : 145;
        const sourceCode = context.getSourceCode();

        function inspectProgram(emitted) {
            if (emitted.exit) {
                return;
            }

            let lines = sourceCode.text.split(/\r?\n/),
                lastLine = -1;

            // Recursive function.
            // --elopio - 20180421
            function checkNodes(nodes) {
                if (!Array.isArray(nodes)) {
                    nodes = [nodes];
                }
                nodes.forEach(node => {
                    let lineNumber = sourceCode.getLine(node) - 1;

                    if (lineNumber > lastLine && lines[lineNumber].length > maxLineLength) {
                        context.report({
                            node,
                            message: `Line exceeds the limit of ${maxLineLength} characters`
                        });

                        lastLine = lineNumber;
                    }

                    checkNodes(node.body || []);
                });
            }

            checkNodes(emitted.node.body);
        }

        return {
            Program: inspectProgram
        };

    }
};


/**
 * @fileoverview Utility functions to help analyze the Source Code
 * @author Raghav Dua <duaraghav8@gmail.com>
 */

"use strict";

const astUtils = require("./ast-utils");
const INHERITABLE_METHODS = [
    "isASTNode",
    "getParent",
    "getColumn",
    "getEndingColumn",
    "getLine",
    "getEndingLine",
    "isAChildOf",
    "isBlockStatement",
    "isBreakStatement",
    "isExpression",
    "isAssignment",
    "isUpdate",
    "isMember",
    "isIfStatement",
    "isLoopStatement"
];

/**
 * SourceCode object constructor - provides the source code text along with functions to operate on the code easily
 * @param {String} sourceCodeText source code being linted
 * @param {ASTNode[]} comments List of objects representing comments throughout the source code
 */
function SourceCode(sourceCodeText, comments) {
    this.text = sourceCodeText;
    this.commentObjects = comments;
}

SourceCode.prototype = {

    constructor: SourceCode,

    /**
	 * Function to retrieve the source code text being linted. Returns the complete source code if no node specified
	 * @param {Object} node The AST node whose source code (specifically) is needed
	 * @param {int} beforeCount Include beforeCount number of characters prior to the node's code
	 * @param {int} afterCount Include afterCount number of characters following the node's code
	 * @returns {String} sourceCodeText source code of the specified node plus extra characters specified by beforeCount & afterCount
	 */
    getText: function(node, beforeCount, afterCount) {
        let sourceCodeText = this.text;

        if (node) {
            if (astUtils.isASTNode(node)) {

                return this.text.slice(
                    Math.max(0, node.start - (Math.abs(beforeCount) || 0)),
                    node.end + (Math.abs(afterCount) || 0)
                );

            }

            throw new Error("Invalid Node object");
        }

        return sourceCodeText;
    },

    getNextChar: function(node) {
        if (node && astUtils.isASTNode(node)) {
            //return null if node is the last node,i.e., no characters after this node's code
            return this.text [node.end] || null;
        }

        throw new Error("Invalid Node object");
    },

    getPrevChar: function(node) {
        if (node && astUtils.isASTNode(node)) {
            //return null if node.start is 0, i.e., node's code is the first thing in the source code text
            return this.text [node.start - 1] || null;
        }

        throw new Error("Invalid Node object");
    },

    getNextChars: function(node, charCount) {
        if (node && astUtils.isASTNode(node)) {
            return this.text.slice(
                node.end, node.end + (Math.abs(charCount) || 0)
            );
        }

        throw new Error("Invalid Node object");
    },

    getPrevChars: function(node, charCount) {
        if (node && astUtils.isASTNode(node)) {
            return this.text.slice(
                Math.max(0, node.start - (Math.abs(charCount) || 0)),
                node.start
            );
        }

        throw new Error("Invalid Node object");
    },

    /**
	 * Get the entire string between 2 nodes - ranging from prevNode.end to currentNode.start (exclusive)
	 * @param {ASTNode} prevNode The 1st AST Node
	 * @param {ASTNode} currentNode The 2nd AST Node
	 * @returns {String} charsBetweenNodes The entire string netween the 2 nodes
	 */
    getStringBetweenNodes: function(prevNode, currentNode) {
        if (
            prevNode && astUtils.isASTNode(prevNode) &&
			currentNode && astUtils.isASTNode(currentNode) &&
			prevNode.start <= currentNode.start
        ) {
            return this.text.slice(prevNode.end, currentNode.start);
        }

        throw new Error("Invalid argument for one or both nodes");
    },

    /**
     * Get the source code split into lines
     * @returns {String[]} Array in which each item is an individual line of the source code
     */
    getLines() {
        // Establish a cache the first time this function is called,
        // so subsequent calls don't have to perform computation
        if (!this.sourceCodeTextLines) {
            // important to account for both types of line endings
            // see issue https://github.com/duaraghav8/Ethlint/issues/173
            this.sourceCodeTextLines = this.text.split(/\r?\n/);
        }

        return this.sourceCodeTextLines;
    },

    /**
     * Get the complete text on line lineNumber (excluding the EOL)
     * @param {int} lineNumber Line number whose text to get
     * @returns {String} code Source code text on the specified line
     */
    getTextOnLine: function(lineNumber) {
        // Call getLines() just to ensure that this.sourceCodeTextLines property is set
        this.getLines();

        if (
            lineNumber && typeof lineNumber === "number" &&
			parseInt(lineNumber) === lineNumber &&	//ensure that argument is an INTEGER
			lineNumber >= 1 && lineNumber <= this.sourceCodeTextLines.length
        ) {
            return this.sourceCodeTextLines [lineNumber - 1];
        }

        throw new Error(`Line number ${lineNumber} invalid or out of bounds.`);
    },

    /**
     * Get all AST Nodes representing comments
     * @returns {ASTNode[]} List of comment nodes
     */
    getComments() {
        return this.commentObjects;
    }

};

INHERITABLE_METHODS.forEach(function(methodName) {
    SourceCode.prototype [methodName] = astUtils [methodName];
});

module.exports = SourceCode;


/**
 * @fileoverview Ensure that operators are surrounded by a single space on either side
 * @author Raghav Dua <duaraghav8@gmail.com>
 */

"use strict";

module.exports = {

    meta: {

        docs: {
            recommended: true,
            type: "error",
            description: "Ensure that operators are surrounded by a single space on either side"
        },

        schema: []

    },

    create: function(context) {

        let sourceCode = context.getSourceCode();

        function inspectAssignmentExpression(emitted) {
            /**
			 * node.operator is refined here by adding backslash before all the 'special' characters.
			 * 'special' chars are thos chars that are part of solidity assignment operators and, if used without backslash in JS RegExp,
			 * behave as wildcard characters. So to make sure they're treated as simple strings, we add '\' before them.
			 * As of today, these chars include: * / + | ^
			 */
            let node = emitted.node,
                op = node.operator.replace(/([\+\*\/\|\^])/g, "\\$1"), opLength = node.operator.length;

            if (emitted.exit) {
                return;
            }

            // If expression is 'abc *= def;', then charsAfterLeftNode will contain ' *= d'.
            let charsAfterLeftNode = sourceCode.getNextChars(node.left, 3 + opLength),
                validationRegexp = new RegExp("^ " + op + " [^\\s]$");

            (!validationRegexp.test(charsAfterLeftNode)) && context.report({
                node: node.left,
                message: "Assignment operator must have exactly single space on both sides of it."
            });
        }

        //statement like `var x = 10` doesn't come under AssignmentExpression, so needs to be checked separately
        function inspectVariableDeclaration(emitted) {
            let node = emitted.node, code = sourceCode.getText(node);

            if (emitted.exit) {
                return;
            }

            //if a particular character is '=', check its left and right for single space
            for (let i = 2; i < code.length; i++) {
                if (code [i] === "=") {
                    (!/^[^\/\s] $/.test(code.slice(i-2, i))) && context.report({
                        node: node,
                        message: "There should be only a single space between assignment operator '=' and its left side."
                    });

                    (!/^ [^\/\s]$/.test(code.slice(i+1, i+3))) && context.report({
                        node: node,
                        message: "There should be only a single space between assignment operator '=' and its right side."
                    });
                }
            }
        }

        function inspectBinaryExpression(emitted) {
            let leftNode,
                node = emitted.node;

            if (emitted.exit) {
                return;
            }

            /**
			 * If the left & right side of expression span over multiple lines,
			 * make sure that the operator resides on the same line as the left expression (ie, first line)
			 */
            // 1. take line no. of both left & right expr. Line no (right) = line (left) + 1
            // Take string btw them, should be NO \n before operator. that's it

            let rightNodeStartingLine = sourceCode.getLine(node.right),
                leftNodeEndingLine = sourceCode.getEndingLine(node.left),
                opRegExp = node.operator.replace(/([\+\*\/\|\^])/g, "\\$1");

            if (rightNodeStartingLine > leftNodeEndingLine) {
                let validationRegexOpOnSameLineAsLeftNodeEnd = new RegExp("^[^\\n]*" + opRegExp),
                    strBetweenLeftAndRightNode = sourceCode.getStringBetweenNodes(node.left, node.right);

                if (rightNodeStartingLine !== leftNodeEndingLine + 1) {
                    context.report({
                        node: node,
                        location: {
                            column: sourceCode.getColumn(node.right),
                            line: sourceCode.getLine(node.right)
                        },
                        message: "In Binary Expressions that span over multiple lines, expression on the right side of the operator (" + node.operator + ") must be exactly 1 line below the line on which the left expression ends."
                    });
                }

                if (!validationRegexOpOnSameLineAsLeftNodeEnd.test(strBetweenLeftAndRightNode)) {
                    context.report({
                        node: node,
                        location: {
                            column: sourceCode.getEndingColumn(node.left)
                        },
                        message: "Operator \"" + node.operator + "\" should be on the line where left side of the Binary expression ends."
                    });
                }

                return;
            }

            // Handle case where left node is a binary expression and right node may be a literal
            if (sourceCode.isASTNode(node.left) && node.left.type === "BinaryExpression"){
                leftNode = node.left.right;
            } else {
                leftNode = node.left;
            }

            let strBetweenLeftAndRight = sourceCode.getStringBetweenNodes(leftNode, node.right).split(node.operator),
                onlyCharsRegExp = /^[^\s\/]$/;

            if (strBetweenLeftAndRight [0].slice(-1) === " " || strBetweenLeftAndRight [1] [0] === " ") {
                if (strBetweenLeftAndRight [0].slice(-1) !== strBetweenLeftAndRight [1] [0]) {
                    context.report({
                        node: node,
                        location: {
                            column: sourceCode.getEndingColumn(node.left) + 1
                        },
                        message: "Single space should be either on both sides of '" + node.operator + "' or not at all."
                    });
                } else {
                    let secondLastCharOnLeft = strBetweenLeftAndRight [0].slice(-2, -1),
                        secondCharOnRight = strBetweenLeftAndRight [1] [1];

                    secondLastCharOnLeft && (!onlyCharsRegExp.test(secondLastCharOnLeft)) && context.report({
                        node: node,
                        location: {
                            column: sourceCode.getEndingColumn(node.left)
                        },
                        message: "There should be a maximum of single space and no comments between left side and '" + node.operator + "'."
                    });

                    secondCharOnRight && (!onlyCharsRegExp.test(secondCharOnRight)) && context.report({
                        node: node,
                        location: {
                            column: sourceCode.getColumn(node.right)
                        },
                        message: "There should be a maximum of single space and no comments between right side and '" + node.operator + "'."
                    });
                }

                return;
            }

            let firstCharOnLeft = strBetweenLeftAndRight [0].slice(-1),
                firstCharOnRight = strBetweenLeftAndRight [1] [0];

            firstCharOnLeft && (!onlyCharsRegExp.test(firstCharOnLeft)) && context.report({
                node: node,
                location: {
                    column: sourceCode.getEndingColumn(node.left)
                },
                message: "There should be no comments between left side and '" + node.operator + "'."
            });

            firstCharOnRight && (!onlyCharsRegExp.test(firstCharOnRight)) && context.report({
                node: node,
                location: {
                    column: sourceCode.getColumn(node.right)
                },
                message: "There should be no comments between right side and '" + node.operator + "'."
            });

        }

        return {
            BinaryExpression: inspectBinaryExpression,
            VariableDeclaration: inspectVariableDeclaration,
            AssignmentExpression: inspectAssignmentExpression
        };

    }

};


/**
 * @fileoverview RuleContext object's class definition - this object is what we pass to every rule being executed
 * @author Raghav Dua <duaraghav8@gmail.com>
 */

"use strict";

let util = require("util"),
    { EOL } = require("os"),
    isErrObjectValid = require("../config/schemas/error-reported-by-rule").validationFunc;

let INHERITABLE_METHODS = [
    "getSourceCode"
];

/**
 * context object Constructor to set read-only properties and provide additional functionality to the rules using it
 * @param {String} ruleName Name of the rule the object is for
 * @param {Object} ruleDesc Description of the rule the object is for
 * @param {Object} ruleMeta meta object defined inside the rule file by rule developer
 * @param {Object} Solium Main Solium object from which to inherit functionality to provide to the rules
 */
function RuleContext(ruleName, ruleDesc, ruleMeta, Solium) {
    let contextObject = this;

    // Set contect attribute 'options' iff options were provided.
    ruleDesc.options && Object.assign(contextObject, { options: ruleDesc.options });

    //set read-only properties of the context object
    Object.defineProperties(contextObject, {

        name: {
            value: ruleName,
            writable: false	//though the default is false anyway, I think its better to express your intention clearly
        },

        meta: {
            value: ruleDesc,
            writable: false
        }

    });

    //inherit all Solium methods which are of relevance to the rule
    INHERITABLE_METHODS.forEach(function(methodName) {
        contextObject [methodName] = function(s, z, a, b, o) {	//every method will receive 5 arguments tops
            return Solium [methodName].call(Solium, s, z, a, b, o);
        };
    });

    /**
     * wrapper around Solium.report () which adds some additional information to the error object
     * @param {Object} error An object describing the lint error, sent by the rule currently running
     */
    contextObject.report = function(error) {

        if (!isErrObjectValid(error)) {
            throw new Error(
                `Rule "${ruleName}": invalid error object was passed. AJV message:${EOL}${util.inspect(isErrObjectValid.errors)}`
            );
        }

        Object.assign(error, { ruleName: ruleName, ruleMeta: ruleMeta, type: contextObject.meta.type });
        Solium.report(error);

    };
}

RuleContext.prototype = { constructor: RuleContext };
module.exports = RuleContext;


/**
 * @fileoverview Load specified rule definitions from the given rule repository.
 * @author Raghav Dua <duaraghav8@gmail.com>
 */

"use strict";

let path = require("path"), util = require("util");
let isAValidSharableConfig = require("./config-inspector").isAValidSharableConfig;

let constants = {
    SOLIUM_RULESET_ALL: "solium:all",
    SOLIUM_RULESET_RECOMMENDED: "solium:all",
    SOLIUM_CORE_RULES_DIRNAME: "rules",
    SOLIUM_PLUGIN_PREFIX: "solium-plugin-",
    SOLIUM_SHARABLE_CONFIG_PREFIX: "solium-config-"
};

constants.SOLIUM_CORE_RULES_DIRPATH = "../" + constants.SOLIUM_CORE_RULES_DIRNAME;

/**
 * Given a ruleset name, determine whether its a core set or a sharable config and load the rule config accordingly.
 * @param {String} upstream The ruleset to resolve
 * @returns {Object} ruleConfigs Set of rule configs exported by the upstream ruleset.
 */
function resolveUpstream(upstream) {
    let coreRulesetRegExp = /^solium:[a-z_]+$/;

    // Determine whether upstream is a solium core ruleset or a sharable config.
    if (coreRulesetRegExp.test(upstream)) {
        try {
            return require("../../config/rulesets/solium-" + upstream.split(":") [1]).rules;
        } catch (e) {
            throw new Error("\"" + upstream + "\" is not a core ruleset.");
        }
    }

    // If flow reaches here, it means upstream is a sharable config.
    let configName = constants.SOLIUM_SHARABLE_CONFIG_PREFIX + upstream, config;

    try {
        config = require(configName);
    } catch (e) {
        if (e.code === "MODULE_NOT_FOUND") {
            throw new Error(
                "The sharable config \"" + configName + "\" is not installed. " +
				"If Solium is installed globally, install the config globally using " +
				"\"npm install -g " + configName + "\". Else install locally using " +
				"\"npm install --save-dev " + configName + "\"."
            );
        }

        throw new Error("The sharable config \"" + configName + "\" could not be loaded: " + e.message);
    }

    if (isAValidSharableConfig(config)) {
        return config.rules;
    }

    throw new Error("Invalid sharable config \"" +
		configName + "\". AJV message:\n" + util.inspect(isAValidSharableConfig.errors));
}

/**
 * Create provided plugin's default rule configuration
 * @param {String} name Plugin name without 'solium-plugin-' prefix
 * @param {Object} plugin The plugin Object as exported by the solium plugin
 * @returns {Object} config Rule configuration object for the given plugin
 */
function resolvePluginConfig(name, plugin) {
    let config = {};

    Object.keys(plugin.rules).forEach(function(ruleName) {
        config [name + "/" + ruleName] = plugin.rules [ruleName].meta.docs.type;
    });

    return config;
}

/**
 * Load rule definitions (rule objects) from Solium's core rule directory & from pre-installed linter plugins.
 * @param {Array} listOfRules List of string that representing rule file names in the rule dir.
 * @returns {Object} ruleDefs Object of key: rule name & value: rule object exported by corresponding Solium rule definition.
 */
function load(listOfRules) {
    let ruleDefs = {};

    listOfRules.forEach(function(name) {
        // If the rule is part of a plugin, first load the plugin assuming that it has already been installed
        // in the same scope as Solium (global/project). Then return the appropriate rule from that plugin.
        if (name.indexOf("/") > -1) {
            let parts = name.split("/"),
                pluginName = constants.SOLIUM_PLUGIN_PREFIX + parts [0], ruleName = parts [1], plugin;

            try {
                plugin = require(pluginName);
            } catch (e) {
                throw new Error("Unable to load Plugin \"" + pluginName + "\".");
            }

            // No need to verify whether this rule's implementation exists & is valid or not.
            // That is done at a later stage in solium.js itself using rule-inspector.
            // TODO: Examine "peerDependencies" of the plugin to ensure its compatible with current version of Solium.
            return ruleDefs [name] = plugin.rules [ruleName];
        }

        // If we're here, it means the rule is just a regular core rule :)
        let ruleFile = path.join(constants.SOLIUM_CORE_RULES_DIRPATH, name);

        try {
            ruleDefs [name] = require(ruleFile);
        } catch (e) {
            throw new Error("Unable to read " + ruleFile);
        }
    });

    return ruleDefs;
}

module.exports = {
    load: load,
    constants: constants,
    resolveUpstream: resolveUpstream,
    resolvePluginConfig: resolvePluginConfig
};