const zlib = require('zlib')

const { ProtoDefCompiler } = require('protodef').Compiler

const beNbtJson = JSON.stringify(require('./nbt.json'))
const leNbtJson = beNbtJson.replace(/([iuf][0-7]+)/g, 'l$1')
const varintJson = JSON.stringify(require('./nbt-varint.json')).replace(/([if][0-7]+)/g, 'l$1')

function createProto (type) {
  const compiler = new ProtoDefCompiler()
  compiler.addTypes(require('./compiler-compound'))
  compiler.addTypes(require('./compiler-tagname'))
  let proto = beNbtJson
  if (type === 'littleVarint') {
    compiler.addTypes(require('./compiler-zigzag'))
    proto = varintJson
  } else if (type === 'little') {
    proto = leNbtJson
  }
  compiler.addTypesToCompile(JSON.parse(proto))
  return compiler.compileProtoDefSync()
}

const protoBE = createProto('big')
const protoLE = createProto('little')
const protoVarInt = createProto('littleVarint')

const protos = {
  big: protoBE,
  little: protoLE,
  littleVarint: protoVarInt
}

function writeUncompressed (value, proto = 'big') {
  if (proto === true) proto = 'little'
  return protos[proto].createPacketBuffer('nbt', value)
}

function parseUncompressed (data, proto = 'big') {
  if (proto === true) proto = 'little'
  return protos[proto].parsePacketBuffer('nbt', data, data.startOffset).data
}

const hasGzipHeader = function (data) {
  let result = true
  if (data[0] !== 0x1f) result = false
  if (data[1] !== 0x8b) result = false
  return result
}

const hasBedrockLevelHeader = (data) =>
  data[1] === 0 && data[2] === 0 && data[3] === 0

async function parseAs (data, type) {
  if (hasGzipHeader(data)) {
    data = await new Promise((resolve, reject) => {
      zlib.gunzip(data, (error, uncompressed) => {
        if (error) reject(error)
        else resolve(uncompressed)
      })
    })
  }
  const parsed = protos[type].parsePacketBuffer('nbt', data, data.startOffset)
  parsed.metadata.buffer = data
  parsed.type = type
  return parsed
}

async function parse (data, format, callback) {
  let fmt = null
  if (typeof format === 'function') {
    callback = format
  } else if (format === true || format === 'little') {
    fmt = 'little'
  } else if (format === 'big') {
    fmt = 'big'
  } else if (format === 'littleVarint') {
    fmt = 'littleVarint'
  } else if (format) {
    throw new Error('Unrecognized format: ' + format)
  }

  data.startOffset = data.startOffset || 0

  if (!fmt && !data.startOffset) {
    if (hasBedrockLevelHeader(data)) { // bedrock level.dat header
      data.startOffset += 8 // skip + 8 bytes
      fmt = 'little'
    }
  }

  // if the format is specified, parse
  if (fmt) {
    try {
      const res = await parseAs(data, fmt)
      if (callback) callback(null, res.data, res.type, res.metadata)
      return { parsed: res.data, type: res.type, metadata: res.metadata }
    } catch (e) {
      if (callback) return callback(e)
      else throw e
    }
  }

  // else try to deduce file type

  // Check if we decoded properly: the EOF should match end of the buffer,
  // or there should be more tags to read, else throw unexpected EOF
  const verifyEOF = ({ buffer, size }) => {
    const readLen = size
    const bufferLen = buffer.length - buffer.startOffset
    const lastByte = buffer[readLen + buffer.startOffset]
    const nextNbtTag = lastByte === 0x0A
    if (readLen < bufferLen && !nextNbtTag) {
      throw new Error(`Unexpected EOF at ${readLen}: still have ${bufferLen - readLen} bytes to read !`)
    }
  }

  // Try to parse as all formats until something passes
  let ret = null
  try {
    ret = await parseAs(data, 'big')
    verifyEOF(ret.metadata)
  } catch (e) {
    try {
      ret = await parseAs(data, 'little')
      verifyEOF(ret.metadata)
    } catch (e2) {
      try {
        ret = await parseAs(data, 'littleVarint')
        verifyEOF(ret.metadata)
      } catch (e3) {
        if (callback) return callback(e)
        else throw e // throw error decoding as big endian
      }
    }
  }

  if (callback) callback(null, ret.data, ret.type, ret.metadata)
  return { parsed: ret.data, type: ret.type, metadata: ret.metadata }
}

function simplify (data) {
  function transform (value, type) {
    if (type === 'compound') {
      return Object.keys(value).reduce(function (acc, key) {
        acc[key] = simplify(value[key])
        return acc
      }, {})
    }
    if (type === 'list') {
      return value.value.map(function (v) { return transform(v, value.type) })
    }
    return value
  }
  return transform(data.value, data.type)
}

const builder = {
  bool (value = false) { return { type: 'bool', value } },
  short (value) { return { type: 'short', value } },
  byte (value) { return { type: 'byte', value } },
  string (value) { return { type: 'string', value } },
  comp (value, name = '') { return { type: 'compound', name, value } },
  int (value) { return { type: 'int', value } },
  float (value) { return { type: 'float', value } },
  double (value) { return { type: 'double', value } },
  long (value) { return { type: 'long', value } },
  list (value) {
    const type = value?.type ?? 'end'
    return { type: 'list', value: { type, value: value?.value ?? [] } }
  },
  byteArray (value = []) { return { type: 'byteArray', value } },
  shortArray (value = []) { return { type: 'shortArray', value } },
  intArray (value = []) { return { type: 'intArray', value } },
  longArray (value = []) { return { type: 'longArray', value } }
}

module.exports = {
  writeUncompressed,
  parseUncompressed,
  simplify,
  hasBedrockLevelHeader,
  parse,
  parseAs,
  proto: protoBE,
  protoLE,
  protos,
  TagType: require('./typings/tag-type'),
  ...builder
}
