"use strict";

var diff = require("diffpatcher/diff")
var patch = require("diffpatcher/patch")
var output = require("./output")
var makeOutput = output.makeOutput
var writeOutput = output.write


var Out = "out@interactivate"
var In = "in@interactivate"
var Reciever = "receiver@interactivate"

function makeOptionGetter(name) {
  return function getOption(editor) {
    return editor.getOption(name)
  }
}

var getRenderRate = makeOptionGetter("interactiveSpeed")
var getSectionSeparator = makeOptionGetter("interactiveSeparator")


var slicer = Array.prototype.slice
function throttle(f, delay) {
  /**
  Creates function that calls throttles calls to given `f` such that,
  it's only called if no further calls are made with in the time
  frame (in miliseconds) returned by given `delay.apply(this, arguments)`
  function.
  **/
  var id = 0
  return function throttled() {
    clearTimeout(id, throttled)
    var ms = delay.apply(this, arguments) || 0
    id = setTimeout.apply(this, [f, ms].concat(slicer.call(arguments)))
  }
}



function calculate(editor) {
  var state = editor[In]
  var input = editor.getValue()
  var separator = getSectionSeparator(editor)
  var sections = input.split(separator)
  var activeLine = editor.getCursor().line

  sections.pop() // last section does not has execution marker so skip it.

  var change = Object.keys(sections).reduce(function(result, index) {
    var input = sections[index]
    var line = result.line + input.split("\n").length - 1
    result.line = line
    var delta = {input: input.trim(), line: line, visible: activeLine !== line}
    result.state[index] = state[index] ? patch(state[index], delta) :
                          delta

    return result
  }, { line: 0, state: {} })

  return diff(editor[In], change.state)
}


function send(packet) {
  var event = document.createEvent("CustomEvent")
  event.initCustomEvent("server", false, true, packet)
  window.dispatchEvent(event)
}


function recieve(editor, event) {
  var packet = event.detail
  var delta = {}
  delta[packet.from] = {pending: null,
                        result: packet.message}
  write(editor, delta)
}

function print(editor) {
  if (!editor.getOption("interactivate")) throw editor.constructor.Pass
  editor.operation(function() {
    var cursor = editor.getCursor()
    editor.replaceSelection("\n// =>\n")
    editor.setCursor({ line: cursor.line + 2, ch: 0 })
  })
}


function getMarkerFor(editor, view) {
  var markers = editor.getAllMarks()
  var count = markers.length
  while (count) {
    count = count - 1
    var marker = markers[count]
    if (marker.replacedWith === view) return marker
  }
  return null
}


function write(editor, changes) {
  var doc = editor.getDoc()
  Object.keys(changes).sort().reduce(function(_, id) {
    if (!editor[Out][id]) editor[Out][id] = makeOutput(id)

    var output = editor[Out][id]
    var change = changes[id]
    if (change === null) editor[Out][id] = null

    writeOutput(output, editor, change)
  }, null)
  editor[In] = patch(editor[In], changes)
}

function post(changes) {
  Object.keys(changes).reduce(function(_, id) {
    var change = changes[id]
    if (change && change.input) {
      send({ to: id, source: change.input })
    }
  }, null)
}

// Function finds modified sections and queues up messegase to an
// eval host. In adition it also renders output views (if they
// do not exist yet) where eval results are written.
var renderOutput = throttle(function render(editor) {
  var delta = calculate(editor)
  var changes = Object.keys(delta).reduce(function(changes, id) {
    var change = delta[id]
    // Only mark change pending if there is some input to be evaled.
    if (change && change.input) change.pending = true
    return changes
  }, delta)

  write(editor, changes)
  post(changes)
}, getRenderRate)

var hideOutput = throttle(function render(editor) {
  var line = editor.getCursor().line
  var state = editor[In]
  var changes = Object.keys(state).reduce(function(delta, id) {
    var value = state[id]
    if (value.line === line) delta[id] = {visible: false}
    else if (!value.visible) delta[id] = {visible: true, line: value.line}

    return delta
  }, [])

  if (changes.length) write(editor, changes)
}, function() { return 200 })


function tooglePlugin(editor, value) {
  if (value) {
    editor[Reciever] = recieve.bind(recieve, editor)
    editor[In] = {}
    editor[Out] = {}
    editor.on("change", renderOutput)
    editor.on("cursorActivity", hideOutput)
    window.addEventListener("client", editor[Reciever], false)
  } else {
    editor.off("change", renderOutput)
    editor.off("cursorActivity", hideOutput)
    window.removeEventListener("client", editor[Reciever], false)
    editor[Reciever] = null
    editor[In] = null
    editor[Out] = null
  }
}

function install(CodeMirror) {
  // Fix constructor property so that it could be accessed from the
  // instance.
  CodeMirror.prototype.constructor = CodeMirror;
  CodeMirror.defaults.interactiveSpeed = 300
  CodeMirror.defaults.interactiveSeparator = /^\/\/ \=\>[^\n]*$/m
  CodeMirror.keyMap.macDefault["Cmd-Enter"] = print
  CodeMirror.keyMap.pcDefault["Ctrl-Enter"] = print

  CodeMirror.defineOption("interactivate", false, tooglePlugin)
}

module.exports = install
