var debug = require('debug')('CssValidator');

var fs = require('fs');
var path = require('path');

var css = require('css');
var utils = require('../lib/utils');
var sizeOf = require('image-size');

var Errors = require('../data/errors.json');
var resourceSpec = require('../data/resource_spec.json');

var CssValidator = function(errorHandler) {
  if (!(this instanceof CssValidator)) {
    return new CssValidator(errorHandler);
  }
  this.errorHandelr = errorHandler;
  this.ast = null;

  //property white list
  this.CssPropertySpec = require('../data/css_property_datatype.json');
  //primitive data type such as <length>, <color>, <number>
  this.CssPrimitiveSpec = require('../data/css_primitive_datatype.json');
};

CssValidator.prototype.validate = function(source, basePath, filePath, startLine, options) {
  var self = this;
  this.startLine = startLine;
  this.basePath = basePath;
  this.filePath = filePath;
  this.relativePath = path.relative(this.basePath, this.filePath);

  this.options = options || {
    'profile': 'wearable',
    'lang': null
  };
  this.profileConfig = resourceSpec[this.options.profile];

  //css tree
  this.ast = css.parse(source, {silent : true});

  if (this.ast.type !== 'stylesheet')
    return this.errorHandelr.printError('CSS_STYLESHEET_UNKNOWN', this.relativePath, this.startLine, 0);

  this.ast.stylesheet.parsingErrors.map(function(e){
    self.errorHandelr.printError('CSS_STYLESHEET_SYNTAX', self.relativePath, self.startLine, 0);
  });

  debug('stylesheet rules')
  debug(this.ast.stylesheet.rules)
  debug('stylesheet parsingErrors')
  debug(this.ast.stylesheet.parsingErrors)
  this._checkCSS(this.ast.stylesheet.rules);
};

CssValidator.prototype._printError = function(errorName, ref, args) {
  if (Array.isArray(args)) {
    args.forEach(function(element,index,array){
      array[index] = String(array[index]).replace(/\r|\n/g, '\\n');
    })
  }
  return this.errorHandelr.printError(errorName, this.relativePath, (this.startLine + (ref ? ref.position.start.line : 0)), (ref ? ref.position.start.column : 0), args, ref);
};

//building a regex
CssValidator.prototype._getPrimitiveRegex = function(dataType, isRange) {
  var self = this;

  if (!self.CssPrimitiveSpec.hasOwnProperty(dataType)) return null;
  if (self.CssPrimitiveSpec[dataType].isRegex)
    return (isRange ? '' : '^') + self.CssPrimitiveSpec[dataType]['regex'] + (isRange ? '' : '$');

  var str = self.CssPrimitiveSpec[dataType]["white"];

  //parsing white str.
  var list = [];
  var temp = '';
  var delimiter = '|';
  var level = 0;

  for (var i = 0; i<str.length;i++) {
    if (str[i] === '|' && level === 0) {
      list.push(temp);
      temp = '';
      continue;
    }
    if (str[i] === ']') {
      level--;
    }
    if (str[i] === '[') {
      level++;
    }

    //FIXME : multi level
    if (level > 1) {
      throw new TypeError('unsupported multiple level in css primitive datatype');
    }

    temp +=str[i];
  }
  list.push(temp);

  var regex = [];
  for (var i in list) {
    var type = list[i].trim();
    var range = null;
    if (type.indexOf('{') !== -1) {
      range = type.substr(type.indexOf('{'), type.indexOf('}'));
    }

    if (type.indexOf('[') !== -1) {  //it's array
      var subList = type.substr(type.indexOf('[') +1, type.indexOf(']') -1).split('|');
      var subRegex = [];

      for (var j in subList) {
        subRegex.push(self.CssPrimitiveSpec.hasOwnProperty(subList[j])
                      ? '('+self._getPrimitiveRegex(subList[j], (range) ? true : false) +')'
                      : ((isRange || range) ? '' : '^') + subList[j] + ((isRange || range) ? '' : '$')
                     );
      }

      if (range) {
        regex.push( '^((' + subRegex.join('|') +')\\s*)' + range + '$');
      } else
        regex.push( '(' + subRegex.join('|') +')' );

    } else
      regex.push(self.CssPrimitiveSpec.hasOwnProperty(type) ? '('+self._getPrimitiveRegex(type, isRange) +')' : (isRange ? '' :'^')+type+ (isRange ? '' :'$'));
  }

  return regex.join('|');
}

