const BitArray = require('./BitArrayNoSpan')
const constants = require('./constants')
const neededBits = require('./neededBits')
const varInt = require('./varInt')

class DirectPaletteContainer {
  constructor (options) {
    this.noSizePrefix = options?.noSizePrefix // 1.21.5+ writes no size prefix before chunk containers, it's computed dynamically to save 1 byte
    this.data = new BitArray({
      bitsPerValue: options?.bitsPerValue ?? constants.GLOBAL_BITS_PER_BLOCK,
      capacity: options?.capacity ?? constants.BLOCK_SECTION_VOLUME
    })
  }

  get (index) {
    return this.data.get(index)
  }

  set (index, value) {
    this.data.set(index, value)
    return this
  }

  write (smartBuffer) {
    smartBuffer.writeUInt8(this.data.bitsPerValue)
    if (!this.noSizePrefix) varInt.write(smartBuffer, this.data.length())
    this.data.writeBuffer(smartBuffer)
  }

  readBuffer (smartBuffer, bitsPerValue) {
    const longs = this.noSizePrefix
      ? Math.ceil(this.data.capacity / Math.floor(64 / bitsPerValue))
      : varInt.read(smartBuffer)
    this.data.readBuffer(smartBuffer, longs * 2)
    return this
  }

  toJson () {
    return JSON.stringify({
      type: 'direct',
      data: this.data.toJson()
    })
  }

  static fromJson (j) {
    const parsed = JSON.parse(j)
    return new DirectPaletteContainer({
      noSizePrefix: parsed.noSizePrefix,
      data: BitArray.fromJson(parsed.data),
      bitsPerValue: parsed.bitsPerValue
    })
  }
}

class IndirectPaletteContainer {
  constructor (options) {
    this.noSizePrefix = options?.noSizePrefix
    this.data = options?.data ?? new BitArray({
      bitsPerValue: options?.bitsPerValue ?? constants.MIN_BITS_PER_BLOCK,
      capacity: options?.capacity ?? constants.BLOCK_SECTION_VOLUME
    })

    this.palette = options?.palette ?? [0]
    this.maxBits = options?.maxBits ?? constants.MAX_BITS_PER_BLOCK
    this.maxBitsPerBlock = options?.maxBitsPerBlock ?? constants.MAX_BITS_PER_BLOCK
  }

  get (index) {
    return this.palette[this.data.get(index)]
  }

  set (index, value) {
    let paletteIndex = this.palette.indexOf(value)
    if (paletteIndex < 0) {
      paletteIndex = this.palette.length
      this.palette.push(value)
      const bitsPerValue = neededBits(paletteIndex)
      if (bitsPerValue > this.data.bitsPerValue) {
        if (bitsPerValue <= this.maxBits) {
          this.data = this.data.resizeTo(bitsPerValue)
        } else {
          return this.convertToDirect(this.maxBitsPerBlock).set(index, value)
        }
      }
    }
    this.data.set(index, paletteIndex)
    return this
  }

  convertToDirect (bitsPerValue) {
    const direct = new DirectPaletteContainer({
      noSizePrefix: this.noSizePrefix,
      bitsPerValue: bitsPerValue ?? constants.GLOBAL_BITS_PER_BLOCK,
      capacity: this.data.capacity
    })
    for (let i = 0; i < this.data.capacity; ++i) {
      direct.data.set(i, this.get(i))
    }
    return direct
  }

  write (smartBuffer) {
    smartBuffer.writeUInt8(this.data.bitsPerValue)
    varInt.write(smartBuffer, this.palette.length)
    for (const paletteElement of this.palette) {
      varInt.write(smartBuffer, paletteElement)
    }
    if (!this.noSizePrefix) varInt.write(smartBuffer, this.data.length())
    this.data.writeBuffer(smartBuffer)
  }

  readBuffer (smartBuffer, bitsPerValue) {
    const longs = this.noSizePrefix
      ? Math.ceil(this.data.capacity / Math.floor(64 / bitsPerValue))
      : varInt.read(smartBuffer)
    this.data.readBuffer(smartBuffer, longs * 2)
    return this
  }

