text: 'use strict';
  
  
  require('babel-polyfill');
  
  let fs = require('fs');
  let gonzales = require('gonzales-pe');
  let minimatch = require('minimatch');
  let Errors = require('./errors');
  let Plugin = require('./plugin');
  
  let vow = require('vow');
  let vfs = require('vow-fs');
  
  class Comb {
    constructor() {
      this.config = {};
      this.exclude = [];
      // Whether lint mode is on.
      this.lint = false;
      // List of file paths that should be excluded from processing.
      this.pathsToExclude = null;
      // List of used plugins.
      this.plugins = [];
      this.pluginsDependencies = {};
      // List of supported syntaxes.
      this.supportedSyntaxes = new Set();
      // Mapping file extensions to syntax
      this.syntaxMap = new Map();
      // Whether verbose mode is on.
      this.verbose = false;
    }
  
    /**
     * Loads configuration from JSON.
     *
     * @param {!Object} config
     * @return {!Comb}
     */
    configure(config) {
      if (typeof config !== 'object')
        // TODO: throw error
        throw new Error();
  
      this.lint = config.lint;
      this.verbose = config.verbose;
      if (config.exclude)
        this.exclude = config.exclude.map(function(pattern) {
          return new minimatch.Minimatch(pattern);
        });
  
      if (config.syntax) {
        for (let key in config.syntax) {
          this.syntaxMap.set(key, config.syntax[key]);
        }
      }
  
      for (let i = 0, l = this.plugins.length; i < l; i++) {
        let plugin = this.plugins[i];
        let name = plugin.name;
        if (!config.hasOwnProperty(name)) continue;
  
        try {
          plugin.value = config[name];
          this.config[name] = plugin.value;
        } catch (e) {
          // TODO: throw error
        }
      }
  
      // Chaining.
      return this;
    }
  
    /**
     * Lints all files in a directory.
     *
     * @param {String} path
     * @returns {Promise}
     */
    lintDirectory(path) {
      let files = this._getAcceptableFilesFromDirectory(path);
      let promises = files.map((file) => this.lintFile(file));
      return Promise.all(promises);
    }
  
    /**
     * Lints a single file.
     *
     * @param {String} path
     * @returns {Promise}
     */
    lintFile(path) {
      let syntax = this._extractSyntax(path);
      return this._readFile(path).then((string) => {
        return this.lintString(string, {syntax: syntax, filename: path});
      });
    }
  
    /**
     * Lints a file or a directory.
     *
     * @param {String} path
     */
    lintPath(path) {
      path = path.replace(/\/$/, '');
      return fs.statSync(path).isDirectory() ?
        this.lintDirectory(path) :
        this.lintFile(path);
    }
  
    /**
     * Lints a string.
     *
     * @param {String} text
     * @param {{context: String, filename: String, syntax: String}} options
     * @returns {Promise} Resolves with <Array> list of found errors.
     */
    lintString(text, options) {
      return this._parseString(text, options)
        .then(this._lintTree.bind(this))
        .then(errors => {
          errors.forEach(error => {
            error.context = this._getContext(text, error.line);
          });
  
          return errors;
        });
    }
  
    _getContext(text, currentLineNumber) {
      var LINES_AROUND = 2;
      var result = [];
      var start = currentLineNumber - 1 - LINES_AROUND;
      var end = currentLineNumber + LINES_AROUND;
      var lines = text.split(/\r\n|\r|\n/);
  
      for (var i = start; i < end; i++) {
        var line = lines[i];
        if (!line) continue;
        var ln = i + 1;
        if (ln === currentLineNumber) {
          result.push(ln + '*| ' + line);
        } else {
          result.push(ln + ' | ' + line);
        }
      }
      return result.join('\n');
    }
  
    /**
     * Processes directory recursively.
     *
     * @param {String} path
     * @returns {Promise}
     */
    processDirectory(path) {
      let that = this;
  
      return vfs.listDir(path).then(function(filenames) {
        return vow.all(filenames.map(function(filename) {
          let fullname = path + '/' + filename;
          return vfs.stat(fullname).then(function(stat) {
            if (stat.isDirectory() && that._shouldProcess(fullname)) {
              return that.processDirectory(fullname);
            } else {
              return that.processFile(fullname);
            }
          });
        })).then(function(results) {
          return [].concat.apply([], results);
        });
      });
    }
  
    /**
     * Processes single file.
     *
     * @param {String} path
     * @returns {Promise}
     */
    processFile(path) {
      let that = this;
  
      if (!this._shouldProcessFile(path)) return;
  
    return new Promise(function(resolve) {
        vfs.read(path, 'utf8').then(function(data) {
          let syntax = that._extractSyntax(path);
          that.processString(data, {
            syntax: syntax,
            filename: path
          }).then(function(processedData) {
            // Suppose there is no style file content shorter than 2 characters.
            if (!processedData || processedData.length < 3) {
              var status = !processedData ? 'empty:' : 'invalid:';
              if (that.verbose) console.log(status, path);
              resolve(0);
            }
            else if (data === processedData) {
              if (that.verbose) console.log('good:', path);
              resolve(0);
            }
            else if (that.lint) {
              if (that.verbose) console.log('bad:', path);
              resolve(0);
            } else {
              vfs.write(path, processedData, 'utf8').then(function() {
                if (that.verbose) console.log('now is good:', path);
                resolve(1);
              });
            }
          });
        });
      });
    }
  
    /**
     * Processes directory or file.
     *
     * @returns {Promise}
     */
    processPath(path) {
      let that = this;
      path = path.replace(/\/$/, '');
  
      return vfs.stat(path).then(function(stat) {
        if (stat.isDirectory()) {
          return that.processDirectory(path);
        } else {
          return that.processFile(path);
        }
      });
    }
  
    /**
     * Processes a string.
     *
     * @param {String} text
     * @param {{context: String, filename: String, syntax: String}} options
     * @returns {Promise<string>} Resolves in processed string
     */
    processString(text, options) {
      this.options = options;
      return this._parseString(text, options)
          .then(this._processTree.bind(this))
          .then((ast) => ast.toString());
    }
  
    /**
     * Add a plugin.
     * @param {Object} options
     * @return {Comb}
     */
    use(options) {
      // Check whether plugin with the same is already used.
      let pluginName = options.name;
      if (this._pluginAlreadyUsed(pluginName)) {
        if (this.verbose)
          console.warn(Errors.twoPluginsWithSameName(pluginName));
        return;
      }
  
      let plugin = new Plugin(options);
  
      plugin.syntax.forEach(function(s) {
        this.supportedSyntaxes.add(s);
      }, this);
  
      // Sort plugins.
      let pluginToRunBefore = plugin.runBefore;
  
      if (!pluginToRunBefore) {
        this.plugins.push(plugin);
      } else {
        if (this._pluginAlreadyUsed(pluginToRunBefore)) {
          let i = this._pluginIndex(pluginToRunBefore);
          this.plugins.splice(i, 0, plugin);
        } else {
          this.plugins.push(plugin);
          if (!this.pluginsDependencies[pluginToRunBefore])
            this.pluginsDependencies[pluginToRunBefore] = [];
          this.pluginsDependencies[pluginToRunBefore].push(pluginName);
        }
      }
  
      let dependents = this.pluginsDependencies[pluginName];
      if (!dependents) return this;
  
      for (let i = 0, l = dependents.length; i < l; i++) {
        let name = dependents[i];
        let x = this._pluginIndex(name);
        let plugin = this.plugins[x];
        this.plugins.splice(x, 1);
        this.plugins.splice(-1, 0, plugin);
      }
  
      // Chaining.
      return this;
    }
  
    _getAcceptableFilesFromDirectory(path) {
      if (!this._shouldProcess(path)) return;
  
      let files = [];
      let filesInThisDir = fs.readdirSync(path);
  
      for (let i = 0, fl = filesInThisDir.length; i < fl; i++) {
        let fullname = path + '/' + filesInThisDir[i];
        let stat = fs.statSync(fullname);
        if (stat.isDirectory() && this._shouldProcess(fullname))
          files = files.concat(this._getAcceptableFilesFromDirectory(fullname));
        else if (this._shouldProcessFile(fullname))
          files.push(fullname);
      }
  
      return files;
    }
  
    /**
     * @param {Node} ast
     * @param {String=} filename
     * @return {Array} List of errors.
     */
    _lintTree(ast, filename) {
      let errors = [];
      let config = this.config;
  
      this.plugins.filter(function(plugin) {
        return typeof plugin.value !== null &&
               typeof plugin.lint === 'function' &&
               plugin.syntax.indexOf(ast.syntax) !== -1;
      }).forEach(function(plugin) {
        let e = plugin.lint(ast, config);
        errors = errors.concat(e);
      });
  
      if (filename) {
        errors.map(function(error) {
          error.filename = filename;
          return error;
        });
      }
  
      return errors;
    }
  
    _parseString(text, options) {
      let syntax = options && options.syntax;
      let filename = options && options.filename || '';
      let context = options && options.context;
      let tree = [];
  
      if (!syntax) syntax = 'css';
      this.syntax = syntax;
        try {
          tree = gonzales.parse(text, {syntax: syntax, rule: context});
        } catch (e) {
          console.log('\nString parsing exception in: ', filename, '\n', e, '\n');
        }
  
      return new Promise(function(resolve, reject) {
          resolve(tree, filename);
          reject();
      });
    }
  
    _pluginAlreadyUsed(name) {
      return this._pluginIndex(name) !== -1;
    }
  
    _pluginIndex(name) {
      let index = -1;
      this.plugins.some(function(plugin, i) {
        if (plugin.name === name) {
          index = i;
          return true;
        }
      });
      return index;
    }
  
    /**
     * @param {Node} ast
     * @return {Node} Transformed AST
     */
    _processTree(ast) {
      let config = this.config;
      let astOriginal = ast;
      let that = this;
  
      this.plugins.filter(function(plugin) {
        return plugin.value !== null &&
               typeof plugin.process === 'function' &&
               plugin.syntax.indexOf(ast.syntax) !== -1;
      }).forEach(function(plugin) {
        try {
          plugin.process(ast, config);
        } catch (e) {
          console.log('\nThe option exception in: ', that.options.filename, '\n', e, '\n');
          return astOriginal;
        }
      });
  
      return ast;
    }
  
    _readFile(path) {
      return new Promise((resolve, reject) => {
        if (!this._shouldProcessFile(path)) reject();
  
        fs.readFile(path, 'utf8', function(e, string) {
          if (e) reject();
          resolve(string);
        });
      });
    }
  
    /**
     * Checks if path is not present in `exclude` list.
     *
     * @param {String} path
     * @returns {Boolean} False if specified path is present in `exclude` list.
     * Otherwise returns true.
     */
    _shouldProcess(path) {
      path = path.replace(/\/$/, '');
      if (!fs.existsSync(path)) {
        console.warn('Path ' + path + ' was not found.');
        return false;
      }
  
      path = path.replace(/^\.\//, '');
      return this.exclude.every(function(e) {
        return !e.match(path);
      });
    }
  
    /**
     * Checks if specified path is not present in `exclude` list and it has one of
     * acceptable syntaxes.
     *
     * @param {String} path
     * @returns {Boolean} False if the path either has unacceptable extension or
     * is present in `exclude` list. True if everything is ok.
     */
    _shouldProcessFile(path) {
      // Get file's extension:
      var syntax = this._extractSyntax(path);
  
      // Check if syntax is supported. If not, ignore the file:
      if (!this.supportedSyntaxes.has(syntax))
        return false;
  
      return this._shouldProcess(path);
    }
  
    /**
     * Extract syntax by file path
     *
     * @param {String} path
     * @returns {String} syntax
     */
    _extractSyntax(path) {
      var extension = path.split('.').pop();
  
      return this.syntaxMap.get('.' + extension) || extension;
    }
  }
  
  module.exports = Comb;
  