import EnumSkyBlock from "../../util/EnumSkyBlock.js";
import Block from "./block/Block.js";
import World from "./World.js";
import ChunkSection from "./ChunkSection.js";
import * as THREE from "../../../../../../libraries/three.module.js";

export default class Chunk {

    static SECTION_AMOUNT = 16;

    constructor(world, x, z) {
        this.world = world;
        this.x = x;
        this.z = z;

        this.group = new THREE.Object3D();
        this.group.matrixAutoUpdate = false;
        this.group.chunkX = x;
        this.group.chunkZ = z;

        this.loaded = false;
        this.isTerrainPopulated = false;

        // Initialize sections
        this.sections = [];
        for (let y = 0; y < Chunk.SECTION_AMOUNT; y++) {
            let section = new ChunkSection(world, this, x, y, z);

            this.sections[y] = section;
            this.group.add(section.group);
        }

        // Create height map
        this.heightMap = [];
    }

    generateSkylightMap() {
        // Calculate height map
        for (let x = 0; x < 16; x++) {
            for (let z = 0; z < 16; z++) {
                this.setHeightAt(x, z, 0);
                this.updateHeightMap(x, World.TOTAL_HEIGHT, z);
            }
        }

        // Update light of neighbor blocks
        for (let x = 0; x < 16; x++) {
            for (let z = 0; z < 16; z++) {
                this.notifyNeighbors(x, z);
            }
        }

        // Rebuild all sections
        this.setModifiedAllSections();
    }

    generateBlockLightMap() {
        let targetY = 32;
        for (let x = 0; x < 16; x++) {
            for (let z = 0; z < 16; z++) {
                for (let y = 0; y < World.TOTAL_HEIGHT; y++) {
                    let section = this.getSection(y >> 4);

                    let typeId = section.getBlockAt(x, y & 15, z);
                    let block = Block.getById(typeId);
                    let blockLight = typeId === 0 ? 0 : block.getLightValue();

                    if (blockLight > 0) {
                        section.setLightAt(EnumSkyBlock.BLOCK, x, y & 15, z, blockLight);
                    }
                }

                let level = 15;
                for (let y = targetY - 2; y < 128 && level > 0;) {
                    y++;

                    let section = this.getSection(y >> 4);

                    let typeId = section.getBlockAt(x, y & 15, z);
                    let block = Block.getById(typeId);

                    let opacity = typeId === 0 ? 0 : block.getOpacity();
                    let blockLight = typeId === 0 ? 0 : block.getLightValue();

                    if (opacity === 0) {
                        opacity = 1;
                    }

                    level -= opacity;

                    if (blockLight > level) {
                        level = blockLight;
                    }

                    if (level < 0) {
                        level = 0;
                    }

                    section.setLightAt(EnumSkyBlock.BLOCK, x, y & 15, z, blockLight);
                }
            }
        }

        this.world.updateLight(EnumSkyBlock.BLOCK,
            this.x * 16, targetY - 1, this.z * 16,
            this.x * 16 + 16, targetY + 1, this.z * 16 + 16
        );
        this.setModifiedAllSections();
    }

    updateBlockLight() {
        this.setModifiedAllSections();
    }

    notifyNeighbors(x, z) {
        let height = this.getHeightAt(x, z);
        let totalX = this.x * 16 + x;
        let totalZ = this.z * 16 + z;

        this.updateSkyLight(totalX - 1, totalZ, height);
        this.updateSkyLight(totalX + 1, totalZ, height);
        this.updateSkyLight(totalX, totalZ - 1, height);
        this.updateSkyLight(totalX, totalZ + 1, height);
    }

    updateSkyLight(x, z, y) {
        let height = this.world.getHeightAt(x, z);
        if (height > y) {
            this.world.updateLight(EnumSkyBlock.SKY, x, y, z, x, height, z);
        } else if (height < y) {
            this.world.updateLight(EnumSkyBlock.SKY, x, height, z, x, y, z);
        }
        this.setModifiedAllSections();
    }

