package com.fastasyncworldedit.core.history.changeset;

import com.fastasyncworldedit.core.configuration.Settings;
import com.fastasyncworldedit.core.history.change.MutableBiomeChange;
import com.fastasyncworldedit.core.history.change.MutableBlockChange;
import com.fastasyncworldedit.core.history.change.MutableEntityChange;
import com.fastasyncworldedit.core.history.change.MutableFullBlockChange;
import com.fastasyncworldedit.core.history.change.MutableTileChange;
import com.fastasyncworldedit.core.internal.io.FaweInputStream;
import com.fastasyncworldedit.core.internal.io.FaweOutputStream;
import com.fastasyncworldedit.core.util.MainUtil;
import com.fastasyncworldedit.core.util.MathMan;
import com.sk89q.jnbt.CompoundTag;
import com.sk89q.jnbt.NBTInputStream;
import com.sk89q.jnbt.NBTOutputStream;
import com.sk89q.worldedit.extent.inventory.BlockBag;
import com.sk89q.worldedit.history.change.Change;
import com.sk89q.worldedit.regions.Region;
import com.sk89q.worldedit.world.World;
import com.sk89q.worldedit.world.biome.BiomeType;
import com.sk89q.worldedit.world.block.BlockTypes;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Iterator;
import java.util.NoSuchElementException;

/**
 * FAWE stream ChangeSet offering support for extended-height worlds
 */
public abstract class FaweStreamChangeSet extends AbstractChangeSet {

    public static final int HEADER_SIZE = 9;
    private static final int version = 1;
    private int mode;
    private final int compression;
    private final int minY;

    protected FaweStreamIdDelegate idDel;
    protected FaweStreamPositionDelegate posDel;

    public FaweStreamChangeSet(World world) {
        this(world, Settings.IMP.HISTORY.COMPRESSION_LEVEL, Settings.IMP.HISTORY.STORE_REDO, Settings.IMP.HISTORY.SMALL_EDITS);
    }

    public FaweStreamChangeSet(World world, int compression, boolean storeRedo, boolean smallLoc) {
        super(world);
        this.compression = compression;
        this.minY = world.getMinY();
        init(storeRedo, smallLoc);
    }

    private void init(boolean storeRedo, boolean smallLoc) {
        if (storeRedo) {
            if (smallLoc) {
                mode = 4;
            } else {
                mode = 3;
            }
        } else if (smallLoc) {
            mode = 1;
        } else {
            mode = 2;
        }
    }

    public interface FaweStreamPositionDelegate {

        void write(OutputStream out, int x, int y, int z) throws IOException;

        int readX(FaweInputStream in) throws IOException;

        int readY(FaweInputStream in) throws IOException;

        int readZ(FaweInputStream in) throws IOException;

    }

    public interface FaweStreamIdDelegate {

        void writeChange(FaweOutputStream out, int from, int to) throws IOException;

        void readCombined(FaweInputStream in, MutableBlockChange change, boolean dir) throws IOException;

        void readCombined(FaweInputStream in, MutableFullBlockChange change) throws IOException;

    }