CssValidator.prototype._getPropertyRegex = function(property) {
  var self = this;
  var isShorthands = false;
  if (!self.CssPropertySpec.hasOwnProperty(property)) return null;

  var type = self.CssPropertySpec[property].dataType;  //supporsed that it's 1 level array or not
  var range = null;
  if (type.indexOf('{') !== -1) {
    range = type.substr(type.indexOf('{'), type.indexOf('}'));
    type = type.substr(0, type.indexOf('{'));
  }

  var list = type.replace('[','').replace(']','').split('||');

  if (list.length > 1)  { // is shorthands
    isShorthands = true;
  }

  var regex = [];
  for (var i in list) {
    var subRegex = self._getPrimitiveRegex(list[i].trim(), (range || isShorthands) ? true : false);
    if (!subRegex) return null;

    regex.push('(' + subRegex + ')' );
  }

  debug('regex list')
  debug(regex);

  if (isShorthands) {
    var candidates = (function(_items) {
      var _possible = [];
      var _move = function(index, bag) {
        if (index !== -1)
          bag.push(_items[index]);

        if (bag.length == _items.length)
          return _possible.push('^(' + bag.join('?\\s*') +'?\\s*)' + '$');

        for (var i in list) {
          if (bag.indexOf(_items[i]) !== -1) continue;
          _move(i, bag.slice());
        }
      }
      _move(-1, []);

      return _possible;
    })(regex.slice());

    debug('shorthands candidates')
    debug(candidates);

    return candidates.join('|');
  } else if (range)
    return '^(' + regex.join('|') +'\\s*)' + range + '$';
  else
    return regex.join('|');
}

CssValidator.prototype._checkProperty = function(o) {
  var self = this;

  var property = String(o.property).toLowerCase();
  var value = o.value;

  if (!self.CssPropertySpec.hasOwnProperty(property)) return false;

  //pass through if value is 'initial' or 'inherit'
  if (['initial','inherit'].indexOf(value) !== -1 )
    return false;

  var whiteRegex = self._getPropertyRegex(property,'white');

  debug('property : ' + property)
  debug('value : ' + value);
  debug('white : ' + whiteRegex);

  //check white list
  if (whiteRegex) {
    if (!value.match(new RegExp(whiteRegex,'i')))
      return {error : true,  way : 'white'};
  }

  // if white has '<uri>', then if value is a type of <uri>
  var uriRegex = self.CssPrimitiveSpec['<uri>']['regex'];
  if (whiteRegex.indexOf(uriRegex) !== -1) {
    if (value.match(uriRegex)) {
      var url = value.substring(value.indexOf('(')+2, value.indexOf(')') -1);
      var result = require('url').parse(url);
      if (result.protocol !== null) {
        if (result.protocol === 'http:' || result.protocol === 'https:')
          self._printError('PERFORMANCE_DISALLOWED_NETWORK_REQUEST', o);
        else
          self._printError('PERFORMANCE_INVALID_PROTOCOL', o, [url]);
      } else {  // without protocol
        var resourcePath = path.join(self.basePath, result.pathname);

        if (self.options.lang !== null)
          resourcePath = utils.checkLocalePath(self.basePath, result.pathname, self.options.lang);

        if (utils.isLocalFile(resourcePath)) {
          var dimensions = null;
          try {
            dimensions = sizeOf(resourcePath);
            if (self.profileConfig.SUPPORT_IMAGE.indexOf(dimensions.type) === -1)
              throw new Error('invalid image type');
          } catch(e) {
            debug(e);
            return self._printError('PERFORMANCE_INVALID_IMAGE_TYPE', o, [path.basename(resourcePath)]);
          }

          if (dimensions && (dimensions.width > self.profileConfig.VIEWPORT_MAX_WIDTH
            || dimensions.height > self.profileConfig.VIEWPORT_MAX_HEIGHT))
            self._printError('PERFORMANCE_EXCEED_IMAGE_DIMENSION', o, [path.basename(resourcePath), dimensions.width, dimensions.height, self.profileConfig.VIEWPORT_MAX_WIDTH, self.profileConfig.VIEWPORT_MAX_HEIGHT, self.options.profile, self.profileConfig.VIEWPORT_WIDTH, self.profileConfig.VIEWPORT_HEIGHT ]);
        } else {
          this._printError('PERFORMANCE_NO_FILE', o, [result.pathname]);
        }
      }
    }
  }

  return {error : false};
}

