/*
typeset.js

Jake Read at the Center for Bits and Atoms
(c) Massachusetts Institute of Technology 2019

This work may be reproduced, modified, distributed, performed, and
displayed for any purpose, but must acknowledge the squidworks and cuttlefish projects.
Copyright is retained and must be preserved. The work is provided as is;
no warranty is provided, and users accept all liability.
*/

// typeset: functional types -> bytes for js -> embedded sys
// bless up @ modern js https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays

let tsdebug = false

// oy,

const checkKey = (type, arr, start) => {
  if (arr[start] !== type.key) {
    console.log('erroneous bytes')
    console.log(arr)
    throw new Error(`mismatched key on phy read: for ${type.name}, find ${arr[start]} instead of ${type.key} ... at ${start} index`)
  }
}

const checkBoolean = (value) => {
  if (typeof value !== 'boolean')
    throw new Error('cannot cast non-boolean to bool at phy')
}

const checkNumber = (value) => {
  if (typeof value !== 'number')
    throw new Error(`cannot cast non-number into physical world "${value}", ${typeof value}`)
}

const checkString = (value) => {
  if (typeof value !== 'string')
    throw new Error(`cannot cast non-string to string at phy! "${value}", "${typeof value}"`)
}

const checkArray = (thing) => {
  if (!Array.isArray(thing))
    throw new Error('this thing is not an array!')
}

const checkUnsigned = (value, bits) => {
  if (value > Math.pow(2, bits))
    throw new Error('value out of byte bounds')
}

const checkSigned = (value, bits) => {
  let comparator = Math.pow(2, bits - 1)
  if (value > comparator || value < -comparator)
    throw new Error('value out of byte bounds')
}

const findPhy = (type) => {
  let phy = TSET.find((cand) => {
    return cand.name === type
  })
  if (phy === undefined)
    // try this... catch err elsewhere
    throw new Error(`could not find phy for datatype: ${type}`)
  return phy
}

const bounds = (value, min, max) => {
  return Math.max(min, Math.min(value, max))
}

const intBounds = (pfloat, min, max) => {
  return Math.round(bounds(pfloat, min, max))
}

// TYPES:
/*
name: string identifier, how we 'specify' what an input / output / state is
key: byte-code for the network. if this, write, and read, don't exist, link can't have one
write: serialize into bytes
read: deserialize
copy: move about within js,
 copies are 'outgoing' ... so typeA.copy.typeB(value) turns the value from
 typeA -> typeB
 the standard is typeA.copy.typeA(value), this is just a memory shuffle
 for the sake of dereferencing: we can write our own types per item
*/

// TODO: TYPES:
/*
we are still not really type-checking on calls to .put(), and should be...
there is probably a slick, clean way to do this
also: would really like to work with typedarrays where they are appropriate

.. *could* imagine some hairbrain walk through these things: if we have, i.e., conversion
from string -> number written, and from number -> uint8, we should be able to
compose a tree of these on the fly ...
*/

