// Credits: https://github.com/reworkcss/css/tree/master/lib/parse
// Licensed under MIT
// Forge: modified while{} loop declarations on some lines to reduce errors on logging

// http://www.w3.org/TR/CSS21/grammar.html
// https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027
const commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g;

export function parse(css, options) {
  options = options || {};

  /**
   * Positional.
   */

  var lineno = 1;
  var column = 1;

  /**
   * Update lineno and column based on `str`.
   */

  function updatePosition(str) {
    var lines = str.match(/\n/g);
    if (lines) lineno += lines.length;
    var i = str.lastIndexOf("\n");
    column = ~i ? str.length - i : column + str.length;
  }

  /**
   * Mark position and patch `node.position`.
   */

  function position() {
    var start = { line: lineno, column: column };
    return function (node) {
      node.position = new Position(start);
      whitespace();
      return node;
    };
  }

  /**
   * Store position information for a node
   */

  class Position {
    /**
     * Non-enumerable source string
     */
    content = css;

    constructor(start) {
      this.start = start;
      this.end = { line: lineno, column: column };
      this.source = options.source;
    }
  }

  /**
   * Error `msg`.
   */

  var errorsList = [];

  function error(msg) {
    var err = new Error(options.source + ":" + lineno + ":" + column + ": " + msg);
    err.reason = msg;
    err.filename = options.source;
    err.line = lineno;
    err.column = column;
    err.source = css;

    if (options.silent) {
      errorsList.push(err);
    } else {
      throw err;
    }
  }

  /**
   * Parse stylesheet.
   */

  function stylesheet() {
    var rulesList = rules();

    return {
      type: "stylesheet",
      stylesheet: {
        source: options.source,
        rules: rulesList,
        parsingErrors: errorsList,
      },
    };
  }

  /**
   * Opening brace.
   */

  function open() {
    return match(/^{\s*/);
  }

  /**
   * Closing brace.
   */

  function close() {
    return match(/^}/);
  }

  /**
   * Parse ruleset.
   */

  function rules() {
    var node;
    var rules = [];
    whitespace();
    comments(rules);
    while (css.length && css.charAt(0) != "}" && (node = atrule() || rule())) {
      if (node !== false) {
        rules.push(node);
        comments(rules);
      }
    }
    return rules;
  }

  /**
   * Match `re` and return captures.
   */

  function match(re) {
    var m = re.exec(css);
    if (!m) return;
    var str = m[0];
    updatePosition(str);
    css = css.slice(str.length);
    return m;
  }

  /**
   * Parse whitespace.
   */

  function whitespace() {
    match(/^\s*/);
  }

  /**
   * Parse comments;
   */

  function comments(rules) {
    rules = rules || [];
    for (var c; (c = comment()); ) {
      if (c !== false) {
        rules.push(c);
      }
    }
    return rules;
  }

  /**
   * Parse comment.
   */

  function comment() {
    var pos = position();
    if ("/" != css.charAt(0) || "*" != css.charAt(1)) return;

    var i = 2;
    while ("" != css.charAt(i) && ("*" != css.charAt(i) || "/" != css.charAt(i + 1))) ++i;
    i += 2;

    if ("" === css.charAt(i - 1)) {
      return error("End of comment missing");
    }

    var str = css.slice(2, i - 2);
    column += 2;
    updatePosition(str);
    css = css.slice(i);
    column += 2;

    return pos({
      type: "comment",
      comment: str,
    });
  }

  /**
   * Parse selector.
   */

  function selector() {
    var m = match(/^([^{]+)/);
    if (!m) return;
    /* @fix Remove all comments from selectors
     * http://ostermiller.org/findcomment.html */
    return trim(m[0])
      .replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, "")
      .replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, function (m) {
        return m.replace(/,/g, "\u200C");
      })
      .split(/\s*(?![^(]*\)),\s*/)
      .map(function (s) {
        return s.replace(/\u200C/g, ",");
      });
  }

  /**
   * Parse declaration.
   */

  function declaration() {
    var pos = position();

    // prop
    var prop = match(/^(\*?[-#\/\*\\\w]+(\[[0-9a-z_-]+\])?)\s*/);
    if (!prop) return;
    prop = trim(prop[0]);

    // :
    if (!match(/^:\s*/)) return error("property missing ':'");

    // val
    var val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/);

    var ret = pos({
      type: "declaration",
      property: prop.replace(commentre, ""),
      value: val ? trim(val[0]).replace(commentre, "") : "",
    });

    // ;
    match(/^[;\s]*/);

    return ret;
  }

  /**
   * Parse declarations.
   */

  function declarations() {
    var decls = [];

    if (!open()) return error("missing '{'");
    comments(decls);

    // declarations
    for (var decl; (decl = declaration()); ) {
      if (decl !== false) {
        decls.push(decl);
        comments(decls);
      }
    }

    if (!close()) return error("missing '}'");
    return decls;
  }

  /**
   * Parse keyframe.
   */

  function keyframe() {
    var vals = [];
    var pos = position();

    for (var m; (m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/)); ) {
      vals.push(m[1]);
      match(/^,\s*/);
    }

    if (!vals.length) return;

    return pos({
      type: "keyframe",
      values: vals,
      declarations: declarations(),
    });
  }

  /**
   * Parse keyframes.
   */

  function atkeyframes() {
    var pos = position();
    var m = match(/^@([-\w]+)?keyframes\s*/);

    if (!m) return;
    var vendor = m[1];

    // identifier
    var m = match(/^([-\w]+)\s*/);
    if (!m) return error("@keyframes missing name");
    var name = m[1];

    if (!open()) return error("@keyframes missing '{'");

    var frames = comments();
    for (var frame; (frame = keyframe()); ) {
      frames.push(frame);
      frames = frames.concat(comments());
    }

    if (!close()) return error("@keyframes missing '}'");

    return pos({
      type: "keyframes",
      name: name,
      vendor: vendor,
      keyframes: frames,
    });
  }

  /**
   * Parse supports.
   */

  function atsupports() {
    var pos = position();
    var m = match(/^@supports *([^{]+)/);

    if (!m) return;
    var supports = trim(m[1]);

    if (!open()) return error("@supports missing '{'");

    var style = comments().concat(rules());

    if (!close()) return error("@supports missing '}'");

    return pos({
      type: "supports",
      supports: supports,
      rules: style,
    });
  }

  /**
   * Parse host.
   */

  function athost() {
    var pos = position();
    var m = match(/^@host\s*/);

    if (!m) return;

    if (!open()) return error("@host missing '{'");

    var style = comments().concat(rules());

    if (!close()) return error("@host missing '}'");

    return pos({
      type: "host",
      rules: style,
    });
  }

  /**
   * Parse media.
   */

  function atmedia() {
    var pos = position();
    var m = match(/^@media *([^{]+)/);

    if (!m) return;
    var media = trim(m[1]);

    if (!open()) return error("@media missing '{'");

    var style = comments().concat(rules());

    if (!close()) return error("@media missing '}'");

    return pos({
      type: "media",
      media: media,
      rules: style,
    });
  }

  /**
   * Parse custom-media.
   */

  function atcustommedia() {
    var pos = position();
    var m = match(/^@custom-media\s+(--[^\s]+)\s*([^{;]+);/);
    if (!m) return;

    return pos({
      type: "custom-media",
      name: trim(m[1]),
      media: trim(m[2]),
    });
  }

  /**
   * Parse paged media.
   */

  function atpage() {
    var pos = position();
    var m = match(/^@page */);
    if (!m) return;

    var sel = selector() || [];

    if (!open()) return error("@page missing '{'");
    var decls = comments();

    // declarations
    for (var decl; (decl = declaration()); ) {
      decls.push(decl);
      decls = decls.concat(comments());
    }

    if (!close()) return error("@page missing '}'");

    return pos({
      type: "page",
      selectors: sel,
      declarations: decls,
    });
  }

  /**
   * Parse document.
   */

  function atdocument() {
    var pos = position();
    var m = match(/^@([-\w]+)?document *([^{]+)/);
    if (!m) return;

    var vendor = trim(m[1]);
    var doc = trim(m[2]);

    if (!open()) return error("@document missing '{'");

    var style = comments().concat(rules());

    if (!close()) return error("@document missing '}'");

    return pos({
      type: "document",
      document: doc,
      vendor: vendor,
      rules: style,
    });
  }

  /**
   * Parse font-face.
   */

  function atfontface() {
    var pos = position();
    var m = match(/^@font-face\s*/);
    if (!m) return;

    if (!open()) return error("@font-face missing '{'");
    var decls = comments();

    // declarations
    for (var decl; (decl = declaration()); ) {
      decls.push(decl);
      decls = decls.concat(comments());
    }

    if (!close()) return error("@font-face missing '}'");

    return pos({
      type: "font-face",
      declarations: decls,
    });
  }

  /**
   * Parse import
   */

  var atimport = _compileAtrule("import");

  /**
   * Parse charset
   */

  var atcharset = _compileAtrule("charset");

  /**
   * Parse namespace
   */

  var atnamespace = _compileAtrule("namespace");

  /**
   * Parse non-block at-rules
   */

  function _compileAtrule(name) {
    var re = new RegExp("^@" + name + "\\s*([^;]+);");
    return function () {
      var pos = position();
      var m = match(re);
      if (!m) return;
      var ret = { type: name };
      ret[name] = m[1].trim();
      return pos(ret);
    };
  }

  /**
   * Parse at rule.
   */

  function atrule() {
    if (css[0] != "@") return;

    return (
      atkeyframes() ||
      atmedia() ||
      atcustommedia() ||
      atsupports() ||
      atimport() ||
      atcharset() ||
      atnamespace() ||
      atdocument() ||
      atpage() ||
      athost() ||
      atfontface()
    );
  }

  /**
   * Parse rule.
   */

  function rule() {
    var pos = position();
    var sel = selector();

    if (!sel) return error("selector missing");
    comments();

    return pos({
      type: "rule",
      selectors: sel,
      declarations: declarations(),
    });
  }

  return addParent(stylesheet());
}

/**
 * Trim `str`.
 */

export function trim(str) {
  return str ? str.replace(/^\s+|\s+$/g, "") : "";
}

/**
 * Adds non-enumerable parent node reference to each node.
 */

export function addParent(obj, parent) {
  var isNode = obj && typeof obj.type === "string";
  var childParent = isNode ? obj : parent;

  for (var k in obj) {
    var value = obj[k];
    if (Array.isArray(value)) {
      value.forEach(function (v) {
        addParent(v, childParent);
      });
    } else if (value && typeof value === "object") {
      addParent(value, childParent);
    }
  }

  if (isNode) {
    Object.defineProperty(obj, "parent", {
      configurable: true,
      writable: true,
      enumerable: false,
      value: parent || null,
    });
  }

  return obj;
}

// Credits: https://github.com/reworkcss/css/blob/master/lib/stringify
// Forge: derived the identity.js module only

export class Compiler {
  constructor(options) {
    options ||= {};
    this.indentation = typeof options.indent === "string" ? options.indent : "  ";
  }

  /**
   * Emit `str`
   */

  emit(str) {
    return str;
  }

  /**
   * Visit `node`.
   */

  visit(node) {
    return this[node.type](node);
  }

  /**
   * Map visit over array of `nodes`, optionally using a `delim`
   */

  mapVisit(nodes, delim) {
    var buf = "";
    delim = delim || "";

    for (var i = 0, length = nodes.length; i < length; i++) {
      buf += this.visit(nodes[i]);
      if (delim && i < length - 1) buf += this.emit(delim);
    }

    return buf;
  }

  /**
   * Compile `node`.
   */

  compile(node) {
    return this.stylesheet(node);
  }

  /**
   * Visit stylesheet node.
   */

  stylesheet(node) {
    return this.mapVisit(node.stylesheet.rules, "\n\n");
  }

  /**
   * Visit comment node.
   */

  comment(node) {
    return this.emit(this.indent() + "/*" + node.comment + "*/", node.position);
  }

  /**
   * Visit import node.
   */

  import(node) {
    return this.emit("@import " + node.import + ";", node.position);
  }

  /**
   * Visit media node.
   */

  media(node) {
    return (
      this.emit("@media " + node.media, node.position) +
      this.emit(" {\n" + this.indent(1)) +
      this.mapVisit(node.rules, "\n\n") +
      this.emit(this.indent(-1) + "\n}")
    );
  }

  /**
   * Visit document node.
   */

  document(node) {
    var doc = "@" + (node.vendor || "") + "document " + node.document;

    return (
      this.emit(doc, node.position) +
      this.emit(" " + " {\n" + this.indent(1)) +
      this.mapVisit(node.rules, "\n\n") +
      this.emit(this.indent(-1) + "\n}")
    );
  }

  /**
   * Visit charset node.
   */
  charset(node) {
    return this.emit("@charset " + node.charset + ";", node.position);
  }

  /**
   * Visit namespace node.
   */
  namespace(node) {
    return this.emit("@namespace " + node.namespace + ";", node.position);
  }

  /**
   * Visit supports node.
   */

  supports(node) {
    return (
      this.emit("@supports " + node.supports, node.position) +
      this.emit(" {\n" + this.indent(1)) +
      this.mapVisit(node.rules, "\n\n") +
      this.emit(this.indent(-1) + "\n}")
    );
  }

  /**
   * Visit keyframes node.
   */

  keyframes(node) {
    return (
      this.emit("@" + (node.vendor || "") + "keyframes " + node.name, node.position) +
      this.emit(" {\n" + this.indent(1)) +
      this.mapVisit(node.keyframes, "\n") +
      this.emit(this.indent(-1) + "}")
    );
  }

  /**
   * Visit keyframe node.
   */

  keyframe(node) {
    var decls = node.declarations;

    return (
      this.emit(this.indent()) +
      this.emit(node.values.join(", "), node.position) +
      this.emit(" {\n" + this.indent(1)) +
      this.mapVisit(decls, "\n") +
      this.emit(this.indent(-1) + "\n" + this.indent() + "}\n")
    );
  }

  /**
   * Visit page node.
   */

  page(node) {
    var sel = node.selectors.length ? node.selectors.join(", ") + " " : "";

    return (
      this.emit("@page " + sel, node.position) +
      this.emit("{\n") +
      this.emit(this.indent(1)) +
      this.mapVisit(node.declarations, "\n") +
      this.emit(this.indent(-1)) +
      this.emit("\n}")
    );
  }

  /**
   * Visit font-face node.
   */

  ["font-face"] = function (node) {
    return (
      this.emit("@font-face ", node.position) +
      this.emit("{\n") +
      this.emit(this.indent(1)) +
      this.mapVisit(node.declarations, "\n") +
      this.emit(this.indent(-1)) +
      this.emit("\n}")
    );
  };

  /**
   * Visit host node.
   */

  host(node) {
    return (
      this.emit("@host", node.position) +
      this.emit(" {\n" + this.indent(1)) +
      this.mapVisit(node.rules, "\n\n") +
      this.emit(this.indent(-1) + "\n}")
    );
  }

  /**
   * Visit custom-media node.
   */

  ["custom-media"] = function (node) {
    return this.emit("@custom-media " + node.name + " " + node.media + ";", node.position);
  };

  /**
   * Visit rule node.
   */

  rule(node) {
    var indent = this.indent();
    var decls = node.declarations;
    if (!decls.length) return "";

    return (
      this.emit(
        node.selectors
          .map(function (s) {
            return indent + s;
          })
          .join(",\n"),
        node.position
      ) +
      this.emit(" {\n") +
      this.emit(this.indent(1)) +
      this.mapVisit(decls, "\n") +
      this.emit(this.indent(-1)) +
      this.emit("\n" + this.indent() + "}")
    );
  }

  /**
   * Visit declaration node.
   */

  declaration(node) {
    return (
      this.emit(this.indent()) +
      this.emit(node.property + ": " + node.value, node.position) +
      this.emit(";")
    );
  }

  /**
   * Increase, decrease or return current indentation.
   */

  indent(level) {
    this.level = this.level || 1;

    if (null != level) {
      this.level += level;
      return "";
    }

    return Array(this.level).join(this.indentation);
  }
}

// Credits: https://github.com/reworkcss/css/tree/master/lib/stringify
// Licensed under MIT
// Forge: removed unused options

/**
 * Stringfy the given AST `node`.
 *
 * @param {Object} node
 * @param {Object} [_options]
 * @return {String}
 * @api public
 */

export function stringify(node, _options) {
  _options ||= {};
  var compiler = new Compiler(_options);
  var code = compiler.compile(node);
  return code;
}
