import Operator from './operator';
import RiScriptParser from '../grammar/antlr/RiScriptParser';
import RiScriptParserVisitor from '../grammar/antlr/RiScriptParserVisitor';

/*
 * This visitor walks the tree generated by a parser, 
 * evaluating each node as it goes.
 */
class Visitor extends RiScriptParserVisitor {

  constructor(parent, RiTa) {
    super();
    this.choices = {};
    this.parent = parent; // RiScript
    this.RiTa = RiTa;
  }

  init(context, opts) {
    this.pendingSymbols = [];
    this.trace = opts && opts.trace;
    this.silent = opts && opts.silent;
    this.context = context || {};//Object.assign({}, context); // clone
    return this;
  }

  // Entry point for tree visiting
  start(ctx) {
    let text = ctx.getText();
    let lastBreak = /\n$/.test(text);
    this.trace && console.log("start: '"
      + text.replace(/\r?\n/g, "\\n") + "'");
    //this.printChildren(ctx);
    let result = this.visitChildren(ctx);
    return lastBreak ? result : result.replace(/\r?\n$/, '');
  }

  visitLine(ctx) {
    let line = this.visitChildren(ctx);
    return line.length ? line + '\n' : '';
  }

  visitLink(ctx) {
    this.trace && console.log("visitLink: '"
      + ctx.getText() + "' link=" + ctx.url().getText());
    return '[' + this.visit(ctx.expr()) + ']'
      + '&lpar;' + ctx.url().getText() + '&rpar;' + ctx.WS();
  }

  visitChoiceNoObj(ctx) {// not used (save)

    let options = [], rand = this.RiTa.randomizer;

    ctx.wexpr().map((w, k) => {
      let wctx = w.weight();
      let weight = wctx ? parseInt(wctx.INT()) : 1;
      let expr = w.expr() || Visitor.EC;
      for (let i = 0; i < weight; i++) options.push(expr);
    });

    let txs = ctx.transform();
    this.trace && console.log("visitChoice: '" + ctx.getText() + "' options=["
      + options.map(o => o.getText()) + "] tfs=" + flattenTx(txs));

    // make the selection
    let tok = rand.random(options); // SIMPLE
    if (this.trace) console.log("  select: '" + tok.getText()
      + "' [" + this.ruleName(tok) + "]");

    // now visit the token 
    let visited = this.visit(tok).trim(); // trim inside choices

    // now apply any transforms
    if (!txs.length) return visited;
    let applied = this.applyTransforms(visited, txs);
    let result = typeof applied !== 'undefined' ? applied
      : '(' + visited + ')' + flattenTx(txs);

    if (this.trace) console.log("resolveChoice: '" + result + "'");

    return result.trim();
  }

  visitChoice(ctx) {

    let text = ctx.getText().replace(TX_RE, '');
    let choice = this.choices[text];
    if (!choice) {
      choice = new ChoiceState(this, ctx);
      //console.log('created Choice: ' + etext);
      this.choices[text] = choice;
    }

    let txs = ctx.transform(), tstr = flattenTx(txs);
    this.trace && console.log("visitChoice: '" + ctx.getText()
      + "' options=[" + choice.optionStr() + "] tfs=" + tstr);

    // make the selection
    let selected = choice.select(tstr);
    if (this.trace) console.log("  select: '" + selected.getText()
      + "' [" + this.ruleName(selected) + "]");

    // now visit the token 
    let visited = this.visit(selected).trim(); // trim inside a choice

    // now apply any transforms
    if (!txs.length) return visited;
    let applied = this.applyTransforms(visited, txs);
    let result = typeof applied !== 'undefined' ? applied
      : '(' + visited + ')' + tstr;

    if (this.trace) console.log("resolveChoice: '" + result + "'");

    return result;
  }

  visitSymbol(ctx) {

    let txs = ctx.transform(), result = ctx.getText(), tn = ctx.SYM();

    // handle transform on empty string    
    if (!tn) {
      this.trace && console.log("emptyTransform: " + ctx.getText());
      let applied = this.applyTransforms('', txs);
      return applied !== null ? applied : result; // keep null
    }

    let ident = symbolName(tn.getText());
    this.trace && console.log("visitSymbol: $" + ident
      + (this.context[Visitor.DYN + ident] ? " [dynamic]" : '') + " tfs=" + flattenTx(txs));

    // if the symbol is pending just return it
    if (this.pendingSymbols.includes(ident)) {
      this.trace && console.log("resolveSymbol[0]: (pending) $" + ident);
      return result;// + flattenTx(txs); // transform should be stored
    }

    let resolved = this.context[ident];  // try the context
    if (!resolved) {

      // try for a dynamic in context
      resolved = this.context[Visitor.DYN + ident];
      if (resolved) return this.resolveDynamic(ident, resolved, txs);

      // otherwise give up, wait for next pass
      this.trace && console.log("resolveSymbol[1]: $" + ident + " -> '" + result + "'");
      return result;
    }

    // if not fully resolved, save for next time (as inline)
    if (this.parent.isParseable(resolved)) {
      this.pendingSymbols.push(ident);
      result = Visitor.LP + Visitor.SYM + ident
        + Visitor.EQ + resolved + Visitor.RP + flattenTx(txs);
      this.trace && console.log("resolveSymbol[P]: $" + ident + " -> " + result);
      return result;
    }

    // now check for transforms
    if (!txs.length) {
      this.trace && console.log("resolveSymbol[2]: $" + ident + " -> '" + resolved + "'");
      return resolved;
    }

    let applied = this.applyTransforms(resolved, txs);
    result = applied || (resolved + flattenTx(txs));
    this.trace && console.log("resolveSymbol[3]: $" + ident + " -> '" + result + "'");

    return result;
  }