const TSET = [
  {
    name: 'boolean',
    key: 32,
    write: function(value) {
      checkBoolean(value)
      let rtarr = [this.key]
      if (value) {
        rtarr.push(1)
      } else {
        rtarr.push(0)
      }
      return rtarr
    },
    read: function(arr, start) {
      checkKey(this, arr, start)
      let item
      if (arr[start + 1] === 1) {
        item = true
      } else if (arr[start + 1] === 0) {
        item = false
      } else {
        throw new Error(`non std boolean byte, ${arr[start + 1]}`)
      }
      return {item: item, increment: 2}
    },
    copy: {
      boolean: function(bool) {
        return bool // direct: a basic js type, so we just pass it on
      },
      number: function(bool) { // converts boolean -> numbers
        if (bool) {
          return 1
        } else {
          return 0
        }
      }
    }
  }, { // booleanArray: 33,
    name: 'byte',
    key: 34,
    write: function(value) {
      checkNumber(value)
      checkUnsigned(value, 8)
      return [this.key, value]
    },
    read: function(arr, start) {
      checkKey(this, arr, start)
      return {
        item: arr[start + 1],
        increment: 2
      }
    },
    copy: {
      byte: function(byte) {
        return byte
      }
    }
  }, {
    name: 'byteArray',
    key: 35,
    write: function(value) {
      checkArray(value)
      let rtarr = writeLenBytes(value.length).concat(value)
      rtarr.unshift(this.key)
      if (tsdebug)
        console.log('byteArray sanity check:', value, 'written as:', rtarr)
      return rtarr
    },
    read: function(arr, start) {
      checkKey(this, arr, start)
      let lb = readLenBytes(arr, start + 1)
      let narr = new Array()
      for (let i = 0; i < lb.len; i++) {
        narr.push(arr[start + 1 + lb.numBytes + i])
      }
      return {
        item: narr,
        increment: lb.len + lb.numBytes + 1
      }
    },
    copy: { // copy:  into another bytearray
      byteArray: function(arr) {
        // TODO would be making bytearrays into buffers, would speed this up ...
        let ret = new Array(arr.length)
        for (let item in arr) {
          ret[item] = arr[item]
        }
        return ret
      },
      reference: function(arr){
        let ret = new Array(arr.length)
        for (let item in arr) {
          ret[item] = arr[item]
        }
        return ret
      }
    }
  }, { // char 36, string 37,
    name: 'string',
    key: 37,
    write: function(str) {
      checkString(str)
      let rtarr = new Array()
      for (let i = 0; i < str.length; i++) {
        rtarr.push(str.charCodeAt(i))
      }
      // length bytes are 7-bit 'msb for continue' numbers ...
      // the -1 because we are not counting the length of the key
      let lb = writeLenBytes(rtarr.length)
      rtarr = lb.concat(rtarr)
      rtarr.unshift(this.key)
      return rtarr
    },
    read: function(arr, start) {
      checkKey(this, arr, start)
      let lb = readLenBytes(arr, start + 1)
      //console.log('lenbytes', lb)
      let str = new String()
      for (let i = 0; i < lb.len; i++) {
        str += String.fromCharCode(arr[start + 1 + lb.numBytes + i])
      }
      return {
        item: str,
        increment: lb.len + lb.numBytes + 1
      }
    },
    copy: {
      string: function(str) {
        return str
      },
      number: function(str) {
        return parseFloat(str)
      },
      boolean: function(str){
        if(str == true){
          return true
        } else {
          return false
        }
      },
      reference: function(str){
        return str
      }
    }
  }, {
    name: 'uint8',
    key: 38,
    write: function(value) {
      checkNumber(value)
      checkUnsigned(value, 8)
      if (value > 255 || value < 0)
        throw new Error('num too large to represent with cast type, will contencate')
        // dont' need to type-buffer this,
      let rtarr = [this.key, value]
      return rtarr
    },
    read: function(arr, start) {
      // assume we're reading out of an array
      // start[] should === key
      if (arr[start] !== this.key)
        throw new Error(`mismatched key on phy read: ${arr[start]}, ${this.key}`)
      if (arr[start + 1] > 255 || arr[start + 1] < 0)
        throw new Error('whaky read-in on uint8')
      return {
        item: arr[start + 1],
        increment: 2
      }
    },
    copy: {
      uint8: function(uint8){
        // by clamping at the copy, we can reduce complexity at read and write ?
        // and improve clarity elsewhere ...
        return bounds(uint8, 0, 255) // clamp number
      },
      uint16: function(uint8){
        // because we always copy-in before copying-out (to other types)
        // we don't have to clamp here again, that's nice.
        // but would to go down to a smaller value ...
        return uint8
      },
      uint32: function(uint8){
        // this could really get exhaustive,
        return uint8
      },
      number: function(uint8){
        return uint8
      }
    }
  }, { // uint8Array 39
    name: 'uint16',
    key: 40,
    write: function(value) {
      if (typeof value !== 'number')
        throw new Error(`cannot cast non-number into physical world "${value}", ${typeof value}`)
      if (value > 65536 || value < 0)
        throw new Error('num too large to represent with cast type, will contencate')
      let tparr = new Uint16Array(1)
      tparr[0] = value
      let btarr = new Uint8Array(tparr.buffer)
      //place
      let rtarr = Array.from(btarr)
      rtarr.unshift(this.key)
      return rtarr
    },
    read: function(arr, start) {
      // assume we're reading out of an array
      // start[] should === key
      if (arr[start] !== this.key)
        throw new Error(`mismatched key on phy read: ${arr[start]}, ${this.key}`)
      let rdarr = arr.slice(start + 1, start + 3)
      let btarr = Uint8Array.from(rdarr)
      if (tsdebug)
        console.log('bytes on read of uint16 (little eadian)', btarr)
        // now make uint32 view on this ...
      let vlarr = new Uint16Array(btarr.buffer)
      if (tsdebug)
        console.log('vlarr', vlarr)
      return {item: vlarr[0], increment: 3}
    },
    copy: {
      uint16: function(uint16){
        return bounds(uint16, 0, 65535)
      },
      uint8: function(uint16){
        return bounds(uint16, 0, 255)
      },
      number: function(uint16){
        return uint16
      }
    }
  }, { // uint16 array 41
    name: 'uint32',
    key: 42,
    write: function(value) {
      if (typeof value !== 'number')
        throw new Error(`cannot cast non-number into physical world "${value}", ${typeof value}`)
      if (value > 4294967296)
        throw new Error('num too large to represent with cast type, will contencate')
      let tparr = new Uint32Array(1)
      tparr[0] = value
      let btarr = new Uint8Array(tparr.buffer)
      //place
      let rtarr = Array.from(btarr)
      rtarr.unshift(this.key)
      if (tsdebug)
        console.log("UINT32 WRITES ARR: ", rtarr, "FOR: ", value)
      return rtarr
    },
    read: function(arr, start) {
      // assume we're reading out of an array
      // start[] should === key
      if (arr[start] !== this.key) {
        console.log("erroneous bytes:", arr)
        console.log("error at byte:", start, "is", arr[start])
        console.log("expected key:", this.key)
        throw new Error(`mismatched key on phy read: ${arr[start]}, ${this.key}`)
      }
      let rdarr = arr.slice(start + 1, start + 5)
      let btarr = Uint8Array.from(rdarr)
      if (tsdebug)
        console.log('bts on read of uint32', btarr)
        // now make uint32 view on this ...
      let vlarr = new Uint32Array(btarr.buffer)
      if (tsdebug)
        console.log("UINT32 READ ARR: ", vlarr[0], "FROM: ", btarr)
      if (tsdebug)
        console.log('vlarr', vlarr)
      return {item: vlarr[0], increment: 5}
    },
    copy: {
      uint32: function(uint32){
        return bounds(uint32, 0, 4294967295)
      },
      number: function(uint32){
        return uint32
      }
    }
  }, // uint32array 43,
  /*
  uint64 44, uint64array 45,
  int8 46, int8array 47,
  int16 48, int16array 49,
  int32 50, int32array 50,
  */
  {
    name: 'int32',
    key: 50,
    write: function(value) {
      if (typeof value !== 'number')
        throw new Error(`cannot cast non-number into physical world "${value}", ${typeof value}`)
      let tparr = new Int32Array(1)
      tparr[0] = value
      let btarr = new Uint8Array(tparr.buffer)
      //place
      let rtarr = Array.from(btarr)
      rtarr.unshift(this.key)
      if (tsdebug)
        console.log("INT32 WRITES ARR: ", rtarr, "FOR: ", value)
      return rtarr
    },
    read: function(arr, start) {
      // assume we're reading out of an array
      // start[] should === key
      if (arr[start] !== this.key)
        throw new Error(`mismatched key on phy read: ${arr[start]}, ${this.key}`)
      let rdarr = arr.slice(start + 1, start + 5)
      let btarr = Uint8Array.from(rdarr)
      if (tsdebug)
        console.log('bts on read of uint32', btarr)
        // now make uint32 view on this ...
      let vlarr = new Int32Array(btarr.buffer)
      if (tsdebug)
        console.log("UINT32 READ ARR: ", vlarr[0], "FROM: ", btarr)
      if (tsdebug)
        console.log('vlarr', vlarr)
      return {item: vlarr[0], increment: 5}
    },
    copy: {
      int32: function(int32){
        return bounds(int32, -2147483647, 2147483647)
      },
      number: function(int32){
        return int32
      }
    }
  },
  /*
  int64 52, int64array 53,
  float32 54, float32array 55,
  float64 56, float64array 57 (these are === javascript 'numbers') ... how to alias ?
  */
  {
    name: 'number',
    key: 56,
    write: function(value) {
      if (typeof value !== 'number')
        throw new Error(`cannot cast non-number into physical world "${value}", ${typeof value}`)
      let tparr = new Float64Array(1)
      tparr[0] = value
      let btarr = new Uint8Array(tparr.buffer)
      // place
      let rtarr = Array.from(btarr)
      rtarr.unshift(this.key)
      return rtarr
    },
    read: function(arr, start) {
      if (arr[start] !== this.key)
        throw new Error(`mismatched key on phy read: ${arr[start]}, ${this.key}`)
      let rdarr = arr.slice(start + 1, start + 9)
      let btarr = Uint8Array.from(rdarr)
      if (tsdebug)
        console.log('bts on read of float64', btarr)
      let vlarr = new Float64Array(btarr.buffer)
      if (tsdebug)
        console.log('vlarr', vlarr)
      return {item: vlarr[0], increment: 9}
    },
    copy: {
      number: function(num){
        return num
      },
      boolean: function(num){
        if(num > 0){
          return true
        } else {
          return false
        }
      },
      uint8: function(num){
        return intBounds(num, 0, 255)
      },
      uint16: function(num){
        return intBounds(num, 0, 65535)
      },
      uint32: function(num){
        return intBounds(num, 0, 4294967295)
      },
      int32: function(num){
        return intBounds(num, -2147483647, 2147483647)
      }
    }
  },
  { // cuttlefish only, not a real pass
    name: 'reference',
    copy: {
      reference: function(ref){
        let type = typeof ref
        if(type === 'string' || type === 'number' || type === 'boolean'){
          console.error('cannot pass core types as a reference')
          return null
        } else {
          return ref
        }
      }
    }
  },
  {
    name: 'object',
    copy: {
      object: function(obj){
        return JSON.parse(JSON.stringify(obj))
      }
    }
  },
  { // cuttlefish only, so no key, read or write fn's
    // this is : https://developer.mozilla.org/en-US/docs/Web/API/ImageData
    name: 'ImageData',
    copy: {
      ImageData: function(imageData){
        return new ImageData(
          new Uint8ClampedArray(imageData.data),
          imageData.width,
          imageData.height
        ) //
      },
      reference: function(imageData){
        return imageData
      }
    }
  },
  {
    name: 'Float32Array',
    copy: {
      Float32Array: function(float32Array) {
        return float32Array.slice();
      }
    }
  },
  {
    name: 'array',
    copy: {
      array: (arr) => [...arr],
      reference: (arr) => {
        return arr
      }
    }
  },
  {
    name: 'MDmseg',
    key: 88,
    write: function(ms) {
      // ok, bless up, we have:
      /*
      p0: 3-arr
      p1: 3-arr
      t: num
      v0: num
      a: num
      // for simplicity, we should write one typedarray
      */
      let f32arr = Float32Array.from([
        ms.p0[0], ms.p0[1], ms.p0[2],
        ms.p1[0], ms.p1[1], ms.p1[2],
        ms.t, ms.v0, ms.a]
      )
      // ok,
      let btarr = new Uint8Array(f32arr.buffer)
      let rtarr = Array.from(btarr)
      rtarr.unshift(this.key)
      return rtarr
    },
    read: function(arr, start) {
      /*
      if (arr[start] !== this.key)
        throw new Error(`mismatched key on phy read: ${arr[start]}, ${this.key}`)
      let rdarr = arr.slice(start + 1, start + 9)
      let btarr = Uint8Array.from(rdarr)
      if (tsdebug)
        console.log('bts on read of float64', btarr)
      let vlarr = new Float64Array(btarr.buffer)
      if (tsdebug)
        console.log('vlarr', vlarr)
      return {item: vlarr[0], increment: 9}
      */
    },
    copy: {
      MDmseg: (mdmseg) => {
        return {
          p0: mdmseg.p0,
          p1: mdmseg.p1,
          t: mdmseg.t,
          v0: mdmseg.v0,
          a: mdmseg.a
        }
      },
      reference: (mdmseg) => {
        return mdmseg
      }
    }
  }
  // etc
] // end TSET

