/**
 * @license
 * Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
 * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
 * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
 * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
 * Code distributed by Google as part of the polymer project is also
 * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
 */

import { Tokenizer } from './tokenizer';
import { Token } from './token';
import { NodeFactory } from './node-factory';

/**
 * Class that implements a shady CSS parser.
 */
class Parser {
  /**
   * Create a Parser instance. When creating a Parser instance, a specialized
   * NodeFactory can be supplied to implement streaming analysis and
   * manipulation of the CSS AST.
   */
  constructor(nodeFactory = new NodeFactory()) {
    this.nodeFactory = nodeFactory;
  }

  /**
   * Parse CSS and generate an AST.
   * @param {string} cssText The CSS to parse.
   * @return {object} A CSS AST containing nodes that correspond to those
   * generated by the Parser's NodeFactory.
   */
  parse(cssText) {
    return this.parseStylesheet(new Tokenizer(cssText));
  }

  /**
   * Consumes tokens from a Tokenizer to parse a Stylesheet node.
   * @param {Tokenizer} tokenizer A Tokenizer instance.
   * @return {object} A Stylesheet node.
   */
  parseStylesheet(tokenizer) {
    return this.nodeFactory.stylesheet(this.parseRules(tokenizer));
  }

  /**
   * Consumes tokens from a Tokenizer to parse a sequence of rules.
   * @param {Tokenizer} tokenizer A Tokenizer instance.
   * @return {array} A list of nodes corresponding to rules. For a parser
   * configured with a basic NodeFactory, any of Comment, AtRule, Ruleset,
   * Declaration and Discarded nodes may be present in the list.
   */
  parseRules(tokenizer) {
    let rules = [];

    while (tokenizer.currentToken) {
      let rule = this.parseRule(tokenizer);

      if (rule) {
        rules.push(rule);
      }
    }

    return rules;
  }

  /**
   * Consumes tokens from a Tokenizer to parse a single rule.
   * @param {Tokenizer} tokenizer A Tokenizer instance.
   * @return {object} If the current token in the Tokenizer is whitespace,
   * returns null. Otherwise, returns the next parseable node.
   */
  parseRule(tokenizer) {
    // Trim leading whitespace:
    if (tokenizer.currentToken.is(Token.type.whitespace)) {
      tokenizer.advance();
      return null;

    } else if (tokenizer.currentToken.is(Token.type.comment)) {
      return this.parseComment(tokenizer);

    } else if (tokenizer.currentToken.is(Token.type.word)) {
      return this.parseDeclarationOrRuleset(tokenizer);

    } else if (tokenizer.currentToken.is(Token.type.propertyBoundary)) {
      return this.parseUnknown(tokenizer);

    } else if (tokenizer.currentToken.is(Token.type.at)) {
      return this.parseAtRule(tokenizer);

    } else {
      return this.parseUnknown(tokenizer);
    }
  }

  /**
   * Consumes tokens from a Tokenizer to parse a Comment node.
   * @param {Tokenizer} tokenizer A Tokenizer instance.
   * @return {object} A Comment node.
   */
  parseComment(tokenizer) {
    return this.nodeFactory.comment(tokenizer.slice(tokenizer.advance()));
  }

  /**
   * Consumes tokens from a Tokenizer through the next boundary token to
   * produce a Discarded node. This supports graceful recovery from many
   * malformed CSS conditions.
   * @param {Tokenizer} tokenizer A Tokenizer instance.
   * @return {object} A Discarded node.
   */
  parseUnknown(tokenizer) {
    let start = tokenizer.advance();
    let end;

    while (tokenizer.currentToken &&
           tokenizer.currentToken.is(Token.type.boundary)) {
      end = tokenizer.advance();
    }

    return this.nodeFactory.discarded(tokenizer.slice(start, end));
  }