CssValidator.prototype._checkCSS = function(o) {
  var self = this;

  if (!o) return;

  if (Array.isArray(o))
    return o.map(function(e){self._checkCSS(e)});

  if (o.type === 'rule') {
    //check selectors
    // console.log(o.selectors);
    o.selectors.map(function(selector){
      if (selector.match(/\s+/)
        && !selector.match(/.*\..+\..+/)
        && !selector.match(/>/)
        && !selector.match(/\+/)
        && !selector.match(/\[|\]/)
        && !selector.match(/~/))
        self._printError('CSS_SELECTOR_INVALID_DESCENDANT', o, [selector]);

      if (selector.match(/.*\..+\..+/))
        self._printError('CSS_SELECTOR_INVALID_SUBSET_CLASS', o, [selector]);

      if (selector.match(/>/))
        self._printError('CSS_SELECTOR_INVALID_CHILD', o, [selector]);

      if (selector.match(/\+/))
        self._printError('CSS_SELECTOR_INVALID_ADJACENT_SIBLING', o, [selector]);

      if (selector.match(/\[|\]/))
        self._printError('CSS_SELECTOR_INVALID_ATTRIBUTE', o, [selector]);

      if (selector.match(/~/))
        self._printError('CSS_SELECTOR_INVALID_GENERAL_SIBLING', o, [selector]);

      if (selector.match(/::/)) {
        self._printError('CSS_SELECTOR_INVALID_PSEUDO_ELEMENT', o, [selector]);
        selector = selector.replace(/::[a-zA-Z-]*/g,'');
      }

      var a = selector.match(/:[a-zA-Z]*/g);
      if (Array.isArray(a)) {
        a.map(function(e){
          if (e !== ':active')
            self._printError('CSS_SELECTOR_INVALID_PSEUDO_CLASS', o, [selector]);
        });
      }

    });

    return self._checkCSS(o.declarations);
  }


  if (o.type === 'keyframes')
    return self._printError('CSS_KEYFRAME_INVALID', o);

  if (o.type === 'keyframe')
    return self._printError('CSS_KEYFRAME_INVALID', o);

  if (o.type === 'media') {
    return self._printError('CSS_MEDIA_QUERY_INVALID', o);
    //return checkCSS(o.rules); //do not check child
  }

  if (o.type === 'comment') return;

  if (o.type === 'declaration') {
    //property name - white list check
    if (!self.CssPropertySpec.hasOwnProperty(String(o.property).toLowerCase())) {
      return self._printError('CSS_PROPERTY_INVALID', o, [o.property]);
    }

    if (o.value.match(/!IMPORTANT/i)) {
      self._printError('CSS_EXCEPTION_IMPORTANT_INVALID', o, [o.value]);
      //remove '!IMPORTANT' for next usage
      o.value = o.value.replace(/!IMPORTANT/i,'').trim();
    }

    //property value - data type check
    var result = self._checkProperty(o);
    if (result && result.error) {
      if (result.way === 'white')
        self._printError('CSS_PROPERTY_INVALID_WHITE', o, [o.property, o.value]);
      else
        throw new TypeError('unknown error type');
    }
    return;
  }

  //raise an error on all @ at-rules
  if ([ 'supports', 'host'
      , 'custom-media', 'page'
      , 'document', 'font-face'
      , 'import', 'charset'
      , 'namespace'].indexOf(o.type) !== -1) {
    return self._printError('CSS_ATRULE_INVALID', o, [o.type]);
  }

  debug(o);
  throw new TypeError('unknown css type');
}

module.exports = CssValidator;