/* global Vue FileReader CodeMirror */
Vue.config.devtools = true

window.app = new Vue({
  el: '#root',
  data: {
    ast: {},
    editor: {},
    error: '',
    fileReader: new FileReader(),
    file: '',
    ids: [],
    js: '',
    json: '',
    lex: [],
    vars: [] // the value of ids
  },
  mounted () {
    this.editor = CodeMirror(document.querySelector('#editor'), {
      lineNumbers: true,
      tabSize: 2,
      extraKeys: {
        Tab: cm => {
          const spaces = Array(cm.getOption('indentUnit') + 1).join(' ')
          cm.replaceSelection(spaces)
        }
      }
    })
    this.fileReader.onload = e => {
      this.editor.setValue(this.fileReader.result)
    }
  },
  methods: {
    fileHandler () {
      const file = document.querySelector('#file').files[0]
      if (file) this.file = file
      this.fileReader.readAsText(this.file)
    },
    reset () {
      this.error = ''
      this.ids = []
      this.vars = []
      this.lex = []
      this.json = ''
    },
    run () {
      try {
        this.reset()
        this.ast = parse(TokenStream(InputStream(this.editor.getValue())))
        this.js = '(function () { "use strict"; try { ' + makeJS(this.ast)
        this.ids = [...new Set(this.ids)]
        this.js +=
          ' parent.app.vars = [' +
          this.ids.join(', ') +
          ']; } catch (e) { parent.app.error = "Runtime Error: " + e.message; }}());'
        // recreate an iframe sandbox if it already exist
        // , then run the transpiled js in it
        const sb = document.querySelector('#sandbox')
        if (sb) sb.remove()
        const iframe = document.createElement('iframe')
        iframe.id = 'sandbox'
        iframe.style.display = 'none'
        iframe.src = 'sandbox.html'
        document.body.appendChild(iframe)
        const script = document.createElement('script')
        script.textContent = this.js
        document
          .querySelector('#sandbox')
          .contentDocument.body.appendChild(script)
      } catch (e) {
        this.error = e.message
      }
    },
    lexer () {
      try {
        this.reset()
        const tokens = TokenStream(InputStream(this.editor.getValue()))
        while (!tokens.eof()) {
          this.lex.push(tokens.next())
        }
      } catch (e) {
        this.error = e.message
      }
    },
    parser () {
      try {
        this.reset()
        this.ast = parse(TokenStream(InputStream(this.editor.getValue())))
        this.json = JSON.stringify(this.ast, null, 2)
      } catch (e) {
        this.error = e.message
      }
    }
  }
})

function InputStream (input) {
  let pos = 0
  let line = 1
  let col = 0
  return {
    next: next,
    peek: peek,
    eof: eof,
    croak: croak
  }
  function next () {
    const ch = input.charAt(pos++)
    if (ch === '\n') {
      line++
      col = 0
    } else col++
    return ch
  }
  function peek () {
    return input.charAt(pos)
  }
  function eof () {
    return peek() === ''
  }
  function croak (msg, type = 'Syntax') {
    throw new Error(type + ' Error: ' + msg + ' (' + line + ':' + col + ')')
  }
}

function TokenStream (input) {
  let current = null
  const reserved =
    ' abstract arguments boolean break byte case catch char class const continue' +
    ' debugger default delete do double else enum eval export extends false final' +
    ' finally float for function goto if implements import in instanceof int' +
    ' interface let long native new null package private protected public return' +
    ' short static super switch synchronized this throw throws transient true try' +
    ' typeof var void volatile while with yield '
  const keywords = ' begin for end if then else while int '
  const ops = ' + - * / = == != > < >= <= and or '
  return {
    next: next,
    peek: peek,
    eof: eof,
    croak: input.croak
  }
  function isKeyword (x) {
    return keywords.indexOf(' ' + x + ' ') >= 0
  }
  function isReservedWord (x) {
    return reserved.indexOf(' ' + x + ' ') >= 0
  }
  function isDigit (ch) {
    return /[0-9]/i.test(ch)
  }
  function isIdStart (ch) {
    return /[a-z_]/i.test(ch)
  }
  function isId (ch) {
    return isIdStart(ch) || '_0123456789'.indexOf(ch) >= 0
  }
  function isOpChar (ch, prev, prev2) {
    if (!prev) return '+-*/=!<>'.indexOf(ch) >= 0
    else if (
      (prev === '=' && !prev2) ||
      prev === '!' ||
      prev === '<' ||
      prev === '>'
    ) {
      return ch === '='
    }
    return false
  }
  function isOp (x) {
    return ops.indexOf(' ' + x + ' ') >= 0
  }
  function isPunc (ch) {
    return ',;()'.indexOf(ch) >= 0
  }
  function isWhitespace (ch) {
    return ' \t\n'.indexOf(ch) >= 0
  }
  function readWhile (predicate) {
    let str = ''
    while (
      !input.eof() &&
      predicate(input.peek(), str[str.length - 1], str[str.length - 2])
    ) {
      str += input.next()
    }
    return str
  }
  function readNumber () {
    const number = readWhile(isDigit)
    return { type: 'num', value: parseInt(number, 10) }
  }
  function readIdent () {
    const id = readWhile(isId)
    if (!isKeyword(id) && isReservedWord(id)) {
      input.croak('Variable name "' + id + '" is not allowed', 'Lexical')
    }
    return {
      type: isKeyword(id) ? 'kw' : isOp(id) ? 'op' : 'var',
      value: id
    }
  }
  function readOp () {
    const id = readWhile(isOpChar)
    return {
      type: isOp(id) ? 'op' : input.croak('Unknown symbol: ' + id, 'Lexical'),
      value: id
    }
  }
  function skipComment () {
    readWhile(function (ch) {
      return ch !== '\n'
    })
    input.next()
  }
  function readNext () {
    readWhile(isWhitespace)
    if (input.eof()) return null
    const ch = input.peek()
    if (ch === '#') {
      skipComment()
      return readNext()
    }
    if (isDigit(ch)) return readNumber()
    if (isPunc(ch)) {
      return {
        type: 'punc',
        value: input.next()
      }
    }
    if (isOpChar(ch)) return readOp()
    if (isIdStart(ch)) return readIdent()
    input.croak('Invalid symbol: ' + ch, 'Lexical')
  }
  function peek () {
    return current || (current = readNext())
  }
  function next () {
    const tok = current
    current = null
    return tok || readNext()
  }
  function eof () {
    return peek() === null
  }
}

