/*

hookup,

*/

// HEADER
import {
  Hunkify,
  Input,
  Output,
  State
} from './hunks.js'
// END HEADER

import {
  TSET,
  LK,
  MSGS,
  findPhy
} from '../typeset.js'

function Link() {
  Hunkify(this)

  let debug = false

  let dtin = new Input('byteArray', 'data', this)
  this.inputs.push(dtin)

  let dtout = new Output('byteArray', 'data', this)
  this.outputs.push(dtout)

  // needs 2 trak status
  let isActive = new State('boolean', 'isActive', false)
  let otherLink = new State('uint16', 'otherLink', 0)
  this.states.push(isActive, otherLink)
  // default messages -> manager, besides also data link
  let inputList = new State('string', 'inputList', "mgrMsgs (byteArray)")
  let outputList = new State('string', 'outputList', "mgrMsgs (byteArray)")
  this.states.push(inputList, outputList)

  /* ---------------------------    ---------------------------- */
  /* ------------------ OP ON KEYS FROM STATE ------------------ */
  /* ---------------------------    ---------------------------- */

  let getTypeAndNameKeys = (str) => {
    let keys = str.split(',')
    // console.log('keys', keys)
    let ks = new Array()
    for (let i in keys) {
      let tk = keys[i].substring(keys[i].indexOf('(') + 1, keys[i].indexOf(')'))
      let nk = keys[i].substring(0, keys[i].indexOf(' ('))
      if (nk[0] == ' ') nk = nk.substring(1)
      if (tk.length < 2 || nk.length < 2) {
        this.log('bad key pair on inputs / outputs at link')
      } else {
        ks.push({
          typeKey: tk,
          nameKey: nk
        })
      }
    }
    return ks
  }

  let swapLists = (newList, input) => {
    // list,
    let nks = getTypeAndNameKeys(newList)
    // old list,
    let oks
    if (input) {
      oks = this.inputs
    } else {
      oks = this.outputs
    }
    // one by one, down the list we go
    for (let kp = 0; kp < nks.length; kp++) {
      // we'l walk the inputs array in step,
      // if the input we want already exists in oks, we'll place that
      let ioe = -1 // 'index of existing placement'
      for (let io in oks) {
        if (oks[io].name === nks[kp].nameKey && oks[io].type === nks[kp].typeKey) {
          ioe = io
          continue
        }
      }
      if (ioe >= 0) {
        if (input) {
          this.inputs[kp + 1] = oks[ioe]
        } else {
          this.outputs[kp + 1] = oks[ioe]
        }
      } else {
        // the object doesn't already exist,
        if (input) {
          // we can only make types we have serialization routines for:
          let phy = findPhy(nks[kp].typeKey)
          if(phy.key && phy.read){
            this.inputs[kp + 1] = new Input(nks[kp].typeKey, nks[kp].nameKey, this, true)
          }
        } else {
          let phy = findPhy(nks[kp].typeKey)
          if(phy.key && phy.write){
            this.outputs[kp + 1] = new Output(nks[kp].typeKey, nks[kp].nameKey, this, true)
          }
        }
      }
    }

    if (input) {
      while (this.inputs.length - 1 > nks.length) {
        this.inputs.pop()
      }
    } else {
      while (this.outputs.length - 1 > nks.length) {
        this.outputs.pop()
      }
    }
  }

  // now the changes,
  inputList.onChange = (value) => {
    // OK: back up to this ...
    swapLists(value, true)
    // we have to report this ...
    this.mgr.evaluateHunk(this)
    // if OK
    inputList.set(value)
  }

  outputList.onChange = (value) => {
    swapLists(value, false)
    this.mgr.evaluateHunk(this)
    // this has to follow on, to complete the promise ?
    // this necessitates that we write a manager-message-buffer on embedded,
    // and breaks one-program-item-at-a-time rules
    outputList.set(value)
  }

  /* ---------------------------    ---------------------------- */
  /* -------------------------- INIT --------------------------- */
  /* ---------------------------    ---------------------------- */

  this.init = () => {
    // since we know this needs to default to nc, and many programs
    // will save with these states set 'true', we reset them now.
    //otherLink.set(0)
    //isActive.set(false)
    otherLink.value = 0
    isActive.value = false
    // just add in order
    let ipKeys = getTypeAndNameKeys(inputList.value)
    for (let kp of ipKeys) {
      if(findPhy(kp.typeKey).key && findPhy(kp.typeKey).read){
        this.inputs.push(new Input(kp.typeKey, kp.nameKey, this, true))
      }
    }

    let opKeys = getTypeAndNameKeys(outputList.value)
    for (let kp of opKeys) {
      if(findPhy(kp.typeKey).key && findPhy(kp.typeKey).write){
        this.outputs.push(new Output(kp.typeKey, kp.nameKey, this, true))
      }
    }

  }

  /* ---------------------------    ---------------------------- */
  /* ---------------------- LINK STATE ------------------------- */
  /* ---------------------------    ---------------------------- */

  // a wait-state
  let isOpening = false

  let openup = (reqResponse) => {
    // exit when already opening, this is called
    // whenever an input port is occupied (wanting to send)
    if (isOpening) return
    // ??
    let msg = [LK.HELLO]
    MSGS.writeTo(msg, this.ind, 'uint16')
    if (reqResponse) {
      // if we're already open, we gucc
      MSGS.writeTo(msg, true, 'boolean')
      //console.log('link trying to open up', msg)
      isOpening = true
    } else {
      // otherwise we need to ask for a hello in return
      MSGS.writeTo(msg, false, 'boolean')
      // console.log('link replying to open up', msg)
    }
    dtout.put(msg)
  }

  let dataout = (data) => {
    if (!isActive.value) {
      console.error('attempt to put on not-active link')
    } else {
      if (!Array.isArray(data)) console.error('non-array put at link')
      dtout.put(data)
    }
  }

  /* ---------------------------    ---------------------------- */
  /* ----------------------- SERIALIZE ------------------------- */
  /* ---------------------------    ---------------------------- */

  // deserialize msgs: data is an array of bytes
  let demsg = (data) => {
    if (!Array.isArray(data)) {
      console.log(data)
      throw new Error(`link demsg receives non-array, logged above`)
    }
    // WRITE IT
    let msg = {}
    // this is quick here, but in c ...
    if (data[0] === LK.HELLO) {
      //console.log('demsg for hello', data)
      msg.isHello = true
      msg.otherLink = MSGS.readFrom(data, 1, 'uint16').item
      msg.reqReturn = MSGS.readFrom(data, 4, 'boolean').item
      return msg
    }
    // data[0] is the route, the link we need to bump on
    msg.port = data[0]
    // check for ack,
    if (data[1] === LK.ACK) {
      msg.isAck = true
      return msg
    }
    // otherwise, get phy and write out
    let phy = TSET.find((item) => {
      return item.key === data[1]
    })
    if (phy === undefined) throw new Error(`type not found at deserialization for expected key ${data[1]}`)
    msg.data = phy.read(data, 1).item
    if (debug) console.log('demsg:', msg)
    return msg
  }

  let outbuffer = new Array()

  // this ...
  let ack = (port) => {
    let msg = new Array()
    msg.push(port, LK.ACK)
    outbuffer.push(msg)
  }

  // serialize messages:
  let sermsg = (port, data, type) => {
    if (typeof port !== 'number' || port > 254) throw new Error('port is no good at serialize')
    // we r ready,
    let msg = [port]
    MSGS.writeTo(msg, data, type)
    if (debug) console.log('LINK sermsg to outbuffer', msg)
    outbuffer.push(msg)
  }

  this.loop = () => {
    // (1) check for data
    if (dtin.io()) {
      // if there's data on the line, we might be opening or operating ...
      // pulls every time, this is ok because we trust the other link
      // to be flow-controlling: this should either be for an open port or
      // an ack,
      // pull it and deserialize it,
      let msg = demsg(dtin.get())
      // ack,
      if (msg.isAck) {
        if (debug) console.log('link ack conf on ', msg.port)
        // AN ACK
        if (this.inputs[msg.port].isNetClear) {
          throw new Error('received ack on unexpected port')
        }
        // is clear upstream
        this.inputs[msg.port].icus = true;
      } else if (msg.isHello) {
        // HELLO OTHERLINK
        // if we're not open, now we are
        if (!isActive.value) {
          isActive.set(true)
        }
        isOpening = false
        otherLink.set(msg.otherLink)
        if (msg.reqReturn) {
          openup(false)
        }
      } else {
        // not an ack or a hello, do regular msg stuff
        // check port existence
        if (this.outputs[msg.port] === undefined) {
          // new approach: blind ackit
          ack(msg.port)
          console.warn('blind ack')
          return
          //throw new Error(`link receives message for port not listed: ${msg.port}`)
        }
        // not an ack, for port, if open, put data
        if (!(this.outputs[msg.port].io())) {
          // clear ahead, typecheck and put
          if (debug) console.log('link putting', msg.data, 'on', msg.port)
          this.outputs[msg.port].put(msg.data)
          this.outputs[msg.port].needsAck = true
          // and ack when it is pulled off,
        } else {
          // oboy: we pulled it off of the link, so
          console.log(`WARN: link receives message for occupied port: ${msg.port}, ${this.outputs[msg.port].name}`)
          console.log('the msg', msg)
        }
      }
      // this is an array, we have to deserialize it
    } // end if(dataIn is occupied)

    // (2) check if we can put
    if (!(dtout.io()) && outbuffer.length > 0) { // if we can send things to the world, do so
      if (!isActive.value) {
        // clear-to-send upstream, we don't have sync state, so
        // want to open, want a reply
        openup(true)
        return
      }
      // because of looping sys, we can only do this once per turn
      let turn = outbuffer.shift()
      if (debug) console.log('LINK OUT ->>', turn)
      dataout(turn)
    }

    // (3) check if we can ack, if data has been consumed
    for (let o = 1; o < this.outputs.length; o++) {
      // if now clear, ack back
      if (this.outputs[o].needsAck && !this.outputs[o].io()) {
        // CLEANUP: .needsAck to false -> ack fn ?
        this.outputs[o].needsAck = false
        ack(o)
      }
    }

    // (4) look at inputs and see if we can put anything on the line
    for (let i = 1; i < this.inputs.length; i++) {
      // only pull off of inputs if we are known to be clear downstream
      if (this.inputs[i].icus && this.inputs[i].io()) {
        if (!isActive.value) {
          // nc, but wants to send, so ... and don't pull offline yet
          // so ping to open up,
          openup(true)
          return
        }
        let data = this.inputs[i].get()
        if (debug) console.log(`link pulling message from input ${i}, data is ${data}`)
        // now we must wait for ack,
        this.inputs[i].icus = false;
        sermsg(i, data, this.inputs[i].type)
      }
    }

  } // end loop
}

// FOOTER
export default Link
// END FOOTER