// 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
}