  /**
   * Consumes tokens from a Tokenizer to parse an At Rule node.
   * @param {Tokenizer} tokenizer A Tokenizer instance.
   * @return {object} An At Rule node.
   */
  parseAtRule(tokenizer) {
    let name = '';
    let rulelist = null;
    let parametersStart = null;
    let parametersEnd = null;

    while (tokenizer.currentToken) {
      if (tokenizer.currentToken.is(Token.type.whitespace)) {
        tokenizer.advance();
      } else if (!name && tokenizer.currentToken.is(Token.type.at)) {
        // Discard the @:
        tokenizer.advance();
        let start = tokenizer.currentToken;
        let end;

        while (tokenizer.currentToken &&
               tokenizer.currentToken.is(Token.type.word)) {
          end = tokenizer.advance();
        }
        name = tokenizer.slice(start, end);
      } else if (tokenizer.currentToken.is(Token.type.openBrace)) {
        rulelist = this.parseRulelist(tokenizer);
        break;
      } else if (tokenizer.currentToken.is(Token.type.propertyBoundary)) {
        tokenizer.advance();
        break;
      } else {
        if (parametersStart == null) {
          parametersStart = tokenizer.advance();
        } else {
          parametersEnd = tokenizer.advance();
        }
      }
    }

    return this.nodeFactory.atRule(
        name,
        parametersStart ? tokenizer.slice(parametersStart, parametersEnd) : '',
        rulelist);
  }

  /**
   * Consumes tokens from a Tokenizer to produce a Rulelist node.
   * @param {Tokenizer} tokenizer A Tokenizer instance.
   * @return {object} A Rulelist node.
   */
  parseRulelist(tokenizer) {
    let rules = [];

    // Take the opening { boundary:
    tokenizer.advance();

    while (tokenizer.currentToken) {
      if (tokenizer.currentToken.is(Token.type.closeBrace)) {
        tokenizer.advance();
        break;
      } else {
        let rule = this.parseRule(tokenizer);
        if (rule) {
          rules.push(rule);
        }
      }
    }

    return this.nodeFactory.rulelist(rules);
  }

  /**
   * Consumes tokens from a Tokenizer instance to produce a Declaration node or
   * a Ruleset node, as appropriate.
   * @param {Tokenizer} tokenizer A Tokenizer node.
   * @return {object} One of a Declaration or Ruleset node.
   */
  parseDeclarationOrRuleset(tokenizer) {
    let ruleStart = null;
    let ruleEnd = null;
    let colon = null;

    while (tokenizer.currentToken) {
      if (tokenizer.currentToken.is(Token.type.whitespace)) {
        tokenizer.advance();
      } else if (tokenizer.currentToken.is(Token.type.openParenthesis)) {
        while (tokenizer.currentToken &&
               !tokenizer.currentToken.is(Token.type.closeParenthesis)) {
          tokenizer.advance();
        }
      } else if (tokenizer.currentToken.is(Token.type.openBrace) ||
                 tokenizer.currentToken.is(Token.type.propertyBoundary)) {
        break;
      } else {
        if (tokenizer.currentToken.is(Token.type.colon)) {
          colon = tokenizer.currentToken;
        }

        if (!ruleStart) {
          ruleStart = tokenizer.advance();
          ruleEnd = ruleStart;
        } else {
          ruleEnd = tokenizer.advance();
        }
      }
    }

    // A ruleset never contains or ends with a semi-colon.
    if (tokenizer.currentToken.is(Token.type.propertyBoundary)) {
      let declarationName = tokenizer.slice(
          ruleStart, colon ? colon.previous : ruleEnd);
      // TODO(cdata): is .trim() bad for performance?
      let expressionValue =
          colon && tokenizer.slice(colon.next, ruleEnd).trim();

      if (tokenizer.currentToken.is(Token.type.semicolon)) {
        tokenizer.advance();
      }

      return this.nodeFactory.declaration(
          declarationName,
          expressionValue && this.nodeFactory.expression(expressionValue));
    // This is the case for a mixin-like structure:
    } else if (colon && colon === ruleEnd) {
      let rulelist = this.parseRulelist(tokenizer);

      if (tokenizer.currentToken.is(Token.type.semicolon)) {
        tokenizer.advance();
      }

      return this.nodeFactory.declaration(
          tokenizer.slice(ruleStart, ruleEnd.previous), rulelist);
    // Otherwise, this is a ruleset:
    } else {
      return this.nodeFactory.ruleset(
          tokenizer.slice(ruleStart, ruleEnd),
          this.parseRulelist(tokenizer));
    }
  }
}

export { Parser };
