/*
 * This file is part of BlueMap, licensed under the MIT License (MIT).
 *
 * Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
 * Copyright (c) contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package de.bluecolored.bluemap.core.world.mca.region;

import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.world.ChunkConsumer;
import de.bluecolored.bluemap.core.world.Region;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import de.bluecolored.bluemap.core.world.mca.chunk.MCAChunk;
import io.airlift.compress.zstd.ZstdInputStream;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

/*
 * LinearFormat:
 *
 *  REGION-FILE:
 *   8 byte - MAGIC value
 *   1 byte - version
 *   8 byte - region timestamp
 *   1 byte - compression level
 *   2 byte - chunk count
 *   4 byte - data-length in bytes
 *   8 byte - data-hash
 *   ? byte - data
 *   8 byte - MAGIC value
 *
 *  DATA: (zstd compressed)
 *   32 * 32 * 8 - header:
 *    4 byte - chunk-data-length
 *    4 byte - timestamp
 *   ? - chunks
 *
 */

public class LinearRegion implements Region {

    public static final String FILE_SUFFIX = ".linear";

    private static final long MAGIC = 0xc3ff13183cca9d9aL;

    private final MCAWorld world;
    private final Path regionFile;
    private final Vector2i regionPos;

    private boolean initialized = false;

    private byte version;
    private long newestTimestamp;
    private byte compressionLevel;
    private short chunkCount;
    private int dataLength;
    private long dataHash;
    private byte[] compressedData;

    public LinearRegion(MCAWorld world, Path regionFile) throws IllegalArgumentException {
        this.world = world;
        this.regionFile = regionFile;

        String[] filenameParts = regionFile.getFileName().toString().split("\\.");
        int rX = Integer.parseInt(filenameParts[1]);
        int rZ = Integer.parseInt(filenameParts[2]);

        this.regionPos = new Vector2i(rX, rZ);
    }

    public LinearRegion(MCAWorld world, Vector2i regionPos) throws IllegalArgumentException {
        this.world = world;
        this.regionPos = regionPos;
        this.regionFile = world.getRegionFolder().resolve(getRegionFileName(regionPos.getX(), regionPos.getY()));
    }

    private synchronized void init() throws IOException {
        if (initialized) return;

        if (Files.notExists(regionFile)) return;

        long fileLength = Files.size(regionFile);
        if (fileLength == 0) return;

        try (
                InputStream in = Files.newInputStream(regionFile, StandardOpenOption.READ);
                BufferedInputStream bIn = new BufferedInputStream(in);
                DataInputStream dIn = new DataInputStream(bIn)
        ) {
            if (dIn.readLong() != MAGIC)
                throw new IOException("Linear region-file format: invalid header magic");

            // read the header
            version = dIn.readByte();
            newestTimestamp = dIn.readLong();
            compressionLevel = dIn.readByte();
            chunkCount = dIn.readShort();
            dataLength = dIn.readInt();
            dataHash = dIn.readLong();

            if (version < 1 || version > 2)
                throw new IOException("Linear region-file format: Unsupported version: " + version);

            if (fileLength != dataLength + 40) // 40 = header + footer
                throw new IOException("Linear region-file format: Invalid file length. Expected " + (dataLength + 40) + " but got " + fileLength);

            compressedData = new byte[dataLength];
            dIn.readFully(compressedData, 0, dataLength);

            if (dIn.readLong() != MAGIC)
                throw new IOException("Linear region-file format: invalid footer magic");

        }

        initialized = true;
    }

    @Override
    public void iterateAllChunks(ChunkConsumer consumer) throws IOException {
        if (!initialized) init();

        int chunkStartX = regionPos.getX() * 32;
        int chunkStartZ = regionPos.getY() * 32;

        byte[] chunkDataBuffer = null;

        try (
                InputStream in = new ZstdInputStream(new ByteArrayInputStream(compressedData));
                BufferedInputStream bIn = new BufferedInputStream(in);
                DataInputStream dIn = new DataInputStream(bIn)
        ) {
            int[] chunkDataLengths = new int[1024];
            int[] chunkTimestamps = new int[1024];
            for (int i = 0 ; i < 1024 ; i++) {
                chunkDataLengths[i] = dIn.readInt();
                chunkTimestamps[i] = dIn.readInt();
            }

            int i = 0;
            int toBeSkipped = 0;
            for (int z = 0; z < 32; z++) {
                for (int x = 0; x < 32; x++) {
                    int length = chunkDataLengths[i];
                    if (length > 0) {
                        int chunkX = chunkStartX + x;
                        int chunkZ = chunkStartZ + z;
                        long timestamp = version == 2 ? chunkTimestamps[i] : newestTimestamp;

                        if (consumer.filter(chunkX, chunkZ, timestamp)) {
                            if (toBeSkipped > 0) skipNBytes(dIn, toBeSkipped);

                            if (chunkDataBuffer == null || chunkDataBuffer.length < length)
                                chunkDataBuffer = new byte[length];
                            dIn.readFully(chunkDataBuffer, 0, length);

                            MCAChunk chunk = world.getChunkLoader().load(chunkDataBuffer, 0, length, Compression.NONE);
                            consumer.accept(chunkX, chunkZ, chunk);
                        } else {
                            // skip before reading the next chunk, but only if there is a next chunk
                            // that we actually want to read, to avoid decompressing unnecessary data
                            toBeSkipped += length;
                        }
                    }

                    i++;
                }
            }

        }
    }

    public static String getRegionFileName(int regionX, int regionZ) {
        return "r." + regionX + "." + regionZ + FILE_SUFFIX;
    }

    /**
     * This method is taken here from a newer version of {@link InputStream},
     * to ensure Java 11 compatibility.
     */
    private static void skipNBytes(InputStream in, long n) throws IOException {
        while (n > 0) {
            long ns = in.skip(n);
            if (ns > 0 && ns <= n) {
                // adjust number to skip
                n -= ns;
            } else if (ns == 0) { // no bytes skipped
                // read one byte to check for EOS
                if (in.read() == -1) {
                    throw new EOFException();
                }
                // one byte read so decrement number to skip
                n--;
            } else { // skipped negative or too many bytes
                throw new IOException("Unable to skip exactly");
            }
        }
    }

}