const SmartBuffer = require('smart-buffer').SmartBuffer
const ChunkSection = require('./ChunkSection')
const constants = require('../common/constants')
const BitArray = require('../common/BitArray')
const varInt = require('../common/varInt')
const CommonChunkColumn = require('../common/CommonChunkColumn')
const neededBits = require('../common/neededBits')

const exists = val => val !== undefined

// wrap with func to provide version specific Block
module.exports = (Block, mcData) => {
  return class ChunkColumn extends CommonChunkColumn {
    static get section () { return ChunkSection }
    constructor () {
      super(mcData)
      this.sectionMask = 0
      this.skyLightSent = true
      this.sections = Array(constants.NUM_SECTIONS).fill(null)
      this.biomes = Array(
        constants.SECTION_WIDTH * constants.SECTION_WIDTH
      ).fill(1)
      this.maxBitsPerBlock = neededBits(Object.values(mcData.blocks).reduce((high, block) => Math.max(high, block.maxStateId), 0))
    }

    toJson () {
      return JSON.stringify({
        biomes: this.biomes,
        blockEntities: this.blockEntities,
        sectionMask: this.sectionMask,
        sections: this.sections.map(section => section === null ? null : section.toJson())
      })
    }

    static fromJson (j) {
      const parsed = JSON.parse(j)
      const chunk = new ChunkColumn()
      chunk.biomes = parsed.biomes
      chunk.blockEntities = parsed.blockEntities
      chunk.sectionMask = parsed.sectionMask
      chunk.sections = parsed.sections.map(s => s === null ? null : ChunkSection.fromJson(s))
      return chunk
    }

    initialize (func) {
      const p = { x: 0, y: 0, z: 0 }
      for (p.y = 0; p.y < constants.CHUNK_HEIGHT; p.y++) {
        for (p.z = 0; p.z < constants.SECTION_WIDTH; p.z++) {
          for (p.x = 0; p.x < constants.SECTION_WIDTH; p.x++) {
            const block = func(p.x, p.y, p.z)
            if (block === null) {
              continue
            }
            this.setBlock(p, block)
          }
        }
      }
    }

    getBlock (pos) {
      const block = new Block(this.getBlockType(pos), this.getBiome(pos), this.getBlockData(pos))
      block.light = this.getBlockLight(pos)
      block.skyLight = this.getSkyLight(pos)
      block.entity = this.getBlockEntity(pos)
      return block
    }

    setBlock (pos, block) {
      if (exists(block.type)) { this.setBlockType(pos, block.type) }
      if (exists(block.metadata)) { this.setBlockData(pos, block.metadata) }
      if (exists(block.biome)) { this.setBiome(pos, block.biome.id) }
      if (exists(block.skyLight) && this.skyLightSent) { this.setSkyLight(pos, block.skyLight) }
      if (exists(block.light)) { this.setBlockLight(pos, block.light) }
      if (block.entity) {
        this.setBlockEntity(pos, block.entity)
      } else {
        this.removeBlockEntity(pos)
      }
    }

    getBlockType (pos) {
      return this.getBlockStateId(pos) >> 4
    }

    getBlockData (pos) {
      return this.getBlockStateId(pos) & 15
    }

    getBlockStateId (pos) {
      const section = this.sections[pos.y >> 4]
      return section ? section.getBlock(toSectionPos(pos)) : 0
    }

    getBlockLight (pos) {
      const section = this.sections[pos.y >> 4]
      return section ? section.getBlockLight(toSectionPos(pos)) : 15
    }

    getSkyLight (pos) {
      const section = this.sections[pos.y >> 4]
      return section ? section.getSkyLight(toSectionPos(pos)) : 15
    }

    getBiome (pos) {
      return this.biomes[getBiomeIndex(pos)]
    }

    setBlockType (pos, id) {
      const data = this.getBlockData(pos)
      this.setBlockStateId(pos, (id << 4) | data)
    }

    setBlockData (pos, data) {
      const id = this.getBlockType(pos)
      this.setBlockStateId(pos, (id << 4) | data)
    }

    setBlockStateId (pos, stateId) {
      const sectionIndex = pos.y >> 4
      if (sectionIndex < 0 || sectionIndex >= 16) return

      let section = this.sections[sectionIndex]
      if (!section) {
        // if it's air
        if (stateId === 0) {
          return
        }
        section = new ChunkSection({
          maxBitsPerBlock: this.maxBitsPerBlock
        })
        this.sectionMask |= 1 << sectionIndex
        this.sections[sectionIndex] = section
      }

      section.setBlock(toSectionPos(pos), stateId)
    }

    setBlockLight (pos, light) {
      const section = this.sections[pos.y >> 4]
      return section && section.setBlockLight(toSectionPos(pos), light)
    }

    setSkyLight (pos, light) {
      const section = this.sections[pos.y >> 4]
      return section && section.setSkyLight(toSectionPos(pos), light)
    }

    setBiome (pos, biome) {
      this.biomes[getBiomeIndex(pos)] = biome
    }

    getMask () {
      return this.sectionMask
    }

    // These methods do nothing, and are present only for API compatibility
    dumpBiomes () {

    }

    dumpLight () {

    }

    loadLight () {

    }

    loadBiomes () {

    }

    dump () {
      const smartBuffer = new SmartBuffer()
      this.sections.forEach((section, i) => {
        if (section !== null && !section.isEmpty()) {
          section.write(smartBuffer)
        }
      })

      // write biome data
      this.biomes.forEach(biome => {
        smartBuffer.writeUInt8(biome)
      })

      return smartBuffer.toBuffer()
    }

    load (data, bitMap = 0xffff, skyLightSent = true, fullChunk = true) {
      // make smartbuffer from node buffer
      // so that we doesn't need to maintain a cursor
      const reader = SmartBuffer.fromBuffer(data)

      this.skyLightSent = skyLightSent
      this.sectionMask |= bitMap
      for (let y = 0; y < constants.NUM_SECTIONS; ++y) {
        // does `data` contain this chunk?
        if (!((bitMap >> y) & 1)) {
          // we can skip write a section if it isn't requested
          continue
        }

        // keep temporary palette
        let palette
        let skyLight

        // get number of bits a palette item use
        const bitsPerBlock = reader.readUInt8()

        // check if the section uses a section palette
        if (bitsPerBlock <= constants.MAX_BITS_PER_BLOCK) {
          palette = []
          // get number of palette items
          const numPaletteItems = varInt.read(reader)

          // save each palette item
          for (let i = 0; i < numPaletteItems; ++i) {
            palette.push(varInt.read(reader))
          }
        } else {
          // remove the 0 length signifying the missing palette array
          varInt.read(reader)
          // global palette is used
          palette = null
        }

        // number of items in data array
        const dataArray = new BitArray({
          bitsPerValue: bitsPerBlock > constants.MAX_BITS_PER_BLOCK ? this.maxBitsPerBlock : bitsPerBlock,
          capacity: 4096
        }).readBuffer(reader, varInt.read(reader) * 2)

        const blockLight = new BitArray({
          bitsPerValue: 4,
          capacity: 4096
        }).readBuffer(reader)

        if (skyLightSent) {
          skyLight = new BitArray({
            bitsPerValue: 4,
            capacity: 4096
          }).readBuffer(reader)
        }

        const section = new ChunkSection({
          data: dataArray,
          palette,
          blockLight,
          ...(skyLightSent ? { skyLight } : { skyLight: null }),
          maxBitsPerBlock: this.maxBitsPerBlock
        })
        this.sections[y] = section
      }

      // read biomes
      if (fullChunk) {
        const p = { x: 0, y: 0, z: 0 }
        for (p.z = 0; p.z < constants.SECTION_WIDTH; p.z++) {
          for (p.x = 0; p.x < constants.SECTION_WIDTH; p.x++) {
            this.setBiome(p, reader.readUInt8())
          }
        }
      }
    }
  }
}

function getBiomeIndex (pos) {
  return (pos.z * 16) | pos.x
}

function toSectionPos (pos) {
  return { x: pos.x, y: pos.y & 15, z: pos.z }
}