    protected void setupStreamDelegates(int mode) {
        this.mode = mode;
        if (mode == 3 || mode == 4) {
            idDel = new FaweStreamIdDelegate() {
                @Override
                public void writeChange(FaweOutputStream stream, int combinedFrom, int combinedTo) throws IOException {
                    stream.writeVarInt(combinedFrom);
                    stream.writeVarInt(combinedTo);
                }

                @Override
                public void readCombined(FaweInputStream is, MutableBlockChange change, boolean dir) throws IOException {
                    if (dir) {
                        is.readVarInt();
                        change.ordinal = is.readVarInt();
                    } else {
                        change.ordinal = is.readVarInt();
                        is.readVarInt();
                    }
                }

                @Override
                public void readCombined(FaweInputStream is, MutableFullBlockChange change) throws IOException {
                    change.from = is.readVarInt();
                    change.to = is.readVarInt();
                }
            };
        } else {
            idDel = new FaweStreamIdDelegate() {
                @Override
                public void writeChange(FaweOutputStream stream, int combinedFrom, int to) throws IOException {
                    stream.writeVarInt(combinedFrom);
                }

                @Override
                public void readCombined(FaweInputStream in, MutableBlockChange change, boolean dir) throws IOException {
                    int from1 = in.read();
                    int from2 = in.read();
                    change.ordinal = in.readVarInt();
                }

                @Override
                public void readCombined(FaweInputStream is, MutableFullBlockChange change) throws IOException {
                    change.from = is.readVarInt();
                    change.to = BlockTypes.AIR.getInternalId();
                }
            };
        }
        if (mode == 1 || mode == 4) { // small
            posDel = new FaweStreamPositionDelegate() {
                int lx;
                int ly;
                int lz;

                @Override
                public void write(OutputStream out, int x, int y, int z) throws IOException {
                    if (y < 0 || y > 255) {
                        throw new UnsupportedOperationException("y cannot be outside range 0-255 for " +
                                "small-edits=true");
                    }
                    int rx = -lx + (lx = x);
                    int ry = -ly + (ly = y);
                    int rz = -lz + (lz = z);
                    byte b1 = (byte) (ry);
                    byte b2 = (byte) (rx);
                    byte b3 = (byte) (rz);
                    int x16 = (rx >> 8) & 0xF;
                    int z16 = (rz >> 8) & 0xF;
                    byte b4 = MathMan.pair16(x16, z16);
                    out.write(b1);
                    out.write(b2);
                    out.write(b3);
                    out.write(b4);
                }

                final byte[] buffer = new byte[4];

                @Override
                public int readX(FaweInputStream in) throws IOException {
                    in.readFully(buffer);
                    return lx = lx + ((((buffer[1] & 0xFF) + ((MathMan.unpair16x(buffer[3])) << 8)) << 20) >> 20);
                }

                @Override
                public int readY(FaweInputStream in) {
                    return (ly = ly + buffer[0]) & 0xFF;
                }

                @Override
                public int readZ(FaweInputStream in) throws IOException {
                    return lz = lz + ((((buffer[2] & 0xFF) + ((MathMan.unpair16y(buffer[3])) << 8)) << 20) >> 20);
                }
            };
        } else {
            posDel = new FaweStreamPositionDelegate() {
                final byte[] buffer = new byte[6];
                int lx;
                int ly;
                int lz;

                @Override
                public void write(OutputStream stream, int x, int y, int z) throws IOException {
                    int rx = -lx + (lx = x);
                    int ry = -ly + (ly = y);
                    int rz = -lz + (lz = z);
                    stream.write((rx) & 0xff);
                    stream.write(((rx) >> 8) & 0xff);
                    stream.write((rz) & 0xff);
                    stream.write(((rz) >> 8) & 0xff);
                    stream.write((ry) & 0xff);
                    stream.write(((ry) >> 8) & 0xff);
                }

                @Override
                public int readX(FaweInputStream is) throws IOException {
                    is.readFully(buffer);
                    return lx = (lx + (buffer[0] & 0xFF) + (buffer[1] << 8));
                }

                @Override
                public int readY(FaweInputStream is) throws IOException {
                    return ly = (ly + (buffer[4] & 0xFF) + (buffer[5] << 8));
                }

                @Override
                public int readZ(FaweInputStream is) throws IOException {
                    return lz = (lz + (buffer[2] & 0xFF) + (buffer[3] << 8));
                }
            };
        }
    }

    public void writeHeader(OutputStream os, int x, int y, int z) throws IOException {
        os.write(mode);
        // Allows for version detection of history in case of changes to format.
        os.write(version);
        setOrigin(x, z);
        os.write((byte) (x >> 24));
        os.write((byte) (x >> 16));
        os.write((byte) (x >> 8));
        os.write((byte) (x));
        os.write((byte) (z >> 24));
        os.write((byte) (z >> 16));
        os.write((byte) (z >> 8));
        os.write((byte) (z));
        setupStreamDelegates(mode);
    }

    public void readHeader(InputStream is) throws IOException {
        // skip mode
        int mode = is.read();
        int version = is.read();
        if (version != FaweStreamChangeSet.version) {
            throw new UnsupportedOperationException(String.format("Version %s history not supported!", version));
        }
        // origin
        int x = ((is.read() << 24) + (is.read() << 16) + (is.read() << 8) + is.read());
        int z = ((is.read() << 24) + (is.read() << 16) + (is.read() << 8) + is.read());
        setOrigin(x, z);
        setupStreamDelegates(mode);
    }