  ////////////////////// ///////////// //////////////////////////

  visitAssign(ctx) {

    // visit value and create a mapping in the symbol table */
    let result, symbol = ctx.symbol() || ctx.dynamic();
    let token = ctx.expr(), id = symbol.getText();

    if (id.startsWith(Visitor.DYN)) {
      this.trace && console.log('visitAssign: ' + id + '=' + flatten(token) + ' [*DYN*]');
      result = token.getText();
    }
    else {
      id = symbolName(id);
      this.trace && console.log('visitAssign: $' + id + '=\'' + flatten(token));
      result = this.visit(token);
    }

    this.context[id] = result;
    this.trace && console.log('resolveAssign: context[' + id + "] -> '"
      + result + "' ");// + JSON.stringify(this.context));

    return ctx.start.column === 0 ? '' : result; // no output if raw
  }

  visitExpr(ctx) {
    if (this.trace) {
      console.log("visitExpr: '" + ctx.getText() + "'");
      //this.printChildren(ctx);
    }
    return this.visitChildren(ctx);
  }

  visitChars(ctx) {
    if (this.trace) console.log("visitChars: '" + ctx.getText() + "'");
    let txt = ctx.getText();
    return txt;
  }

  visitCexpr(ctx) {
    let conds = ctx.cond();
    this.trace && console.log('visitCexpr:' + ctx.expr().getText() + "'",
      'cond={' + conds.map(c => c.getText().replace(',', '')) + '}');

    for (let i = 0; i < conds.length; i++) {
      let id = symbolName(conds[i].symbol().getText());
      let op = Operator.fromString(conds[i].op().getText());
      let val = conds[i].chars().getText();
      let sym = this.context[id];
      let accept = sym ? op.invoke(sym, val) : false;
      /* this.trace && console.log('  cond(' + ctx.getText() + ')',
        id, op.toString(), val, '->', accept); */
      if (!accept) return this.visitExpr(Visitor.EC);
    }
    return this.visitExpr(ctx.expr());
  }

  visitCond(ctx) {
    if (this.trace) console.log("visitCond: '"
      + ctx.getText() + "'\t" + stack(ctx));
    return this.visitChildren(ctx);
  }

  visitWeight(ctx) {
    if (this.trace) console.log("visitWeight: '"
      + ctx.getText() + "'\t" + stack(ctx));
    return this.visitChildren(ctx);
  }

  visitWexpr(ctx) {
    if (this.trace) console.log("visitWexpr: '"
      + ctx.getText() + "'\t" + stack(ctx));
    return this.visitChildren(ctx);
  }

  visitOp(ctx) {
    if (this.trace) console.log("visitOp: '"
      + ctx.getText() + "'\t" + stack(ctx));
    return this.visitChildren(ctx);
  }

  visitTerminal(tn) {
    let text = tn.getText();
    if (this.trace && text !== Visitor.EOF) {
      console.log("visitTerminal: '" + text.replace(/\r?\n/, "\\n") + "'");
    }
    return null;
  }

  visitTransform(ctx) { // should never happen
    throw Error("[ERROR] visitTransform: '" + ctx.getText() + "'");
  }

  ///////////////////////////// helpers ///////////////////////////////

  resolveDynamic(ident, resolved, txs) {
    if (!/^\([^()]*\)$/.test(resolved)) {  // add parens if needed
      resolved = Visitor.LP + resolved + Visitor.RP;
    }
    let result = resolved + flattenTx(txs);
    this.trace && console.log("resolveDynamic[1]: $$" + ident + " -> '" + result + "'");
    return result;
  }

  applyTransforms(term, tfs) {

    if (typeof term === 'undefined' || !tfs || !tfs.length) return;

    let result = term;

    // make sure the terminal is resolved
    if (typeof term === 'string') {
      result = normalize(term);
      if (this.parent.isParseable(result)) { // save for later
        //throw Error("applyTransforms.isParseable=true: '" + result + "'");
        return;
      }
    }

    // split the transform string and apply each transform
    //let transforms = tfs[0].getText().replace(/^\./g, '').split("\.");
    for (let i = 0; i < tfs.length; i++) {
      result = this.applyTransform(result, tfs[i]);
    }

    return result;
  }