function parse (input) {
  const PRECEDENCE = {
    '=': 1,
    or: 2,
    and: 3,
    '<': 7,
    '>': 7,
    '<=': 7,
    '>=': 7,
    '==': 7,
    '!=': 7,
    '+': 10,
    '-': 10,
    '*': 20,
    '/': 20
  }
  return parseToplevel()
  function parseToplevel () {
    const prog = []
    while (!input.eof()) {
      prog.push(parseExpression())
      const last = prog.slice(-1)[0]
      if (requireSemicolon(last)) skipPunc(';')
    }
    return { type: 'prog', prog: prog }
  }
  function parseBlock () {
    const body = []
    while (!input.eof() && !isKw('else') && !isKw('end')) {
      body.push(parseExpression())
      const last = body.slice(-1)[0]
      if (requireSemicolon(last)) skipPunc(';')
    }
    return { type: 'block', body: body }
  }
  function parseExpression () {
    if (input.eof()) input.croak('Expecting an expression but reached EOF')
    return maybeBinary(parseAtom(), 0)
  }
  function parseAtom () {
    if (isPunc('(')) {
      input.next()
      const exp = parseExpression()
      skipPunc(')')
      return exp
    }
    if (isKw('begin')) return parseBegin()
    if (isKw('int')) return parseInit()
    if (isKw('for')) return parseFor()
    if (isKw('while')) return parseWhile()
    if (isKw('if')) return parseIf()
    if (isOp('-')) return parseUnary('-')
    if (isOp('+')) return parseUnary('+')
    const tok = input.next()
    if (tok.type === 'var' || tok.type === 'num') {
      return tok
    }
    input.croak('Unexpected token ' + tok.value)
  }
  function parseBegin () {
    skipKw('begin')
    const block = parseBlock()
    skipKw('end')
    return { type: 'begin', body: block.body }
  }
  function parseInit () {
    const vars = []
    let first = true
    skipKw('int')
    while (!input.eof()) {
      if (isPunc(';')) break
      if (first) first = false
      else skipPunc(',')
      if (isPunc(';')) break // the last "," can be missing
      const tok = parseExpression()
      if (tok.type !== 'var' && tok.type !== 'assign') {
        input.croak('Unexpected token ' + tok.type)
      }
      vars.push(tok)
    }
    skipPunc(';')
    return {
      type: 'init',
      vars: vars
    }
  }
  function parseFor () {
    skipKw('for')
    skipPunc(',')
    const init = parseExpression()
    if (init.type !== 'assign') {
      input.croak('Unexpected token ' + init.type)
    }
    skipPunc(',')
    const cond = parseExpression()
    isRval(cond)
    skipPunc(',')
    const inc = parseExpression()
    isRval(inc)
    skipPunc(';')
    const body = parseBlock()
    skipKw('end')
    return {
      type: 'for',
      init: init,
      cond: cond,
      inc: inc,
      body: body
    }
  }
  function parseWhile () {
    skipKw('while')
    skipPunc('(')
    const cond = parseExpression()
    isRval(cond)
    skipPunc(')')
    const body = parseBlock()
    skipKw('end')
    return { type: 'while', cond: cond, body: body }
  }
  function parseIf () {
    skipKw('if')
    skipPunc('(')
    const cond = parseExpression()
    isRval(cond)
    skipPunc(')')
    skipKw('then')
    const then = parseBlock()
    const ret = { type: 'if', cond: cond, then: then }
    if (isKw('else')) {
      input.next()
      ret.else = parseBlock()
    }
    skipKw('end')
    return ret
  }
  function parseUnary (op) {
    skipOp(op)
    const right = parseAtom()
    isRval(right)
    return { type: 'unary', operator: op, right: right }
  }
  function maybeBinary (left, myPrec) {
    if (left.type === 'assign' && left.left.type !== 'var') {
      input.croak('Invalid left-hand side in assignment')
    }
    const tok = isOp()
    if (tok) {
      const hisPrec = PRECEDENCE[tok.value]
      if (hisPrec > myPrec) {
        input.next()
        const right = maybeBinary(parseAtom(), hisPrec)
        isRval(left)
        isRval(right)
        return maybeBinary(
          {
            type: tok.value === '=' ? 'assign' : 'binary',
            operator: tok.value,
            left: left,
            right: right
          },
          myPrec
        )
      }
    }
    return left
  }
  function isPunc (ch) {
    var tok = input.peek()
    return tok && tok.type === 'punc' && (!ch || tok.value === ch) && tok
  }
  function isKw (kw) {
    var tok = input.peek()
    return tok && tok.type === 'kw' && (!kw || tok.value === kw) && tok
  }
  function isOp (op) {
    var tok = input.peek()
    return tok && tok.type === 'op' && (!op || tok.value === op) && tok
  }
  function isRval (tok) {
    if (
      tok.type !== 'var' &&
      tok.type !== 'num' &&
      tok.type !== 'unary' &&
      tok.type !== 'binary'
    ) {
      input.croak('Unexpected token ' + tok.type)
    }
  }
  function skipOp (ch) {
    if (isOp(ch)) input.next()
    else if (!input.eof()) {
      input.croak('Expecting ' + ch + ' but got ' + input.peek().value)
    } else {
      input.croak('Expecting ' + ch + ' but reached EOF')
    }
  }
  function skipPunc (ch) {
    if (isPunc(ch)) input.next()
    else if (!input.eof()) {
      input.croak('Expecting ' + ch + ' but got ' + input.peek().value)
    } else {
      input.croak('Expecting ' + ch + ' but reached EOF')
    }
  }
  function skipKw (kw) {
    if (isKw(kw)) input.next()
    else if (!input.eof()) {
      input.croak('Expecting ' + kw + ' but got ' + input.peek().value)
    } else {
      input.croak('Expecting ' + kw + ' but reached EOF')
    }
  }
  function isBlock (tok) {
    return (
      tok.type === 'begin' ||
      tok.type === 'for' ||
      tok.type === 'while' ||
      tok.type === 'if'
    )
  }
  function requireSemicolon (tok) {
    return !isBlock(tok) && tok.type !== 'init'
  }
}

