'use strict';

class BaseHighlighter{
  constructor(){
    this.state = BaseHighlighter.createState();
  }
  reset(){
    this.state.reset();
  }
  parse(){
    if(!this.state.initialState){
      throw "This highlighter is not initialized";
    }
  }
  static createState(){
    return new (function(){
      this.token = "";
      let current = null;
      let previous = null;
      let initialState = null;
      this.set = (a) => {
        previous = current;
        current = a;
        return
      }
      this.now = () => current;
      this.previous = ()=> previous;
      this.initializeTo = (some) => { initialState = some }
      this.reset = () => {
        this.token = "";
        previous = initialState;
        current = initialState;
      }
    })();
  }
  static BaseState = Symbol("base");
}

class SimpleHighlighter extends BaseHighlighter{
  constructor(){
    super();
    this.state.initializeTo(BaseHighlighter.BaseState);
  }
  static State = {
    SingleQuote : Symbol("quote"),
    DoubleQuote : Symbol("quote")
  }
  
  parse(info,targetNode){
    if(info.content.length > 0){
      SimpleHighlighter.parseSimple(info,this.state,targetNode);
    }
    if(this.state.token){
      throw "simple token is not 0"
    }
    this.state.token = "";
  }
  
  static parseSimple(info,state,target){
    let pointer = 0;
    let currentState = state.now();
    const length = info.content.length;
    while(pointer < length){
      let character = info.content[pointer++];
      if(character === "\r"){
        continue
      }
      currentState = state.now();
      switch(currentState){
        case BaseHighlighter.BaseState:
          switch(character){
            case "\"":
              target.append(state.token);
              state.token = "\"";
              state.set(SimpleHighlighter.State.DoubleQuote);
              break;
            case "'":
              targetNode.append(state.token);
              state.token = "'";
              state.set(SimpleHighlighter.State.SingleQuote);
              break;
            default:
              state.token += character;
          }
          break;
        case SimpleHighlighter.State.SingleQuote:
          switch(character){
            case "'":
              target.appendChild(SimpleHighlighter.createQuote()).textContent = state.token + "'";
              state.token = "";
              state.set(BaseHighlighter.BaseState);
              break;
            default:
              state.token += character;
          }
          break;
        case SimpleHighlighter.State.DoubleQuote:
          switch(character){
            case "\"":
              target.appendChild(SimpleHighlighter.createQuote()).textContent = state.token + "\"";
              state.token = "";
              state.set(BaseHighlighter.BaseState);
              break;
            default:
              state.token += character;
          }
          break
      }
    }
    if(state.token.length > 0){
      if(currentState === BaseHighlighter.BaseState){
        target.append(state.token);
        state.token = "";
      }else{
        target.appendChild(SimpleHighlighter.createQuote()).textContent = state.token;
      }
    }
  }
  
  static createQuote(){
    let n = document.createElement("span");
    n.className = "quote";
    return n
  }
}

class CSSHighlighter extends BaseHighlighter{
  constructor(){
    super();
    this.state.initializeTo(CSSHighlighter.State.Selector);
    this.state.curly = false;
    this.state.fnLevel = 0;
    this.state.generateLinks = false;
  }
  
  reset(){
    this.state.reset();
    this.state.curly = false;
    this.state.fnLevel = 0;
  }
  
  parse(info,targetNode){
    if(info.content.length > 0){
      CSSHighlighter.parseCSS(info,this.state,targetNode);
    }
    if(this.state.token){
      throw "CSS token is not 0"
    }
    this.state.token = "";
  }
  