    public FaweOutputStream getCompressedOS(OutputStream os) throws IOException {
        return MainUtil.getCompressedOS(os, compression);
    }

    @Override
    public boolean isEmpty() {
        if (blockSize > 0) {
            return false;
        }
        if (waitingCombined.get() != 0 || waitingAsync.get() != 0) {
            return false;
        }
        flush();
        return blockSize == 0;
    }

    @Override
    public int size() {
        // Flush so we can accurately get the size
        flush();
        return blockSize;
    }

    public abstract int getCompressedSize();

    public abstract long getSizeInMemory();

    public long getSizeOnDisk() {
        return 0;
    }

    public abstract FaweOutputStream getBlockOS(int x, int y, int z) throws IOException;

    public abstract FaweOutputStream getBiomeOS() throws IOException;

    public abstract NBTOutputStream getEntityCreateOS() throws IOException;

    public abstract NBTOutputStream getEntityRemoveOS() throws IOException;

    public abstract NBTOutputStream getTileCreateOS() throws IOException;

    public abstract NBTOutputStream getTileRemoveOS() throws IOException;

    public abstract FaweInputStream getBlockIS() throws IOException;

    public abstract FaweInputStream getBiomeIS() throws IOException;

    public abstract NBTInputStream getEntityCreateIS() throws IOException;

    public abstract NBTInputStream getEntityRemoveIS() throws IOException;

    public abstract NBTInputStream getTileCreateIS() throws IOException;

    public abstract NBTInputStream getTileRemoveIS() throws IOException;

    protected int blockSize;

    private int originX;
    private int originZ;

    public void setOrigin(int x, int z) {
        originX = x;
        originZ = z;
    }

    public int getOriginX() {
        return originX;
    }

    public int getOriginZ() {
        return originZ;
    }