function makeJS (ast) {
  return js(ast)
  function js (ast) {
    const cases = [
      'js_num',
      'js_var',
      'js_unary',
      'js_binary',
      'js_assign',
      'js_init',
      'js_begin',
      'js_block',
      'js_prog',
      'js_for',
      'js_while',
      'js_if'
    ]
    const handler = {
      js_num (ast) {
        return JSON.stringify(ast.value)
      },
      js_var (ast) {
        window.app.ids.push(ast.value)
        return ast.value
      },
      js_unary (ast) {
        const op = ast.operator
        return '( ' + op + js(ast.right) + ' )'
      },
      js_binary (ast) {
        let op = ' ' + ast.operator + ' '
        if (op === ' and ') op = ' && '
        if (op === ' or ') op = ' || '
        if (op === ' / ') {
          return 'Math.trunc(' + js(ast.left) + op + js(ast.right) + ')'
        }
        return '( ' + js(ast.left) + op + js(ast.right) + ' )'
      },
      js_assign (ast) {
        return js(ast.left) + ' = ' + js(ast.right)
      },
      js_init (ast) {
        const vars = []
        for (let i = 0; i < ast.vars.length; i += 1) {
          vars.push(js(ast.vars[i]))
        }
        return 'var ' + vars.join(', ')
      },
      js_prog (ast) {
        const prog = []
        for (let i = 0; i < ast.prog.length; i += 1) {
          prog.push(js(ast.prog[i]))
        }
        return prog.join('; ') + ';'
      },
      js_block (ast) {
        const body = []
        for (let i = 0; i < ast.body.length; i += 1) {
          body.push(js(ast.body[i]))
        }
        return body.join('; ') + ';'
      },
      js_begin (ast) {
        return this.js_block(ast)
      },
      js_for (ast) {
        return (
          'for (' +
          js(ast.init) +
          '; ' +
          js(ast.cond) +
          '; ' +
          js(ast.init.left) +
          ' += ' +
          js(ast.inc) +
          ') { ' +
          js(ast.body) +
          ' }'
        )
      },
      js_while (ast) {
        return 'while (' + js(ast.cond) + ') { ' + js(ast.body) + ' }'
      },
      js_if (ast) {
        let str = 'if (' + js(ast.cond) + ') { ' + js(ast.then) + ' }'
        if (ast.else) {
          str += ' else { ' + js(ast.else) + ' }'
        }
        return str
      }
    }
    for (let i = 0; i < cases.length; i += 1) {
      if ('js_' + ast.type === cases[i]) {
        return handler[cases[i]](ast)
      }
    }
  }
}