  static parseCSS(info,state,targetNode){
    
    state.generateLinks = (info.linkMatcher instanceof RegExp) && (typeof info.linkGenerator === "function");
    
    if(state.generateLinks && info.linkChanged){
      if (info.linkGenerator != state.linkGenerator){
        state.linkGenerator = info.linkGenerator;
      }
      if (info.linkMatcher != state.linkMatcher){
        state.linkMatcher = info.linkMatcher;
      }
    }
    
    let currentState;
    const chars = Array.from(info.content);
    if(info.content.endsWith("\r\n")){
      chars[chars.length - 2] = "\n";
      chars.pop();
    }
    const length = chars.length;
    let tokenStart = 0;
    let pointer = 0;
    while(pointer < length){
      let character = chars[pointer];
      currentState = state.now();
      switch(currentState){
      
        case CSSHighlighter.State.Selector:
          switch(character){
            case "/":
              if(chars[pointer+1] === "*"){
                state.set(CSSHighlighter.State.Comment);
                if(pointer - tokenStart > 0){
                  state.token = chars.slice(tokenStart,pointer).join("");
                  CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.Selector,targetNode);
                }
                tokenStart = pointer;
                pointer++;
              }
              break;
            case "{":
              state.set(CSSHighlighter.State.Property);
              state.token = chars.slice(tokenStart,pointer).join("");
              CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.Selector,targetNode);
              tokenStart = pointer + 1;
              targetNode.appendChild(CSSHighlighter.addBracket("{"));
              break;
            case "}":
              state.token = chars.slice(tokenStart,pointer).join("");
              CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.Text,targetNode);
              tokenStart = pointer + 1;
              
              targetNode.appendChild(CSSHighlighter.addBracket("}"));
              
              break;
            case "@":
              state.set(CSSHighlighter.State.AtRule);
          }
          break;
      