    @Override
    public void add(int x, int y, int z, int combinedFrom, int combinedTo) {
        blockSize++;
        try {
            FaweOutputStream stream = getBlockOS(x, y, z);
            //x
            posDel.write(stream, x - originX, y, z - originZ);
            idDel.writeChange(stream, combinedFrom, combinedTo);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    @Override
    public void addBiomeChange(int bx, int by, int bz, BiomeType from, BiomeType to) {
        blockSize++;
        try {
            int x = bx >> 2;
            int y = by >> 2;
            int z = bz >> 2;
            FaweOutputStream os = getBiomeOS();
            os.write((byte) (x >> 24));
            os.write((byte) (x >> 16));
            os.write((byte) (x >> 8));
            os.write((byte) (x));
            os.write((byte) (z >> 24));
            os.write((byte) (z >> 16));
            os.write((byte) (z >> 8));
            os.write((byte) (z));
            // only need to store biomes in the 4x4x4 chunks so only need one byte for y still (signed byte -128 -> 127)
            //  means -512 -> 508. Add 128 to avoid negative value casting.
            os.write((byte) (y + 128));
            os.writeVarInt(from.getInternalId());
            os.writeVarInt(to.getInternalId());
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    @Override
    public void addTileCreate(CompoundTag tag) {
        if (tag == null) {
            return;
        }
        blockSize++;
        try {
            NBTOutputStream nbtos = getTileCreateOS();
            nbtos.writeTag(tag);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void addTileRemove(CompoundTag tag) {
        if (tag == null) {
            return;
        }
        blockSize++;
        try {
            NBTOutputStream nbtos = getTileRemoveOS();
            nbtos.writeTag(tag);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void addEntityRemove(CompoundTag tag) {
        if (tag == null) {
            return;
        }
        blockSize++;
        try {
            NBTOutputStream nbtos = getEntityRemoveOS();
            nbtos.writeTag(tag);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void addEntityCreate(CompoundTag tag) {
        if (tag == null) {
            return;
        }
        blockSize++;
        try {
            NBTOutputStream nbtos = getEntityCreateOS();
            nbtos.writeTag(tag);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public Iterator<MutableBlockChange> getBlockIterator(final boolean dir) throws IOException {
        final FaweInputStream is = getBlockIS();
        if (is == null) {
            return Collections.emptyIterator();
        }
        final MutableBlockChange change = new MutableBlockChange(0, 0, 0, BlockTypes.AIR.getInternalId());
        return new Iterator<MutableBlockChange>() {
            private MutableBlockChange last = read();

            public MutableBlockChange read() {
                try {
                    change.x = posDel.readX(is) + originX;
                    change.y = posDel.readY(is);
                    change.z = posDel.readZ(is) + originZ;
                    idDel.readCombined(is, change, dir);
                    return change;
                } catch (EOFException ignored) {
                } catch (Exception e) {
                    e.printStackTrace();
                    e.printStackTrace();
                }
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }

            @Override
            public boolean hasNext() {
                return last != null || ((last = read()) != null);
            }

            @Override
            public MutableBlockChange next() {
                MutableBlockChange tmp = last;
                if (tmp == null) {
                    tmp = read();
                }
                last = null;
                return tmp;
            }

            @Override
            public void remove() {
                throw new IllegalArgumentException("CANNOT REMOVE");
            }
        };
    }

    public Iterator<MutableBiomeChange> getBiomeIterator(final boolean dir) throws IOException {
        final FaweInputStream is = getBiomeIS();
        if (is == null) {
            return Collections.emptyIterator();
        }
        final MutableBiomeChange change = new MutableBiomeChange();
        return new Iterator<MutableBiomeChange>() {
            private MutableBiomeChange last = new MutableBiomeChange();

            public MutableBiomeChange read() {
                try {
                    int int1 = is.read();
                    if (int1 != -1) {
                        int x = ((int1 << 24) + (is.read() << 16) + (is.read() << 8) + is.read()) << 2;
                        int z = ((is.read() << 24) + (is.read() << 16) + (is.read() << 8) + is.read()) << 2;
                        int y = (is.read() - 128) << 2;
                        int from = is.readVarInt();
                        int to = is.readVarInt();
                        change.setBiome(x, y, z, from, to);
                        return change;
                    }
                } catch (EOFException ignored) {
                } catch (Exception e) {
                    e.printStackTrace();
                    e.printStackTrace();
                }
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }

            @Override
            public boolean hasNext() {
                return last != null || ((last = read()) != null);
            }

            @Override
            public MutableBiomeChange next() {
                MutableBiomeChange tmp = last;
                if (tmp == null) {
                    tmp = read();
                }
                last = null;
                return tmp;
            }

            @Override
            public void remove() {
                throw new IllegalArgumentException("CANNOT REMOVE");
            }
        };
    }

    @Override
    public Iterator<Change> getIterator(BlockBag blockBag, int mode, boolean redo) {
        if (blockBag != null && mode > 0) {
            try {
                return (Iterator<Change>) (Iterator<?>) getFullBlockIterator(blockBag, mode, redo);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return getIterator(redo);
    }

    public Iterator<MutableFullBlockChange> getFullBlockIterator(BlockBag blockBag, int inventory, final boolean dir) throws
            IOException {
        final FaweInputStream is = new FaweInputStream(getBlockIS());
        final MutableFullBlockChange change = new MutableFullBlockChange(blockBag, inventory, dir);
        return new Iterator<MutableFullBlockChange>() {
            private MutableFullBlockChange last = read();

            public MutableFullBlockChange read() {
                try {
                    change.x = posDel.readX(is) + originX;
                    change.y = posDel.readY(is);
                    change.z = posDel.readZ(is) + originZ;
                    idDel.readCombined(is, change);
                    return change;
                } catch (EOFException ignored) {
                } catch (Exception e) {
                    e.printStackTrace();
                    e.printStackTrace();
                }
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }

            @Override
            public boolean hasNext() {
                return last != null || ((last = read()) != null);
            }

            @Override
            public MutableFullBlockChange next() {
                MutableFullBlockChange tmp = last;
                if (tmp == null) {
                    tmp = read();
                }
                last = null;
                return tmp;
            }

            @Override
            public void remove() {
                throw new IllegalArgumentException("CANNOT REMOVE");
            }
        };
    }

    public Iterator<MutableEntityChange> getEntityIterator(final NBTInputStream is, final boolean create) {
        if (is == null) {
            return Collections.emptyIterator();
        }
        final MutableEntityChange change = new MutableEntityChange(null, create);
        try {
            return new Iterator<MutableEntityChange>() {
                private MutableEntityChange last = read();

                public MutableEntityChange read() {
                    try {
                        change.tag = (CompoundTag) is.readTag();
                        return change;
                    } catch (Exception ignored) {
                    }
                    try {
                        is.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    return null;
                }

                @Override
                public boolean hasNext() {
                    return last != null || ((last = read()) != null);
                }

                @Override
                public MutableEntityChange next() {
                    MutableEntityChange tmp = last;
                    if (tmp == null) {
                        tmp = read();
                    }
                    last = null;
                    return tmp;
                }

                @Override
                public void remove() {
                    throw new IllegalArgumentException("CANNOT REMOVE");
                }
            };
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public Iterator<MutableTileChange> getTileIterator(final NBTInputStream is, final boolean create) {
        if (is == null) {
            return Collections.emptyIterator();
        }
        final MutableTileChange change = new MutableTileChange(null, create);
        try {
            return new Iterator<MutableTileChange>() {
                private MutableTileChange last = read();

                public MutableTileChange read() {
                    try {
                        change.tag = (CompoundTag) is.readTag();
                        return change;
                    } catch (Exception ignored) {
                    }
                    try {
                        is.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    return null;
                }

                @Override
                public boolean hasNext() {
                    return last != null || ((last = read()) != null);
                }

                @Override
                public MutableTileChange next() {
                    MutableTileChange tmp = last;
                    if (tmp == null) {
                        tmp = read();
                    }
                    last = null;
                    return tmp;
                }

                @Override
                public void remove() {
                    throw new IllegalArgumentException("CANNOT REMOVE");
                }
            };
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public Iterator<Change> getIterator(final boolean dir) {
        try {
            close();
            final Iterator<MutableTileChange> tileCreate = getTileIterator(getTileCreateIS(), true);
            final Iterator<MutableTileChange> tileRemove = getTileIterator(getTileRemoveIS(), false);

            final Iterator<MutableEntityChange> entityCreate = getEntityIterator(getEntityCreateIS(), true);
            final Iterator<MutableEntityChange> entityRemove = getEntityIterator(getEntityRemoveIS(), false);

            final Iterator<MutableBlockChange> blockChange = getBlockIterator(dir);

            final Iterator<MutableBiomeChange> biomeChange = getBiomeIterator(dir);

            return new Iterator<Change>() {
                final Iterator<Change>[] iterators = new Iterator[]{tileCreate, tileRemove, entityCreate, entityRemove, blockChange, biomeChange};
                int i = 0;
                Iterator<Change> current = iterators[0];

                @Override
                public boolean hasNext() {
                    if (current.hasNext()) {
                        return true;
                    } else if (i >= iterators.length - 1) {
                        return false;
                    } else {
                        current = iterators[++i];
                    }
                    return hasNext();
                }

                @Override
                public void remove() {
                    current.remove();
                }

                @Override
                public Change next() {
                    try {
                        return current.next();
                    } catch (Throwable ignored) {
                        if (i >= iterators.length - 1) {
                            throw new NoSuchElementException("End of iterator");
                        }
                        current = iterators[++i];
                        return next();
                    }
                }
            };
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Collections.emptyIterator();
    }

    @Override
    public Iterator<Change> backwardIterator() {
        return getIterator(false);
    }

    @Override
    public Iterator<Change> forwardIterator() {
        return getIterator(true);
    }

    protected SimpleChangeSetSummary summarizeShallow() {
        return new SimpleChangeSetSummary(getOriginX(), getOriginZ());
    }

    @Override
    public SimpleChangeSetSummary summarize(Region region, boolean shallow) {
        int ox = getOriginX();
        int oz = getOriginZ();
        SimpleChangeSetSummary summary = summarizeShallow();
        if (region != null && !region.contains(ox, oz)) {
            return summary;
        }
        try (FaweInputStream fis = getBlockIS()) {
            if (!shallow) {
                int amount = (Settings.IMP.HISTORY.BUFFER_SIZE - HEADER_SIZE) / 9;
                MutableFullBlockChange change = new MutableFullBlockChange(null, 0, false);
                for (int i = 0; i < amount; i++) {
                    int x = posDel.readX(fis) + ox;
                    int y = posDel.readY(fis);
                    int z = posDel.readZ(fis) + ox;
                    idDel.readCombined(fis, change);
                    summary.add(x, z, change.to);
                }
            }
        } catch (EOFException ignored) {
        } catch (IOException e) {
            e.printStackTrace();
        }
        return summary;
    }

}