  toJson () {
    return JSON.stringify({
      type: 'indirect',
      palette: this.palette,
      maxBits: this.maxBits,
      maxBitsPerBlock: this.maxBitsPerBlock,
      data: this.data.toJson()
    })
  }

  static fromJson (j) {
    const parsed = JSON.parse(j)
    return new IndirectPaletteContainer({
      noSizePrefix: parsed.noSizePrefix,
      palette: parsed.palette,
      maxBits: parsed.maxBits,
      maxBitsPerBlock: parsed.maxBitsPerBlock,
      data: BitArray.fromJson(parsed.data)
    })
  }
}

class SingleValueContainer {
  constructor (options) {
    this.noSizePrefix = options?.noSizePrefix
    this.value = options?.value ?? 0
    this.bitsPerValue = options?.bitsPerValue ?? constants.MIN_BITS_PER_BLOCK
    this.capacity = options?.capacity ?? constants.BLOCK_SECTION_VOLUME
    this.maxBits = options?.maxBits ?? constants.MAX_BITS_PER_BLOCK
    this.maxBitsPerBlock = options?.maxBitsPerBlock ?? constants.MAX_BITS_PER_BLOCK
  }

  get (index) {
    return this.value
  }

  set (index, value) {
    if (value === this.value) { return this }

    const data = new BitArray({
      bitsPerValue: this.bitsPerValue,
      capacity: this.capacity
    })
    data.set(index, 1)

    return new IndirectPaletteContainer({
      noSizePrefix: this.noSizePrefix,
      data,
      palette: [this.value, value],
      capacity: this.capacity,
      bitsPerValue: this.bitsPerValue,
      maxBits: this.maxBits,
      maxBitsPerBlock: this.maxBitsPerBlock
    })
  }

  write (smartBuffer) {
    smartBuffer.writeUInt8(0) // bitsPerValue is 0 for single value
    varInt.write(smartBuffer, this.value)
    if (!this.noSizePrefix) smartBuffer.writeUInt8(0)
  }

  toJson () {
    return JSON.stringify({
      type: 'single',
      value: this.value,
      bitsPerValue: this.bitsPerValue,
      capacity: this.capacity,
      maxBits: this.maxBits,
      maxBitsPerBlock: this.maxBitsPerBlock
    })
  }

  static fromJson (j) {
    const parsed = JSON.parse(j)
    return new SingleValueContainer({
      noSizePrefix: parsed.noSizePrefix,
      value: parsed.value,
      bitsPerValue: parsed.bitsPerValue,
      capacity: parsed.capacity,
      maxBits: parsed.maxBits,
      maxBitsPerBlock: parsed.maxBitsPerBlock
    })
  }
}

function containerFromJson (j) {
  const parsed = JSON.parse(j)
  if (parsed.type === 'direct') {
    return new DirectPaletteContainer({
      noSizePrefix: parsed.noSizePrefix,
      data: BitArray.fromJson(parsed.data)
    })
  } else if (parsed.type === 'indirect') {
    return new IndirectPaletteContainer({
      noSizePrefix: parsed.noSizePrefix,
      palette: parsed.palette,
      maxBits: parsed.maxBits,
      data: BitArray.fromJson(parsed.data),
      maxBitsPerBlock: parsed.maxBitsPerBlock
    })
  } else if (parsed.type === 'single') {
    return new SingleValueContainer({
      noSizePrefix: parsed.noSizePrefix,
      value: parsed.value,
      bitsPerValue: parsed.bitsPerValue,
      capacity: parsed.capacity,
      maxBits: parsed.maxBits,
      maxBitsPerBlock: parsed.maxBitsPerBlock
    })
  }
  return undefined
}

module.exports = {
  SingleValueContainer,
  IndirectPaletteContainer,
  DirectPaletteContainer,
  fromJson: containerFromJson
}