        case CSSHighlighter.State.Comment:
          if(character === "*"){
            if(chars[pointer+1] === "/"){
              pointer++;
              state.token = chars.slice(tokenStart,pointer+1).join("");
              state.set(state.previous());
              CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.Comment,targetNode);
              
              tokenStart = pointer + 1;
              break
            }
          }
          break;

        case CSSHighlighter.State.Property:
          switch(character){
            case "/":
              if(chars[pointer+1] === "*"){
                state.set(CSSHighlighter.State.Comment);
              }
              break;
            case ":":
              state.token = chars.slice(tokenStart,pointer+1).join("");
              CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.Property,targetNode);
              state.set(CSSHighlighter.State.Value);
              tokenStart = pointer + 1;
              break;
            case "}":
              state.token = chars.slice(tokenStart,pointer).join("");
              CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.Text,targetNode);
              state.set(CSSHighlighter.State.Selector);
              tokenStart = pointer + 1;
              targetNode.appendChild(CSSHighlighter.addBracket("}"));
          }
          break;
        case CSSHighlighter.State.Value:
          let createToken = true;
          let indexOffset = 1;
          switch(character){
            case ";":
              state.set(CSSHighlighter.State.Property);
              break;
            case "}":
              indexOffset = 0;
              state.set(CSSHighlighter.State.Selector);
              break;
            case "(":
              state.fnLevel++;
              state.set(CSSHighlighter.State.Function);
              break;
            default:
              createToken = false;
          }
          if(createToken){
            state.token = chars.slice(tokenStart,pointer+indexOffset).join("");
            CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.Value,targetNode);
            tokenStart = pointer + 1;
            if(indexOffset === 0){
              targetNode.appendChild(CSSHighlighter.addBracket("}"))
            }
          }
          break;
        case CSSHighlighter.State.AtRule:
          switch(character){
            case " ":
              state.token = chars.slice(tokenStart,pointer+1).join("");
              CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.AtRule,targetNode);
              state.set(CSSHighlighter.State.AtValue);
              tokenStart = pointer + 1;
          }
          break;
        case CSSHighlighter.State.AtValue:
          let idxOffset = 0;
          switch(character){
            case ";":
              indexOffset = 1;
            case "{":
              state.token = chars.slice(tokenStart,pointer + idxOffset).join("");
              CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.AtValue,targetNode);
              state.set(CSSHighlighter.State.Selector);
              tokenStart = pointer + 1;
              if(!idxOffset){
                targetNode.appendChild(CSSHighlighter.addBracket("{"));
              }
            default:
          }
          break
        case CSSHighlighter.State.Function:
          switch(character){
            case ")":
              state.fnLevel--;
              if(state.fnLevel === 0){
                state.token = chars.slice(tokenStart,pointer).join("");
                CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.Function,targetNode);
                tokenStart = pointer;
                state.set(CSSHighlighter.State.Value);
              }
              break;
            case "}":
              state.fnLevel = 0;
              state.set(CSSHighlighter.State.Selector);
          }
      }
      pointer++
    }
    if(pointer > tokenStart){
      state.token = chars.slice(tokenStart,pointer).join("");
      CSSHighlighter.createElementFromToken(state,currentState,targetNode);
    }
    state.token = "";
  }
  
  static State = {
    Selector: Symbol("selector"),
    Text:     Symbol("text"),
    Comment:  Symbol("comment"),
    Property: Symbol("property"),
    Value:    Symbol("value"),
    AtRule:   Symbol("atrule"),
    AtValue:  Symbol("atvalue"),
    Function: Symbol("function"),
    Curly:    Symbol("curly")
  }
  
  static selectorToClassMap = new Map([
    [":","pseudo"],
    ["#","id"],
    [".","class"],
    ["[","attribute"]
  ]);
  
  static addBracket(n){
    let span = document.createElement("span");
    span.className = "curly";
    span.textContent = n;
    return span
  }
  
  static createElementFromToken(state,type,targetNode){
    if(state.token.length === 0){
      return
    }
    let n = document.createElement("span");
    switch(type){
      case CSSHighlighter.State.Selector:
      // This isn't exactly correct, but it works because parser treats \r\n sequences that follow a closed comment as "selector"
        //rulesetUnderConstruction = createNewRuleset();
        let parts = state.token.split(/([\.#:\[]\w[\w-_"'=\]]*|\s\w[\w-_"'=\]]*)/);
      
        for(let part of parts){
          if(part.length === 0){
            continue
          }
          let character = part[0];
          switch (character){
            case ":":
            case "#":
            case "[":
            case ".":
              let p = n.appendChild(document.createElement("span"));
              p.className = CSSHighlighter.selectorToClassMap.get(character);
              p.textContent = part;
              break;
            default:
              n.append(part);
          }
        }
        break
      case CSSHighlighter.State.Comment:
        if(state.generateLinks){
          let linksToFile = state.token.match(state.linkMatcher);
          if(linksToFile && linksToFile.length){
            const transformed = linksToFile.map(state.linkGenerator);
            n.append(CSSHighlighter.createLinksFromMatchingToken(linksToFile,transformed,state));
            break;
          }
        }
        n.textContent = state.token;
        break;
      case CSSHighlighter.State.Value:
        let startImportant = state.token.indexOf("!important");
        if(startImportant === -1){
          n.textContent = state.token;
        }else{
          n.textContent = state.token.substr(0,startImportant);
          let importantTag = document.createElement("span");
          importantTag.className = "important-tag";
          importantTag.textContent = "!important";
          n.appendChild(importantTag);
          if(state.token.length > (9 + startImportant)){
            n.append(state.token.substr(startImportant + 10))
          }
        }
        break;
      case CSSHighlighter.State.Function:
        n.textContent = state.token;
        break
      default:
        n.textContent = state.token;
    }
    
    n.className = (`token ${type.description}`);
    
    targetNode.appendChild(n);
    return
  }
  static createLinksFromMatchingToken(parts,transformed,state){
    let frag = new DocumentFragment();
    let linkIdx = 0;
    let fromIdx = 0;
    while(linkIdx < parts.length){
      let part = parts[linkIdx];
      let idx = state.token.indexOf(part);
      frag.append(state.token.substring(fromIdx,idx));
      let link = document.createElement("a");
      link.textContent = part;
      link.href = transformed[linkIdx++];
      link.target = "_blank";
      frag.appendChild(link);
      fromIdx = idx + part.length;
    }
    frag.append(state.token.substring(fromIdx));
    return frag
  }
}

export { CSSHighlighter,SimpleHighlighter }