const { getFieldInfo, tryCatch } = require('./utils')
const reduce = require('lodash.reduce')
const Validator = require('protodef-validator')

function isFieldInfo (type) {
  return typeof type === 'string' ||
    (Array.isArray(type) && typeof type[0] === 'string') ||
    type.type
}

function findArgs (acc, v, k) {
  if (typeof v === 'string' && v.charAt(0) === '$') { acc.push({ path: k, val: v.substr(1) }) } else if (Array.isArray(v) || typeof v === 'object') { acc = acc.concat(reduce(v, findArgs, []).map((v) => ({ path: k + '.' + v.path, val: v.val }))) }
  return acc
}

function setField (path, val, into) {
  const c = path.split('.').reverse()
  while (c.length > 1) {
    into = into[c.pop()]
  }
  into[c.pop()] = val
}

function extendType (functions, defaultTypeArgs) {
  const json = JSON.stringify(defaultTypeArgs)
  const argPos = reduce(defaultTypeArgs, findArgs, [])
  function produceArgs (typeArgs) {
    const args = JSON.parse(json)
    argPos.forEach((v) => {
      setField(v.path, typeArgs[v.val], args)
    })
    return args
  }
  return [function read (buffer, offset, typeArgs, context) {
    return functions[0].call(this, buffer, offset, produceArgs(typeArgs), context)
  }, function write (value, buffer, offset, typeArgs, context) {
    return functions[1].call(this, value, buffer, offset, produceArgs(typeArgs), context)
  }, function sizeOf (value, typeArgs, context) {
    if (typeof functions[2] === 'function') { return functions[2].call(this, value, produceArgs(typeArgs), context) } else { return functions[2] }
  }]
}

class ProtoDef {
  constructor (validation = true) {
    this.types = {}
    this.validator = validation ? new Validator() : null
    this.addDefaultTypes()
  }

  addDefaultTypes () {
    this.addTypes(require('./datatypes/numeric'))
    this.addTypes(require('./datatypes/utils'))
    this.addTypes(require('./datatypes/structures'))
    this.addTypes(require('./datatypes/conditional'))
  }

  addProtocol (protocolData, path) {
    const self = this
    function recursiveAddTypes (protocolData, path) {
      if (protocolData === undefined) { return }
      if (protocolData.types) { self.addTypes(protocolData.types) }
      recursiveAddTypes(protocolData?.[path[0]], path.slice(1))
    }

    if (this.validator) { this.validator.validateProtocol(protocolData) }

    recursiveAddTypes(protocolData, path)
  }

  addType (name, functions, validate = true) {
    if (functions === 'native') {
      if (this.validator) { this.validator.addType(name) }
      return
    }
    if (isFieldInfo(functions)) {
      if (this.validator) {
        if (validate) { this.validator.validateType(functions) }
        this.validator.addType(name)
      }

      const { type, typeArgs } = getFieldInfo(functions)
      this.types[name] = typeArgs ? extendType(this.types[type], typeArgs) : this.types[type]
    } else {
      if (this.validator) {
        if (functions[3]) {
          this.validator.addType(name, functions[3])
        } else { this.validator.addType(name) }
      }

      this.types[name] = functions
    }
  }

  addTypes (types) {
    Object.keys(types).forEach((name) => this.addType(name, types[name], false))
    if (this.validator) {
      Object.keys(types).forEach((name) => {
        if (isFieldInfo(types[name])) {
          this.validator.validateType(types[name])
        }
      })
    }
  }

  setVariable (key, val) {
    this.types[key] = val
  }

  read (buffer, cursor, _fieldInfo, rootNodes) {
    const { type, typeArgs } = getFieldInfo(_fieldInfo)
    const typeFunctions = this.types[type]
    if (!typeFunctions) { throw new Error('missing data type: ' + type) }
    return typeFunctions[0].call(this, buffer, cursor, typeArgs, rootNodes)
  }

  write (value, buffer, offset, _fieldInfo, rootNode) {
    const { type, typeArgs } = getFieldInfo(_fieldInfo)
    const typeFunctions = this.types[type]
    if (!typeFunctions) { throw new Error('missing data type: ' + type) }
    return typeFunctions[1].call(this, value, buffer, offset, typeArgs, rootNode)
  }

  sizeOf (value, _fieldInfo, rootNode) {
    const { type, typeArgs } = getFieldInfo(_fieldInfo)
    const typeFunctions = this.types[type]
    if (!typeFunctions) {
      throw new Error('missing data type: ' + type)
    }
    if (typeof typeFunctions[2] === 'function') {
      return typeFunctions[2].call(this, value, typeArgs, rootNode)
    } else {
      return typeFunctions[2]
    }
  }

  createPacketBuffer (type, packet) {
    const length = tryCatch(() => this.sizeOf(packet, type, {}),
      (e) => {
        e.message = `SizeOf error for ${e.field} : ${e.message}`
        throw e
      })
    const buffer = Buffer.allocUnsafe(length)
    tryCatch(() => this.write(packet, buffer, 0, type, {}),
      (e) => {
        e.message = `Write error for ${e.field} : ${e.message}`
        throw e
      })
    return buffer
  }

  parsePacketBuffer (type, buffer, offset = 0) {
    const { value, size } = tryCatch(() => this.read(buffer, offset, type, {}),
      (e) => {
        e.message = `Read error for ${e.field} : ${e.message}`
        throw e
      })
    return {
      data: value,
      metadata: {
        size
      },
      buffer: buffer.slice(0, size),
      fullBuffer: buffer
    }
  }
}

module.exports = ProtoDef