  // Attempts to apply transform, returns null on failure
  applyTransform(target, transform) {

    let expr = transform.expr();
    //console.log(expr ? "EXPR:"+expr.getText() : "NONE");
    let tx = transform.DIDENT().getText().replace(/^\./, '');
    let result, raw = target + transform.getText();
    if (this.trace) console.log("applyTransform: '" + raw);

    // function in context
    if (typeof this.context[tx] === 'function') {
      result = expr ? this.context[tx](target, expr.getText())
        : this.context[tx](target);
    }
    // function in transforms
    else if (typeof this.parent.transforms[tx] === 'function') {
      result = this.parent.transforms[tx](target);
    }
    // member functions (usually on String)
    else if (typeof target[tx] === 'function') {
      result = target[tx]();
      if (target === '' && result === '') {
        if (!this.silent && !this.RiTa.SILENT) console.warn
          ("[WARN] Unresolved transform[0]: " + raw);
      }
    }
    else {    // check for property

      if (target.hasOwnProperty(tx)) {
        result = target[tx];
      }
      else {
        result = raw;
        if (!this.silent && !this.RiTa.SILENT) console.warn
          ("[WARN] Unresolved transform: " + result);
      }
    }

    if (this.trace) console.log("resolveTransform: '"
      + target + "' -> '" + (result || undefined) + "'");

    return result;
  }

  stack(rule) {
    let ruleNames = this.parent.parser.getRuleNames(), sb = "    [";
    while (rule) {
      let ruleIndex = rule.getRuleIndex();
      sb += (ruleIndex < 0) ? "n/a" : ruleNames[ruleIndex] + " <- ";
      rule = rule.parent;
    }
    return sb.replace(/ <- $/, "]");
  }

  visitChildren(node) {
    let result = '';
    for (let i = 0; i < node.getChildCount(); i++) {
      let child = node.getChild(i);
      let visit = this.visit(child);
      result += visit || '';
    }
    return result;
  }

  ruleName(ctx) {
    return ctx.hasOwnProperty('symbol') ?
      (ctx.symbol.type > -1 ? this.parent.lexer.symbolicNames[ctx.symbol.type] : "EOF")
      : this.parent.parser.ruleNames[ctx.ruleIndex];
  }

  printChildren(ctx) {
    for (let i = 0; i < ctx.getChildCount(); i++) {
      let child = ctx.getChild(i);
      console.log("  child[" + i + "]: '" + child.getText() +
        "' [" + this.ruleName(child) + "]");
    }
  }
}

class ChoiceState {

  constructor(parent, ctx) {
    this.options = []
    this.rand = parent.RiTa.randomizer;

    ctx.wexpr().forEach((w, k) => {
      let wctx = w.weight();
      let weight = wctx ? parseInt(wctx.INT()) : 1;
      let expr = w.expr() || Visitor.EC;
      for (let i = 0; i < weight; i++) this.options.push(expr);
    });
  }

  optionStr() {
    return this.options.map(o => o.getText());
  }

  select(txStr) {
    if (!this.options.length) throw Error('no options');
    if (this.options.length === 1) return this.options[0];

    let res;
    /*  if (this.type == SEQUENCE) res = this.selectSequence();
        if (this.type == RSEQUENCE) res = this.selectRandSequence(); */
    if (txStr.includes('.' + NOREPEAT[0]) || txStr.includes('.' + NOREPEAT[1])) {
      res = this.selectNoRepeat();
    }
    else {
      res = this.rand.random(this.options); // SIMPLE
    }
    return (this.last = res);
  }

  selectNoRepeat() {
    let cand;
    do {
      cand = this.rand.random(this.options);
    } while (cand === this.last);
    return cand;
  }

  /*selectSequence() {
    let idx = this.index++ % this.options.length;
    return (this.last = this.options[idx]); d
  }


  selectRandSequence() {
    while (this.index == this.options.length) {
      this.options = this.rand.randomOrdering(this.options);
      // make sure we are not repeating
      if (this.options[0] != this.last) this.index = 0;
    }
    return this.selectSequence();
  }*/
}

function normalize(s) {
  return s && s.length ?
    s.replace(/\r/g, '')
      .replace(/\\n/g, '')
      .replace(/\n/g, ' ') : '';
}

function symbolName(text) {
  return (text.length && text[0] === Visitor.SYM)
    ? text.substring(1) : text;
}

function flatten(tok) {
  if (!tok) return '';
  return tok.getText();
}

function flattenTx(txs) {
  if (!txs || !txs.length) return '';
  return txs.reduce((a, c) => a += c.getText(), '');
}

Visitor.LP = '(';
Visitor.RP = ')';
Visitor.EQ = '=';
Visitor.OR = 'OR';
Visitor.SYM = '$';
Visitor.DYN = '$$';
Visitor.DOT = '.';
Visitor.EOF = '<EOF>';
Visitor.ASSIGN = '[]';
Visitor.FUNC = '()';
Visitor.EC = new RiScriptParser.ExprContext();

const NOREPEAT = ['norepeat', 'nr'];
//const RSEQUENCE = 'rseq', SEQUENCE = 'seq', 
//const TYPES = [RSEQUENCE, SEQUENCE, NOREPEAT];
const TX_RE = /\.[A-Za-z_0-9][A-Za-z_0-9]*(\(\))?/;

export default Visitor;
