'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var TOKEN_STYLES = {
  addition: "inserted",
  attributes: "propertyName",
  bold: "strong",
  cite: "keyword",
  code: "monospace",
  definitionList: "list",
  deletion: "deleted",
  div: "punctuation",
  em: "emphasis",
  footnote: "variable",
  footCite: "qualifier",
  header: "heading",
  html: "comment",
  image: "atom",
  italic: "emphasis",
  link: "link",
  linkDefinition: "link",
  list1: "list",
  list2: "list.special",
  list3: "list",
  notextile: "string.special",
  pre: "operator",
  p: "content",
  quote: "bracket",
  span: "quote",
  specialChar: "character",
  strong: "strong",
  sub: "content.special",
  sup: "content.special",
  table: "variableName.special",
  tableHeading: "operator"
};

function startNewLine(stream, state) {
  state.mode = Modes.newLayout;
  state.tableHeading = false;

  if (state.layoutType === "definitionList" && state.spanningLayout &&
      stream.match(RE("definitionListEnd"), false))
    state.spanningLayout = false;
}

function handlePhraseModifier(stream, state, ch) {
  if (ch === "_") {
    if (stream.eat("_"))
      return togglePhraseModifier(stream, state, "italic", /__/, 2);
    else
      return togglePhraseModifier(stream, state, "em", /_/, 1);
  }

  if (ch === "*") {
    if (stream.eat("*")) {
      return togglePhraseModifier(stream, state, "bold", /\*\*/, 2);
    }
    return togglePhraseModifier(stream, state, "strong", /\*/, 1);
  }

  if (ch === "[") {
    if (stream.match(/\d+\]/)) state.footCite = true;
    return tokenStyles(state);
  }

  if (ch === "(") {
    var spec = stream.match(/^(r|tm|c)\)/);
    if (spec)
      return TOKEN_STYLES.specialChar
  }

  if (ch === "<" && stream.match(/(\w+)[^>]+>[^<]+<\/\1>/))
    return TOKEN_STYLES.html

  if (ch === "?" && stream.eat("?"))
    return togglePhraseModifier(stream, state, "cite", /\?\?/, 2);

  if (ch === "=" && stream.eat("="))
    return togglePhraseModifier(stream, state, "notextile", /==/, 2);

  if (ch === "-" && !stream.eat("-"))
    return togglePhraseModifier(stream, state, "deletion", /-/, 1);

  if (ch === "+")
    return togglePhraseModifier(stream, state, "addition", /\+/, 1);

  if (ch === "~")
    return togglePhraseModifier(stream, state, "sub", /~/, 1);

  if (ch === "^")
    return togglePhraseModifier(stream, state, "sup", /\^/, 1);

  if (ch === "%")
    return togglePhraseModifier(stream, state, "span", /%/, 1);

  if (ch === "@")
    return togglePhraseModifier(stream, state, "code", /@/, 1);

  if (ch === "!") {
    var type = togglePhraseModifier(stream, state, "image", /(?:\([^\)]+\))?!/, 1);
    stream.match(/^:\S+/); // optional Url portion
    return type;
  }
  return tokenStyles(state);
}

function togglePhraseModifier(stream, state, phraseModifier, closeRE, openSize) {
  var charBefore = stream.pos > openSize ? stream.string.charAt(stream.pos - openSize - 1) : null;
  var charAfter = stream.peek();
  if (state[phraseModifier]) {
    if ((!charAfter || /\W/.test(charAfter)) && charBefore && /\S/.test(charBefore)) {
      var type = tokenStyles(state);
      state[phraseModifier] = false;
      return type;
    }
  } else if ((!charBefore || /\W/.test(charBefore)) && charAfter && /\S/.test(charAfter) &&
             stream.match(new RegExp("^.*\\S" + closeRE.source + "(?:\\W|$)"), false)) {
    state[phraseModifier] = true;
    state.mode = Modes.attributes;
  }
  return tokenStyles(state);
}
function tokenStyles(state) {
  var disabled = textileDisabled(state);
  if (disabled) return disabled;

  var styles = [];
  if (state.layoutType) styles.push(TOKEN_STYLES[state.layoutType]);

  styles = styles.concat(activeStyles(
    state, "addition", "bold", "cite", "code", "deletion", "em", "footCite",
    "image", "italic", "link", "span", "strong", "sub", "sup", "table", "tableHeading"));

  if (state.layoutType === "header")
    styles.push(TOKEN_STYLES.header + "-" + state.header);

  return styles.length ? styles.join(" ") : null;
}