let intTypes = [
  'uint8',
  'uint16',
  'uint32',
  'uint64',
  'int8',
  'int16',
  'int32',
  'int64'
]

let floatTypes = ['number']

const isIntType = (type) => {
  for (let t of intTypes) {
    if (type == t)
      return true
  }
  return false
}

const isFloatType = (type) => {
  for (let t of floatTypes) {
    if (type == t)
      return true
  }
  return false
}

const isNumType = (type) => {
  if (isIntType(type))
    return true
  if (isFloatType(type))
    return true
  return false
}

const writeLenBytes = (len) => {
  // return array of len bytes for this number
  let bts = new Array()
  if (len > 65536) {
    throw new Error('cannot write length bytes for len > 2^16')
  } else {
    // this is little eadian ... right ?
    bts.push(len & 255);
    bts.push((len >> 8) & 255);
  }
  // here to check,
  //if(len > 255){
  //  console.log(`LEN > 255: writes len bytes `, bts[0], bts[1], 'for', len)
  //}
  return bts
}

const readLenBytes = (arr, start) => {
  // need 2 know how many to increment as well,
  let len = (arr[start + 1] << 8) | arr[start]
  // still interested in this,
  //if(len > 255){
  //  console.log(`LEN > 255: reads len bytes `, arr[start], arr[start+1], 'for len', len)
  //}
  return {len: len, numBytes: 2}
}