    updateHeightMap(relX, y, relZ) {
        let currentHighestY = this.getHeightAt(relX, relZ);
        let highestY = currentHighestY;
        if (y > currentHighestY) {
            highestY = y;
        }

        highestY = this.calculateHeightAt(relX, relZ, highestY);

        if (highestY === currentHighestY) {
            return;
        }

        this.setHeightAt(relX, relZ, highestY);

        let x = this.x * 16 + relX;
        let z = this.z * 16 + relZ;

        if (highestY < currentHighestY) {
            for (let hy = highestY; hy < currentHighestY; hy++) {
                this.setLightAt(EnumSkyBlock.SKY, relX, hy, relZ, 15);
            }
        } else {
            this.world.updateLight(EnumSkyBlock.SKY, x, currentHighestY, z, x, highestY, z);
            for (let hy = currentHighestY; hy < highestY; hy++) {
                this.setLightAt(EnumSkyBlock.SKY, relX, hy, relZ, 0);
            }
        }

        let lightLevel = 15;
        let prevHeight = highestY;
        while (highestY > 0 && lightLevel > 0) {
            highestY--;

            let typeId = this.getBlockID(relX, highestY, relZ);
            let block = Block.getById(typeId);

            let opacity = Math.floor(typeId === 0 ? 0 : block.getOpacity() * 255);
            if (opacity === 0) {
                opacity = 1;
            }
            lightLevel -= opacity;
            if (lightLevel < 0) {
                lightLevel = 0;
            }

            this.setLightAt(EnumSkyBlock.SKY, relX, highestY, relZ, lightLevel);
        }

        highestY = this.calculateHeightAt(relX, relZ, highestY);

        if (highestY !== prevHeight) {
            this.world.updateLight(EnumSkyBlock.SKY, x - 1, highestY, z - 1, x + 1, prevHeight, z + 1);
        }
        this.setModifiedAllSections();
    }

    calculateHeightAt(x, z, startY) {
        let y = startY;
        while (y > 0) {
            let typeId = this.getBlockAt(x, y - 1, z);
            let block = Block.getById(typeId);
            let opacity = typeId === 0 ? 0 : block.getOpacity();

            if (opacity !== 0) {
                break;
            }
            y--;
        }
        return y;
    }

    updateHeightMapAt(x, z) {
        let y = this.calculateHeightAt(x, z, World.TOTAL_HEIGHT);
        this.setHeightAt(x, z, y);
    }

    setHeightAt(x, z, height) {
        this.heightMap[z << 4 | x] = height;
    }

    /**
     * Is the highest solid block or above
     */
    isHighestBlock(x, y, z) {
        return y >= this.getHighestBlockAt(x, z);
    }

    /**
     * Is above the highest solid block
     */
    isAboveGround(x, y, z) {
        return y >= this.getHeightAt(x, z);
    }

    /**
     * Get the first non-solid block
     */
    getHeightAt(x, z) {
        return this.heightMap[z << 4 | x];
    }

    /**
     * Get the highest solid block
     */
    getHighestBlockAt(x, z) {
        return this.getHeightAt(x, z) - 1;
    }

    setLightAt(sourceType, x, y, z, level) {
        let section = this.getSection(y >> 4);
        section.setLightAt(sourceType, x, y & 15, z, level);
    }

    setBlockDataAt(x, y, z, data) {
        this.setBlockAt(x, y, z, this.getBlockAt(x, y, z), data);
    }

    setBlockAt(x, y, z, typeId, data = 0) {
        let section = this.getSection(y >> 4);
        let yInSection = y & 15;

        let height = this.getHeightAt(x, z);
        let prevTypeId = section.getBlockAt(x, yInSection, z);
        let prevData = section.getBlockDataAt(x, yInSection, z);

        // Check if block type has changed
        if (prevTypeId === typeId && prevData === data) {
            return false;
        }

        // Update block type and data
        section.setBlockAt(x, yInSection, z, typeId);
        section.setBlockDataAt(x, yInSection, z, data);

        if (!this.loaded) {
            return;
        }

        // Update height map
        let block = Block.getById(typeId);
        if (typeId !== 0 && block.isSolid()) {
            if (y >= height) {
                this.updateHeightMap(x, y + 1, z);
            }
        } else if (y === height - 1) {
            this.updateHeightMap(x, y, z);
        }

        let totalX = this.x * 16 + x;
        let totalZ = this.z * 16 + z;

        // Update light
        this.world.updateLight(EnumSkyBlock.SKY, totalX, y, totalZ, totalX, y, totalZ);
        this.world.updateLight(EnumSkyBlock.BLOCK, totalX, y, totalZ, totalX, y, totalZ);

        // Notify surrounding blocks
        this.notifyNeighbors(x, z);

        // Handle block abilities
        if (typeId !== 0) {
            block.onBlockAdded(this.world, totalX, y, totalZ);
        }

        return true;
    }

    getBlockID(x, y, z) {
        return this.getBlockAt(x, y, z);
    }

    getBlockAt(x, y, z) {
        return this.getSection(y >> 4).getBlockAt(x, y & 15, z);
    }

    getBlockDataAt(x, y, z) {
        return this.getSection(y >> 4).getBlockDataAt(x, y & 15, z);
    }

    getSection(y) {
        return this.sections[y];
    }

    rebuild(renderer) {
        for (let y = 0; y < this.sections.length; y++) {
            this.sections[y].rebuild(renderer);
        }
    }

    isLoaded() {
        return this.loaded;
    }

    unload() {
        this.loaded = false;
    }

    setModifiedAllSections() {
        for (let y = 0; y < this.sections.length; y++) {
            this.sections[y].isModified = true;
        }
    }
}