function textileDisabled(state) {
  var type = state.layoutType;

  switch(type) {
  case "notextile":
  case "code":
  case "pre":
    return TOKEN_STYLES[type];
  default:
    if (state.notextile)
      return TOKEN_STYLES.notextile + (type ? (" " + TOKEN_STYLES[type]) : "");
    return null;
  }
}

function activeStyles(state) {
  var styles = [];
  for (var i = 1; i < arguments.length; ++i) {
    if (state[arguments[i]])
      styles.push(TOKEN_STYLES[arguments[i]]);
  }
  return styles;
}

function blankLine(state) {
  var spanningLayout = state.spanningLayout, type = state.layoutType;

  for (var key in state) if (state.hasOwnProperty(key))
    delete state[key];

  state.mode = Modes.newLayout;
  if (spanningLayout) {
    state.layoutType = type;
    state.spanningLayout = true;
  }
}

var REs = {
  cache: {},
  single: {
    bc: "bc",
    bq: "bq",
    definitionList: /- .*?:=+/,
    definitionListEnd: /.*=:\s*$/,
    div: "div",
    drawTable: /\|.*\|/,
    foot: /fn\d+/,
    header: /h[1-6]/,
    html: /\s*<(?:\/)?(\w+)(?:[^>]+)?>(?:[^<]+<\/\1>)?/,
    link: /[^"]+":\S/,
    linkDefinition: /\[[^\s\]]+\]\S+/,
    list: /(?:#+|\*+)/,
    notextile: "notextile",
    para: "p",
    pre: "pre",
    table: "table",
    tableCellAttributes: /[\/\\]\d+/,
    tableHeading: /\|_\./,
    tableText: /[^"_\*\[\(\?\+~\^%@|-]+/,
    text: /[^!"_=\*\[\(<\?\+~\^%@-]+/
  },
  attributes: {
    align: /(?:<>|<|>|=)/,
    selector: /\([^\(][^\)]+\)/,
    lang: /\[[^\[\]]+\]/,
    pad: /(?:\(+|\)+){1,2}/,
    css: /\{[^\}]+\}/
  },
  createRe: function(name) {
    switch (name) {
    case "drawTable":
      return REs.makeRe("^", REs.single.drawTable, "$");
    case "html":
      return REs.makeRe("^", REs.single.html, "(?:", REs.single.html, ")*", "$");
    case "linkDefinition":
      return REs.makeRe("^", REs.single.linkDefinition, "$");
    case "listLayout":
      return REs.makeRe("^", REs.single.list, RE("allAttributes"), "*\\s+");
    case "tableCellAttributes":
      return REs.makeRe("^", REs.choiceRe(REs.single.tableCellAttributes,
                                          RE("allAttributes")), "+\\.");
    case "type":
      return REs.makeRe("^", RE("allTypes"));
    case "typeLayout":
      return REs.makeRe("^", RE("allTypes"), RE("allAttributes"),
                        "*\\.\\.?", "(\\s+|$)");
    case "attributes":
      return REs.makeRe("^", RE("allAttributes"), "+");

    case "allTypes":
      return REs.choiceRe(REs.single.div, REs.single.foot,
                          REs.single.header, REs.single.bc, REs.single.bq,
                          REs.single.notextile, REs.single.pre, REs.single.table,
                          REs.single.para);

    case "allAttributes":
      return REs.choiceRe(REs.attributes.selector, REs.attributes.css,
                          REs.attributes.lang, REs.attributes.align, REs.attributes.pad);

    default:
      return REs.makeRe("^", REs.single[name]);
    }
  },
  makeRe: function() {
    var pattern = "";
    for (var i = 0; i < arguments.length; ++i) {
      var arg = arguments[i];
      pattern += (typeof arg === "string") ? arg : arg.source;
    }
    return new RegExp(pattern);
  },
  choiceRe: function() {
    var parts = [arguments[0]];
    for (var i = 1; i < arguments.length; ++i) {
      parts[i * 2 - 1] = "|";
      parts[i * 2] = arguments[i];
    }

    parts.unshift("(?:");
    parts.push(")");
    return REs.makeRe.apply(null, parts);
  }
};

function RE(name) {
  return (REs.cache[name] || (REs.cache[name] = REs.createRe(name)));
}

var Modes = {
  newLayout: function(stream, state) {
    if (stream.match(RE("typeLayout"), false)) {
      state.spanningLayout = false;
      return (state.mode = Modes.blockType)(stream, state);
    }
    var newMode;
    if (!textileDisabled(state)) {
      if (stream.match(RE("listLayout"), false))
        newMode = Modes.list;
      else if (stream.match(RE("drawTable"), false))
        newMode = Modes.table;
      else if (stream.match(RE("linkDefinition"), false))
        newMode = Modes.linkDefinition;
      else if (stream.match(RE("definitionList")))
        newMode = Modes.definitionList;
      else if (stream.match(RE("html"), false))
        newMode = Modes.html;
    }
    return (state.mode = (newMode || Modes.text))(stream, state);
  },

  blockType: function(stream, state) {
    var match, type;
    state.layoutType = null;

    if (match = stream.match(RE("type")))
      type = match[0];
    else
      return (state.mode = Modes.text)(stream, state);

    if (match = type.match(RE("header"))) {
      state.layoutType = "header";
      state.header = parseInt(match[0][1]);
    } else if (type.match(RE("bq"))) {
      state.layoutType = "quote";
    } else if (type.match(RE("bc"))) {
      state.layoutType = "code";
    } else if (type.match(RE("foot"))) {
      state.layoutType = "footnote";
    } else if (type.match(RE("notextile"))) {
      state.layoutType = "notextile";
    } else if (type.match(RE("pre"))) {
      state.layoutType = "pre";
    } else if (type.match(RE("div"))) {
      state.layoutType = "div";
    } else if (type.match(RE("table"))) {
      state.layoutType = "table";
    }

    state.mode = Modes.attributes;
    return tokenStyles(state);
  },

  text: function(stream, state) {
    if (stream.match(RE("text"))) return tokenStyles(state);

    var ch = stream.next();
    if (ch === '"')
      return (state.mode = Modes.link)(stream, state);
    return handlePhraseModifier(stream, state, ch);
  },

  attributes: function(stream, state) {
    state.mode = Modes.layoutLength;

    if (stream.match(RE("attributes")))
      return TOKEN_STYLES.attributes
    else
      return tokenStyles(state);
  },

  layoutLength: function(stream, state) {
    if (stream.eat(".") && stream.eat("."))
      state.spanningLayout = true;

    state.mode = Modes.text;
    return tokenStyles(state);
  },

  list: function(stream, state) {
    var match = stream.match(RE("list"));
    state.listDepth = match[0].length;
    var listMod = (state.listDepth - 1) % 3;
    if (!listMod)
      state.layoutType = "list1";
    else if (listMod === 1)
      state.layoutType = "list2";
    else
      state.layoutType = "list3";

    state.mode = Modes.attributes;
    return tokenStyles(state);
  },

  link: function(stream, state) {
    state.mode = Modes.text;
    if (stream.match(RE("link"))) {
      stream.match(/\S+/);
      return TOKEN_STYLES.link
    }
    return tokenStyles(state);
  },

  linkDefinition: function(stream) {
    stream.skipToEnd();
    return TOKEN_STYLES.linkDefinition
  },

  definitionList: function(stream, state) {
    stream.match(RE("definitionList"));

    state.layoutType = "definitionList";

    if (stream.match(/\s*$/))
      state.spanningLayout = true;
    else
      state.mode = Modes.attributes;

    return tokenStyles(state);
  },

  html: function(stream) {
    stream.skipToEnd();
    return TOKEN_STYLES.html
  },

  table: function(stream, state) {
    state.layoutType = "table";
    return (state.mode = Modes.tableCell)(stream, state);
  },

  tableCell: function(stream, state) {
    if (stream.match(RE("tableHeading")))
      state.tableHeading = true;
    else
      stream.eat("|");

    state.mode = Modes.tableCellAttributes;
    return tokenStyles(state);
  },

  tableCellAttributes: function(stream, state) {
    state.mode = Modes.tableText;

    if (stream.match(RE("tableCellAttributes")))
      return TOKEN_STYLES.attributes
    else
      return tokenStyles(state);
  },

  tableText: function(stream, state) {
    if (stream.match(RE("tableText")))
      return tokenStyles(state);

    if (stream.peek() === "|") { // end of cell
      state.mode = Modes.tableCell;
      return tokenStyles(state);
    }
    return handlePhraseModifier(stream, state, stream.next());
  }
};

const textile = {
  name: "textile",
  startState: function() {
    return { mode: Modes.newLayout };
  },
  token: function(stream, state) {
    if (stream.sol()) startNewLine(stream, state);
    return state.mode(stream, state);
  },
  blankLine: blankLine
};

exports.textile = textile;