// heavy mixin of functional programming
const MSGS = {
  writeTo: function(bytes, thing, type, debug) {
    let phy = findPhy(type)
    // try some js type conversion,
    // course correction here: sometimes states that are numbers are saved as strings (json)
    // we can unf- this here,
    if (typeof thing === 'string' && isNumType(type)) {
      //console.warn('patching num')
      if (isIntType(type)) {
        thing = parseInt(thing)
      } else {
        thing = parseFloat(thing)
      }
      //console.log('new num val', thing)
    } else if (typeof thing === 'string' && type === 'boolean') {
      // ha! use (?) for 'truthiness'
      //console.warn('patching bool')
      if (thing == 'true') {
        thing = true
      } else {
        thing = false
      }
      //console.log('new bool val', thing)
    }
    let block = phy.write(thing)
    if (debug)
      console.log(`writing for type ${type} and thing '${thing}' the following block of bytes`, block)
      // write-in to msg like this
    // this *must be* slow AF, pls correct
    block.forEach((byte) => {
      bytes.push(byte)
    })
  },
  readFrom: function(bytes, place, type) {
    let phy = findPhy(type)
    // check that type exists at place, rip it oot and return it
    return phy.read(bytes, place)
  },
  readListFrom: function(bytes, place, type) {
    // using this where I expect a lit of values, i.e. the addLink(str,str,str,str) arguments,
    // plucks thru, continuing to pull values as long as the next in the serialized list is of
    // the right type
    let phy = findPhy(type)
    // the list of items,
    let list = new Array()
    while (place < bytes.length) {
      let res = phy.read(bytes, place)
      list.push(res.item)
      place += res.increment
      if (bytes[place] !== phy.key)
        break
        // this could throw us into infinite loops, so
      if (res.increment < 1)
        throw new Error('dangerous increment while reading list')
    }
    if (list.length < 1)
      throw new Error('reading list, found no items...')
    if (tsdebug)
      console.log('read list as', list)
    return list
  }
}

