/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

/* eslint-disable complexity */
const {cssTokenizer, cssTokenizerWithLineColumn} = require("devtools/shared/css/parsing-utils");
const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");

/**
 * Here is what this file (+ css-parsing-utils.js) do.
 *
 * The main objective here is to provide as much suggestions to the user editing
 * a stylesheet in Style Editor. The possible things that can be suggested are:
 *  - CSS property names
 *  - CSS property values
 *  - CSS Selectors
 *  - Some other known CSS keywords
 *
 * Gecko provides a list of both property names and their corresponding values.
 * We take out a list of matching selectors using the Inspector actor's
 * `getSuggestionsForQuery` method. Now the only thing is to parse the CSS being
 * edited by the user, figure out what token or word is being written and last
 * but the most difficult, what is being edited.
 *
 * The file 'css-parsing-utils' helps to convert the CSS into meaningful tokens,
 * each having a certain type associated with it. These tokens help us to figure
 * out the currently edited word and to write a CSS state machine to figure out
 * what the user is currently editing. By that, I mean, whether he is editing a
 * selector or a property or a value, or even fine grained information like an
 * id in the selector.
 *
 * The `resolveState` method iterated over the tokens spitted out by the
 * tokenizer, using switch cases, follows a state machine logic and finally
 * figures out these informations:
 *  - The state of the CSS at the cursor (one out of CSS_STATES)
 *  - The current token that is being edited `cmpleting`
 *  - If the state is "selector", the selector state (one of SELECTOR_STATES)
 *  - If the state is "selector", the current selector till the cursor
 *  - If the state is "value", the corresponding property name
 *
 * In case of "value" and "property" states, we simply use the information
 * provided by Gecko to filter out the possible suggestions.
 * For "selector" state, we request the Inspector actor to query the page DOM
 * and filter out the possible suggestions.
 * For "media" and "keyframes" state, the only possible suggestions for now are
 * "media" and "keyframes" respectively, although "media" can have suggestions
 * like "max-width", "orientation" etc. Similarly "value" state can also have
 * much better logical suggestions if we fine grain identify a sub state just
 * like we do for the "selector" state.
 */

// Autocompletion types.

/* eslint-disable no-inline-comments */
const CSS_STATES = {
  "null": "null",
  property: "property",    // foo { bar|: … }
  value: "value",          // foo {bar: baz|}
  selector: "selector",    // f| {bar: baz}
  media: "media",          // @med| , or , @media scr| { }
  keyframes: "keyframes",  // @keyf|
  frame: "frame",          // @keyframs foobar { t|
};

const SELECTOR_STATES = {
  "null": "null",
  id: "id",                // #f|
  class: "class",          // #foo.b|
  tag: "tag",              // fo|
  pseudo: "pseudo",        // foo:|
  attribute: "attribute",  // foo[b|
  value: "value",          // foo[bar=b|
};
/* eslint-enable no-inline-comments */

/**
 * Constructor for the autocompletion object.
 *
 * @param options {Object} An options object containing the following options:
 *        - walker {Object} The object used for query selecting from the current
 *                 target's DOM.
 *        - maxEntries {Number} Maximum selectors suggestions to display.
 *        - cssProperties {Object} The database of CSS properties.
 */
function CSSCompleter(options = {}) {
  this.walker = options.walker;
  this.maxEntries = options.maxEntries || 15;
  // If no css properties database is passed in, default to the client list.
  this.cssProperties = options.cssProperties || getClientCssProperties();

  this.propertyNames = this.cssProperties.getNames().sort();

  // Array containing the [line, ch, scopeStack] for the locations where the
  // CSS state is "null"
  this.nullStates = [];
}