// typically: call, response expected
// manager keys
const MK = {
  // bzzt
  ERR: 254, // (str) message
  // heartbeats, wakeup
  HELLO: 231, // (eom)
  // request a top-level description
  QUERY: 251, // (eom)
  BRIEF: 250, // (str) name of interpreter, # hunks, # links (and then begin firing list back)
  // please show what is available
  REQLISTAVAIL: 249, // (eom)
  LISTOFAVAIL: 248, // (list)(str) names 'dirs/like/this' (includes programs ?) (this might be multiple packets?)
  // business ... we should be able to centralize all control w/i view.js if we can write these
  REQADDHUNK: 247, // (str) name
  REQNAMECHANGE: 246,
  HUNKALIVE: 245, // (hunkdescription): name, id, inputlist, outputlist, statelist
  HUNKREPLACE: 244,
  REQSTATECHANGE: 243,
  HUNKSTATECHANGE: 242,
  REQRMHUNK: 241, // (str) id
  HUNKREMOVED: 240, // (str) id
  REQADDLINK: 239, // (str) id, (str) outname, (str) id, (str) inname
  LINKALIVE: 238, // (str) id, (str) outname, (str) id, (str) inname
  REQRMLINK: 237, // (str) id, (str) outname, (str) id, (str) inname
  LINKREMOVED: 236, // (str) id, (str) outname, (str) id, (str) inname
  // to id,
  MSGID: 235
}

// hunk description keys,
const HK = {
  NAME: 253,
  TYPE: 252,
  IND: 251,
  DESCR: 250,
  INPUT: 249,
  OUTPUT: 247,
  CONNECTIONS: 246,
  CONNECTION: 245,
  STATE: 244
}

// link keys,
const LK = {
  ACK: 254,
  HELLO: 253 // ~ binary solo ~
}

// should write out as list of pairs ?
// or write fn to do key(stringtype)

export {
  TSET,
  MK, // manager keys
  HK, // hunk def keys
  LK, // link keys
  MSGS,
  findPhy,
  isIntType,
  isFloatType,
  isNumType
}