CSSCompleter.prototype = {

  /**
   * Returns a list of suggestions based on the caret position.
   *
   * @param source {String} String of the source code.
   * @param caret {Object} Cursor location with line and ch properties.
   *
   * @returns [{object}] A sorted list of objects containing the following
   *          peroperties:
   *          - label {String} Full keyword for the suggestion
   *          - preLabel {String} Already entered part of the label
   */
  complete: function (source, caret) {
    // Getting the context from the caret position.
    if (!this.resolveState(source, caret)) {
      // We couldn't resolve the context, we won't be able to complete.
      return Promise.resolve([]);
    }

    // Properly suggest based on the state.
    switch (this.state) {
      case CSS_STATES.property:
        return this.completeProperties(this.completing);

      case CSS_STATES.value:
        return this.completeValues(this.propertyName, this.completing);

      case CSS_STATES.selector:
        return this.suggestSelectors();

      case CSS_STATES.media:
      case CSS_STATES.keyframes:
        if ("media".startsWith(this.completing)) {
          return Promise.resolve([{
            label: "media",
            preLabel: this.completing,
            text: "media"
          }]);
        } else if ("keyframes".startsWith(this.completing)) {
          return Promise.resolve([{
            label: "keyframes",
            preLabel: this.completing,
            text: "keyframes"
          }]);
        }
    }
    return Promise.resolve([]);
  },

  /**
   * Resolves the state of CSS at the cursor location. This method implements a
   * custom written CSS state machine. The various switch statements provide the
   * transition rules for the state. It also finds out various informatino about
   * the nearby CSS like the property name being completed, the complete
   * selector, etc.
   *
   * @param source {String} String of the source code.
   * @param caret {Object} Cursor location with line and ch properties.
   *
   * @returns CSS_STATE
   *          One of CSS_STATE enum or null if the state cannot be resolved.
   */
  resolveState: function (source, {line, ch}) {
    // Function to return the last element of an array
    let peek = arr => arr[arr.length - 1];
    // _state can be one of CSS_STATES;
    let _state = CSS_STATES.null;
    let selector = "";
    let selectorState = SELECTOR_STATES.null;
    let propertyName = null;
    let scopeStack = [];
    let selectors = [];

    // Fetch the closest null state line, ch from cached null state locations
    let matchedStateIndex = this.findNearestNullState(line);
    if (matchedStateIndex > -1) {
      let state = this.nullStates[matchedStateIndex];
      line -= state[0];
      if (line == 0) {
        ch -= state[1];
      }
      source = source.split("\n").slice(state[0]);
      source[0] = source[0].slice(state[1]);
      source = source.join("\n");
      scopeStack = [...state[2]];
      this.nullStates.length = matchedStateIndex + 1;
    } else {
      this.nullStates = [];
    }
    let tokens = cssTokenizerWithLineColumn(source);
    let tokIndex = tokens.length - 1;
    if (tokIndex >= 0 &&
        (tokens[tokIndex].loc.end.line < line ||
         (tokens[tokIndex].loc.end.line === line &&
          tokens[tokIndex].loc.end.column < ch))) {
      // If the last token ends before the cursor location, we didn't
      // tokenize it correctly.  This special case can happen if the
      // final token is a comment.
      return null;
    }

    let cursor = 0;
    // This will maintain a stack of paired elements like { & }, @m & }, : & ;
    // etc
    let token = null;
    let selectorBeforeNot = null;
    while (cursor <= tokIndex && (token = tokens[cursor++])) {
      switch (_state) {
        case CSS_STATES.property:
          // From CSS_STATES.property, we can either go to CSS_STATES.value
          // state when we hit the first ':' or CSS_STATES.selector if "}" is
          // reached.
          if (token.tokenType === "symbol") {
            switch (token.text) {
              case ":":
                scopeStack.push(":");
                if (tokens[cursor - 2].tokenType != "whitespace") {
                  propertyName = tokens[cursor - 2].text;
                } else {
                  propertyName = tokens[cursor - 3].text;
                }
                _state = CSS_STATES.value;
                break;

              case "}":
                if (/[{f]/.test(peek(scopeStack))) {
                  let popped = scopeStack.pop();
                  if (popped == "f") {
                    _state = CSS_STATES.frame;
                  } else {
                    selector = "";
                    selectors = [];
                    _state = CSS_STATES.null;
                  }
                }
                break;
            }
          }
          break;

        case CSS_STATES.value:
          // From CSS_STATES.value, we can go to one of CSS_STATES.property,
          // CSS_STATES.frame, CSS_STATES.selector and CSS_STATES.null
          if (token.tokenType === "symbol") {
            switch (token.text) {
              case ";":
                if (/[:]/.test(peek(scopeStack))) {
                  scopeStack.pop();
                  _state = CSS_STATES.property;
                }
                break;

              case "}":
                if (peek(scopeStack) == ":") {
                  scopeStack.pop();
                }

                if (/[{f]/.test(peek(scopeStack))) {
                  let popped = scopeStack.pop();
                  if (popped == "f") {
                    _state = CSS_STATES.frame;
                  } else {
                    selector = "";
                    selectors = [];
                    _state = CSS_STATES.null;
                  }
                }
                break;
            }
          }
          break;

        case CSS_STATES.selector:
          // From CSS_STATES.selector, we can only go to CSS_STATES.property
          // when we hit "{"
          if (token.tokenType === "symbol" && token.text == "{") {
            scopeStack.push("{");
            _state = CSS_STATES.property;
            selectors.push(selector);
            selector = "";
            break;
          }

          switch (selectorState) {
            case SELECTOR_STATES.id:
            case SELECTOR_STATES.class:
            case SELECTOR_STATES.tag:
              switch (token.tokenType) {
                case "hash":
                case "id":
                  selectorState = SELECTOR_STATES.id;
                  selector += "#" + token.text;
                  break;

                case "symbol":
                  if (token.text == ".") {
                    selectorState = SELECTOR_STATES.class;
                    selector += ".";
                    if (cursor <= tokIndex &&
                        tokens[cursor].tokenType == "ident") {
                      token = tokens[cursor++];
                      selector += token.text;
                    }
                  } else if (token.text == "#") {
                    selectorState = SELECTOR_STATES.id;
                    selector += "#";
                  } else if (/[>~+]/.test(token.text)) {
                    selectorState = SELECTOR_STATES.null;
                    selector += token.text;
                  } else if (token.text == ",") {
                    selectorState = SELECTOR_STATES.null;
                    selectors.push(selector);
                    selector = "";
                  } else if (token.text == ":") {
                    selectorState = SELECTOR_STATES.pseudo;
                    selector += ":";
                    if (cursor > tokIndex) {
                      break;
                    }

                    token = tokens[cursor++];
                    switch (token.tokenType) {
                      case "function":
                        if (token.text == "not") {
                          selectorBeforeNot = selector;
                          selector = "";
                          scopeStack.push("(");
                        } else {
                          selector += token.text + "(";
                        }
                        selectorState = SELECTOR_STATES.null;
                        break;

                      case "ident":
                        selector += token.text;
                        break;
                    }
                  } else if (token.text == "[") {
                    selectorState = SELECTOR_STATES.attribute;
                    scopeStack.push("[");
                    selector += "[";
                  } else if (token.text == ")") {
                    if (peek(scopeStack) == "(") {
                      scopeStack.pop();
                      selector = selectorBeforeNot + "not(" + selector + ")";
                      selectorBeforeNot = null;
                    } else {
                      selector += ")";
                    }
                    selectorState = SELECTOR_STATES.null;
                  }
                  break;

                case "whitespace":
                  selectorState = SELECTOR_STATES.null;
                  selector && (selector += " ");
                  break;
              }
              break;

            case SELECTOR_STATES.null:
              // From SELECTOR_STATES.null state, we can go to one of
              // SELECTOR_STATES.id, SELECTOR_STATES.class or
              // SELECTOR_STATES.tag
              switch (token.tokenType) {
                case "hash":
                case "id":
                  selectorState = SELECTOR_STATES.id;
                  selector += "#" + token.text;
                  break;

                case "ident":
                  selectorState = SELECTOR_STATES.tag;
                  selector += token.text;
                  break;

                case "symbol":
                  if (token.text == ".") {
                    selectorState = SELECTOR_STATES.class;
                    selector += ".";
                    if (cursor <= tokIndex &&
                        tokens[cursor].tokenType == "ident") {
                      token = tokens[cursor++];
                      selector += token.text;
                    }
                  } else if (token.text == "#") {
                    selectorState = SELECTOR_STATES.id;
                    selector += "#";
                  } else if (token.text == "*") {
                    selectorState = SELECTOR_STATES.tag;
                    selector += "*";
                  } else if (/[>~+]/.test(token.text)) {
                    selector += token.text;
                  } else if (token.text == ",") {
                    selectorState = SELECTOR_STATES.null;
                    selectors.push(selector);
                    selector = "";
                  } else if (token.text == ":") {
                    selectorState = SELECTOR_STATES.pseudo;
                    selector += ":";
                    if (cursor > tokIndex) {
                      break;
                    }

                    token = tokens[cursor++];
                    switch (token.tokenType) {
                      case "function":
                        if (token.text == "not") {
                          selectorBeforeNot = selector;
                          selector = "";
                          scopeStack.push("(");
                        } else {
                          selector += token.text + "(";
                        }
                        selectorState = SELECTOR_STATES.null;
                        break;

                      case "ident":
                        selector += token.text;
                        break;
                    }
                  } else if (token.text == "[") {
                    selectorState = SELECTOR_STATES.attribute;
                    scopeStack.push("[");
                    selector += "[";
                  } else if (token.text == ")") {
                    if (peek(scopeStack) == "(") {
                      scopeStack.pop();
                      selector = selectorBeforeNot + "not(" + selector + ")";
                      selectorBeforeNot = null;
                    } else {
                      selector += ")";
                    }
                    selectorState = SELECTOR_STATES.null;
                  }
                  break;

                case "whitespace":
                  selector && (selector += " ");
                  break;
              }
              break;

            case SELECTOR_STATES.pseudo:
              switch (token.tokenType) {
                case "symbol":
                  if (/[>~+]/.test(token.text)) {
                    selectorState = SELECTOR_STATES.null;
                    selector += token.text;
                  } else if (token.text == ",") {
                    selectorState = SELECTOR_STATES.null;
                    selectors.push(selector);
                    selector = "";
                  } else if (token.text == ":") {
                    selectorState = SELECTOR_STATES.pseudo;
                    selector += ":";
                    if (cursor > tokIndex) {
                      break;
                    }

                    token = tokens[cursor++];
                    switch (token.tokenType) {
                      case "function":
                        if (token.text == "not") {
                          selectorBeforeNot = selector;
                          selector = "";
                          scopeStack.push("(");
                        } else {
                          selector += token.text + "(";
                        }
                        selectorState = SELECTOR_STATES.null;
                        break;

                      case "ident":
                        selector += token.text;
                        break;
                    }
                  } else if (token.text == "[") {
                    selectorState = SELECTOR_STATES.attribute;
                    scopeStack.push("[");
                    selector += "[";
                  }
                  break;

                case "whitespace":
                  selectorState = SELECTOR_STATES.null;
                  selector && (selector += " ");
                  break;
              }
              break;

            case SELECTOR_STATES.attribute:
              switch (token.tokenType) {
                case "symbol":
                  if (/[~|^$*]/.test(token.text)) {
                    selector += token.text;
                    token = tokens[cursor++];
                  } else if (token.text == "=") {
                    selectorState = SELECTOR_STATES.value;
                    selector += token.text;
                  } else if (token.text == "]") {
                    if (peek(scopeStack) == "[") {
                      scopeStack.pop();
                    }

                    selectorState = SELECTOR_STATES.null;
                    selector += "]";
                  }
                  break;

                case "ident":
                case "string":
                  selector += token.text;
                  break;

                case "whitespace":
                  selector && (selector += " ");
                  break;
              }
              break;

            case SELECTOR_STATES.value:
              switch (token.tokenType) {
                case "string":
                case "ident":
                  selector += token.text;
                  break;

                case "symbol":
                  if (token.text == "]") {
                    if (peek(scopeStack) == "[") {
                      scopeStack.pop();
                    }

                    selectorState = SELECTOR_STATES.null;
                    selector += "]";
                  }
                  break;

                case "whitespace":
                  selector && (selector += " ");
                  break;
              }
              break;
          }
          break;

        case CSS_STATES.null:
          // From CSS_STATES.null state, we can go to either CSS_STATES.media or
          // CSS_STATES.selector.
          switch (token.tokenType) {
            case "hash":
            case "id":
              selectorState = SELECTOR_STATES.id;
              selector = "#" + token.text;
              _state = CSS_STATES.selector;
              break;

            case "ident":
              selectorState = SELECTOR_STATES.tag;
              selector = token.text;
              _state = CSS_STATES.selector;
              break;

            case "symbol":
              if (token.text == ".") {
                selectorState = SELECTOR_STATES.class;
                selector = ".";
                _state = CSS_STATES.selector;
                if (cursor <= tokIndex &&
                    tokens[cursor].tokenType == "ident") {
                  token = tokens[cursor++];
                  selector += token.text;
                }
              } else if (token.text == "#") {
                selectorState = SELECTOR_STATES.id;
                selector = "#";
                _state = CSS_STATES.selector;
              } else if (token.text == "*") {
                selectorState = SELECTOR_STATES.tag;
                selector = "*";
                _state = CSS_STATES.selector;
              } else if (token.text == ":") {
                _state = CSS_STATES.selector;
                selectorState = SELECTOR_STATES.pseudo;
                selector += ":";
                if (cursor > tokIndex) {
                  break;
                }

                token = tokens[cursor++];
                switch (token.tokenType) {
                  case "function":
                    if (token.text == "not") {
                      selectorBeforeNot = selector;
                      selector = "";
                      scopeStack.push("(");
                    } else {
                      selector += token.text + "(";
                    }
                    selectorState = SELECTOR_STATES.null;
                    break;

                  case "ident":
                    selector += token.text;
                    break;
                }
              } else if (token.text == "[") {
                _state = CSS_STATES.selector;
                selectorState = SELECTOR_STATES.attribute;
                scopeStack.push("[");
                selector += "[";
              } else if (token.text == "}") {
                if (peek(scopeStack) == "@m") {
                  scopeStack.pop();
                }
              }
              break;

            case "at":
              _state = token.text.startsWith("m") ? CSS_STATES.media
                                                   : CSS_STATES.keyframes;
              break;
          }
          break;

        case CSS_STATES.media:
          // From CSS_STATES.media, we can only go to CSS_STATES.null state when
          // we hit the first '{'
          if (token.tokenType == "symbol" && token.text == "{") {
            scopeStack.push("@m");
            _state = CSS_STATES.null;
          }
          break;

        case CSS_STATES.keyframes:
          // From CSS_STATES.keyframes, we can only go to CSS_STATES.frame state
          // when we hit the first '{'
          if (token.tokenType == "symbol" && token.text == "{") {
            scopeStack.push("@k");
            _state = CSS_STATES.frame;
          }
          break;

        case CSS_STATES.frame:
          // From CSS_STATES.frame, we can either go to CSS_STATES.property
          // state when we hit the first '{' or to CSS_STATES.selector when we
          // hit '}'
          if (token.tokenType == "symbol") {
            if (token.text == "{") {
              scopeStack.push("f");
              _state = CSS_STATES.property;
            } else if (token.text == "}") {
              if (peek(scopeStack) == "@k") {
                scopeStack.pop();
              }

              _state = CSS_STATES.null;
            }
          }
          break;
      }
      if (_state == CSS_STATES.null) {
        if (this.nullStates.length == 0) {
          this.nullStates.push([token.loc.end.line, token.loc.end.column,
                                [...scopeStack]]);
          continue;
        }
        let tokenLine = token.loc.end.line;
        let tokenCh = token.loc.end.column;
        if (tokenLine == 0) {
          continue;
        }
        if (matchedStateIndex > -1) {
          tokenLine += this.nullStates[matchedStateIndex][0];
        }
        this.nullStates.push([tokenLine, tokenCh, [...scopeStack]]);
      }
    }
    this.state = _state;
    this.propertyName = _state == CSS_STATES.value ? propertyName : null;
    this.selectorState = _state == CSS_STATES.selector ? selectorState : null;
    this.selectorBeforeNot = selectorBeforeNot == null ?
                             null : selectorBeforeNot;
    if (token) {
      selector = selector.slice(0, selector.length + token.loc.end.column - ch);
      this.selector = selector;
    } else {
      this.selector = "";
    }
    this.selectors = selectors;

    if (token && token.tokenType != "whitespace") {
      let text;
      if (token.tokenType == "dimension" || !token.text) {
        text = source.substring(token.startOffset, token.endOffset);
      } else {
        text = token.text;
      }
      this.completing = (text.slice(0, ch - token.loc.start.column)
                         .replace(/^[.#]$/, ""));
    } else {
      this.completing = "";
    }
    // Special case the situation when the user just entered ":" after typing a
    // property name.
    if (this.completing == ":" && _state == CSS_STATES.value) {
      this.completing = "";
    }

    // Special check for !important; case.
    if (token && tokens[cursor - 2] && tokens[cursor - 2].text == "!" &&
        this.completing == "important".slice(0, this.completing.length)) {
      this.completing = "!" + this.completing;
    }
    return _state;
  },

  /**
   * Queries the DOM Walker actor for suggestions regarding the selector being
   * completed
   */
  suggestSelectors: function () {
    let walker = this.walker;
    if (!walker) {
      return Promise.resolve([]);
    }

    let query = this.selector;
    // Even though the selector matched atleast one node, there is still
    // possibility of suggestions.
    switch (this.selectorState) {
      case SELECTOR_STATES.null:
        if (this.completing === ",") {
          return Promise.resolve([]);
        }

        query += "*";
        break;

      case SELECTOR_STATES.tag:
        query = query.slice(0, query.length - this.completing.length);
        break;

      case SELECTOR_STATES.id:
      case SELECTOR_STATES.class:
      case SELECTOR_STATES.pseudo:
        if (/^[.:#]$/.test(this.completing)) {
          query = query.slice(0, query.length - this.completing.length);
          this.completing = "";
        } else {
          query = query.slice(0, query.length - this.completing.length - 1);
        }
        break;
    }

    if (/[\s+>~]$/.test(query) &&
        this.selectorState != SELECTOR_STATES.attribute &&
        this.selectorState != SELECTOR_STATES.value) {
      query += "*";
    }

    // Set the values that this request was supposed to suggest to.
    this._currentQuery = query;
    return walker.getSuggestionsForQuery(query, this.completing,
                                         this.selectorState)
                 .then(result => this.prepareSelectorResults(result));
  },

 /**
  * Prepares the selector suggestions returned by the walker actor.
  */
  prepareSelectorResults: function (result) {
    if (this._currentQuery != result.query) {
      return [];
    }

    result = result.suggestions;
    let query = this.selector;
    let completion = [];
    for (let [value, count, state] of result) {
      switch (this.selectorState) {
        case SELECTOR_STATES.id:
        case SELECTOR_STATES.class:
        case SELECTOR_STATES.pseudo:
          if (/^[.:#]$/.test(this.completing)) {
            value = query.slice(0, query.length - this.completing.length) +
                       value;
          } else {
            value = query.slice(0, query.length - this.completing.length - 1) +
                       value;
          }
          break;

        case SELECTOR_STATES.tag:
          value = query.slice(0, query.length - this.completing.length) +
                    value;
          break;

        case SELECTOR_STATES.null:
          value = query + value;
          break;

        default:
          value = query.slice(0, query.length - this.completing.length) +
                    value;
      }

      let item = {
        label: value,
        preLabel: query,
        text: value,
        score: count
      };

      // In case the query's state is tag and the item's state is id or class
      // adjust the preLabel
      if (this.selectorState === SELECTOR_STATES.tag &&
          state === SELECTOR_STATES.class) {
        item.preLabel = "." + item.preLabel;
      }
      if (this.selectorState === SELECTOR_STATES.tag &&
          state === SELECTOR_STATES.id) {
        item.preLabel = "#" + item.preLabel;
      }

      completion.push(item);

      if (completion.length > this.maxEntries - 1) {
        break;
      }
    }
    return completion;
  },

  /**
   * Returns CSS property name suggestions based on the input.
   *
   * @param startProp {String} Initial part of the property being completed.
   */
  completeProperties: function (startProp) {
    let finalList = [];
    if (!startProp) {
      return Promise.resolve(finalList);
    }

    let length = this.propertyNames.length;
    let i = 0, count = 0;
    for (; i < length && count < this.maxEntries; i++) {
      if (this.propertyNames[i].startsWith(startProp)) {
        count++;
        let propName = this.propertyNames[i];
        finalList.push({
          preLabel: startProp,
          label: propName,
          text: propName + ": "
        });
      } else if (this.propertyNames[i] > startProp) {
        // We have crossed all possible matches alphabetically.
        break;
      }
    }
    return Promise.resolve(finalList);
  },

  /**
   * Returns CSS value suggestions based on the corresponding property.
   *
   * @param propName {String} The property to which the value being completed
   *        belongs.
   * @param startValue {String} Initial part of the value being completed.
   */
  completeValues: function (propName, startValue) {
    let finalList = [];
    let list = ["!important;", ...this.cssProperties.getValues(propName)];
    // If there is no character being completed, we are showing an initial list
    // of possible values. Skipping '!important' in this case.
    if (!startValue) {
      list.splice(0, 1);
    }

    let length = list.length;
    let i = 0, count = 0;
    for (; i < length && count < this.maxEntries; i++) {
      if (list[i].startsWith(startValue)) {
        count++;
        let value = list[i];
        finalList.push({
          preLabel: startValue,
          label: value,
          text: value
        });
      } else if (list[i] > startValue) {
        // We have crossed all possible matches alphabetically.
        break;
      }
    }
    return Promise.resolve(finalList);
  },

  /**
   * A biased binary search in a sorted array where the middle element is
   * calculated based on the values at the lower and the upper index in each
   * iteration.
   *
   * This method returns the index of the closest null state from the passed
   * `line` argument. Once we have the closest null state, we can start applying
   * the state machine logic from that location instead of the absolute starting
   * of the CSS source. This speeds up the tokenizing and the state machine a
   * lot while using autocompletion at high line numbers in a CSS source.
   */
  findNearestNullState: function (line) {
    let arr = this.nullStates;
    let high = arr.length - 1;
    let low = 0;
    let target = 0;

    if (high < 0) {
      return -1;
    }
    if (arr[high][0] <= line) {
      return high;
    }
    if (arr[low][0] > line) {
      return -1;
    }

    while (high > low) {
      if (arr[low][0] <= line && arr[low [0] + 1] > line) {
        return low;
      }
      if (arr[high][0] > line && arr[high - 1][0] <= line) {
        return high - 1;
      }

      target = (((line - arr[low][0]) / (arr[high][0] - arr[low][0])) *
                (high - low)) | 0;

      if (arr[target][0] <= line && arr[target + 1][0] > line) {
        return target;
      } else if (line > arr[target][0]) {
        low = target + 1;
        high--;
      } else {
        high = target - 1;
        low++;
      }
    }

    return -1;
  },

  /**
   * Invalidates the state cache for and above the line.
   */
  invalidateCache: function (line) {
    this.nullStates.length = this.findNearestNullState(line) + 1;
  },

  /**
   * Get the state information about a token surrounding the {line, ch} position
   *
   * @param {string} source
   *        The complete source of the CSS file. Unlike resolve state method,
   *        this method requires the full source.
   * @param {object} caret
   *        The line, ch position of the caret.
   *
   * @returns {object}
   *          An object containing the state of token covered by the caret.
   *          The object has following properties when the the state is
   *          "selector", "value" or "property", null otherwise:
   *           - state {string} one of CSS_STATES - "selector", "value" etc.
   *           - selector {string} The selector at the caret when `state` is
   *                      selector. OR
   *           - selectors {[string]} Array of selector strings in case when
   *                       `state` is "value" or "property"
   *           - propertyName {string} The property name at the current caret or
   *                          the property name corresponding to the value at
   *                          the caret.
   *           - value {string} The css value at the current caret.
   *           - loc {object} An object containing the starting and the ending
   *                 caret position of the whole selector, value or property.
   *                  - { start: {line, ch}, end: {line, ch}}
   */
  getInfoAt: function (source, caret) {
    // Limits the input source till the {line, ch} caret position
    function limit(sourceArg, {line, ch}) {
      line++;
      let list = sourceArg.split("\n");
      if (list.length < line) {
        return sourceArg;
      }
      if (line == 1) {
        return list[0].slice(0, ch);
      }
      return [...list.slice(0, line - 1),
              list[line - 1].slice(0, ch)].join("\n");
    }

    // Get the state at the given line, ch
    let state = this.resolveState(limit(source, caret), caret);
    let propertyName = this.propertyName;
    let {line, ch} = caret;
    let sourceArray = source.split("\n");
    let limitedSource = limit(source, caret);

    /**
     * Method to traverse forwards from the caret location to figure out the
     * ending point of a selector or css value.
     *
     * @param {function} check
     *        A method which takes the current state as an input and determines
     *        whether the state changed or not.
     */
    let traverseForward = check => {
      let location;
      // Backward loop to determine the beginning location of the selector.
      do {
        let lineText = sourceArray[line];
        if (line == caret.line) {
          lineText = lineText.substring(caret.ch);
        }

        let prevToken = undefined;
        let tokens = cssTokenizer(lineText);
        let found = false;
        let ech = line == caret.line ? caret.ch : 0;
        for (let token of tokens) {
          // If the line is completely spaces, handle it differently
          if (lineText.trim() == "") {
            limitedSource += lineText;
          } else {
            limitedSource += sourceArray[line]
                              .substring(ech + token.startOffset,
                                         ech + token.endOffset);
          }

          // Whitespace cannot change state.
          if (token.tokenType == "whitespace") {
            prevToken = token;
            continue;
          }

          let forwState = this.resolveState(limitedSource, {
            line: line,
            ch: token.endOffset + ech
          });
          if (check(forwState)) {
            if (prevToken && prevToken.tokenType == "whitespace") {
              token = prevToken;
            }
            location = {
              line: line,
              ch: token.startOffset + ech
            };
            found = true;
            break;
          }
          prevToken = token;
        }
        limitedSource += "\n";
        if (found) {
          break;
        }
      } while (line++ < sourceArray.length);
      return location;
    };

    /**
     * Method to traverse backwards from the caret location to figure out the
     * starting point of a selector or css value.
     *
     * @param {function} check
     *        A method which takes the current state as an input and determines
     *        whether the state changed or not.
     * @param {boolean} isValue
     *        true if the traversal is being done for a css value state.
     */
    let traverseBackwards = (check, isValue) => {
      let location;
      // Backward loop to determine the beginning location of the selector.
      do {
        let lineText = sourceArray[line];
        if (line == caret.line) {
          lineText = lineText.substring(0, caret.ch);
        }

        let tokens = Array.from(cssTokenizer(lineText));
        let found = false;
        for (let i = tokens.length - 1; i >= 0; i--) {
          let token = tokens[i];
          // If the line is completely spaces, handle it differently
          if (lineText.trim() == "") {
            limitedSource = limitedSource.slice(0, -1 * lineText.length);
          } else {
            let length = token.endOffset - token.startOffset;
            limitedSource = limitedSource.slice(0, -1 * length);
          }

          // Whitespace cannot change state.
          if (token.tokenType == "whitespace") {
            continue;
          }

          let backState = this.resolveState(limitedSource, {
            line: line,
            ch: token.startOffset
          });
          if (check(backState)) {
            if (tokens[i + 1] && tokens[i + 1].tokenType == "whitespace") {
              token = tokens[i + 1];
            }
            location = {
              line: line,
              ch: isValue ? token.endOffset : token.startOffset
            };
            found = true;
            break;
          }
        }
        limitedSource = limitedSource.slice(0, -1);
        if (found) {
          break;
        }
      } while (line-- >= 0);
      return location;
    };

    if (state == CSS_STATES.selector) {
      // For selector state, the ending and starting point of the selector is
      // either when the state changes or the selector becomes empty and a
      // single selector can span multiple lines.
      // Backward loop to determine the beginning location of the selector.
      let start = traverseBackwards(backState => {
        return (backState != CSS_STATES.selector ||
               (this.selector == "" && this.selectorBeforeNot == null));
      });

      line = caret.line;
      limitedSource = limit(source, caret);
      // Forward loop to determine the ending location of the selector.
      let end = traverseForward(forwState => {
        return (forwState != CSS_STATES.selector ||
               (this.selector == "" && this.selectorBeforeNot == null));
      });

      // Since we have start and end positions, figure out the whole selector.
      let selector = source.split("\n").slice(start.line, end.line + 1);
      selector[selector.length - 1] =
        selector[selector.length - 1].substring(0, end.ch);
      selector[0] = selector[0].substring(start.ch);
      selector = selector.join("\n");
      return {
        state: state,
        selector: selector,
        loc: {
          start: start,
          end: end
        }
      };
    } else if (state == CSS_STATES.property) {
      // A property can only be a single word and thus very easy to calculate.
      let tokens = cssTokenizer(sourceArray[line]);
      for (let token of tokens) {
        // Note that, because we're tokenizing a single line, the
        // token's offset is also the column number.
        if (token.startOffset <= ch && token.endOffset >= ch) {
          return {
            state: state,
            propertyName: token.text,
            selectors: this.selectors,
            loc: {
              start: {
                line: line,
                ch: token.startOffset
              },
              end: {
                line: line,
                ch: token.endOffset
              }
            }
          };
        }
      }
    } else if (state == CSS_STATES.value) {
      // CSS value can be multiline too, so we go forward and backwards to
      // determine the bounds of the value at caret
      let start = traverseBackwards(backState => backState != CSS_STATES.value, true);

      line = caret.line;
      limitedSource = limit(source, caret);
      let end = traverseForward(forwState => forwState != CSS_STATES.value);

      let value = source.split("\n").slice(start.line, end.line + 1);
      value[value.length - 1] = value[value.length - 1].substring(0, end.ch);
      value[0] = value[0].substring(start.ch);
      value = value.join("\n");
      return {
        state: state,
        propertyName: propertyName,
        selectors: this.selectors,
        value: value,
        loc: {
          start: start,
          end: end
        }
      };
    }
    return null;
  }
};

module.exports = CSSCompleter;
