/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2025 SteamWar.de-Serverteam
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Affero General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   GNU Affero General Public License for more details.
 *
 *   You should have received a copy of the GNU Affero General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package de.steamwar.core;

import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.sk89q.jnbt.*;
import com.sk89q.worldedit.*;
import com.sk89q.worldedit.blocks.BaseBlock;
import com.sk89q.worldedit.bukkit.BukkitWorld;
import com.sk89q.worldedit.extension.input.ParserContext;
import com.sk89q.worldedit.extension.platform.Actor;
import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat;
import com.sk89q.worldedit.extent.clipboard.io.ClipboardReader;
import com.sk89q.worldedit.extent.clipboard.io.SchematicReader;
import com.sk89q.worldedit.math.transform.Transform;
import com.sk89q.worldedit.regions.CuboidRegion;
import com.sk89q.worldedit.regions.Region;
import com.sk89q.worldedit.session.ClipboardHolder;
import com.sk89q.worldedit.world.registry.WorldData;
import de.steamwar.sql.NodeData;
import org.bukkit.entity.Player;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class WorldEditWrapper8 implements WorldEditWrapper {

    @Override
    public InputStream getPlayerClipboard(Player player) {
        return WorldEditWrapper.getPlayerClipboard(player, (outputStream, clipboard, clipboardHolder) ->
                ClipboardFormat.SCHEMATIC.getWriter(outputStream).write(clipboard, clipboardHolder.getWorldData()));
    }

    @Override
    public void setPlayerClipboard(Player player, InputStream is, NodeData.SchematicFormat schemFormat) {
        WorldData world = new BukkitWorld(player.getWorld()).getWorldData();
        Clipboard clipboard;
        try {
            clipboard = getClipboard(is, schemFormat);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        Actor actor = WorldEditWrapper.getWorldEditPlugin().wrapCommandSender(player);
        WorldEditWrapper.getWorldEditPlugin().getWorldEdit().getSessionManager().get(actor).setClipboard(new ClipboardHolder(clipboard, world));
    }

    @Override
    public Clipboard getClipboard(InputStream is, NodeData.SchematicFormat schemFormat) throws IOException {
        switch (schemFormat) {
            case MCEDIT:
                return new SchematicReader(new NBTInputStream(is)).read(WorldEdit.getInstance().getServer().getWorlds().get(0).getWorldData());
            case SPONGE_V2:
            case SPONGE_V3:
                return new SpongeSchematicReader(new NBTInputStream(is)).read(WorldEdit.getInstance().getServer().getWorlds().get(0).getWorldData());
            default:
                throw new IllegalArgumentException("Unsupported schematic format");
        }
    }

    @Override
    public org.bukkit.util.Vector getOrigin(Clipboard clipboard) {
        return new org.bukkit.util.Vector(clipboard.getOrigin().getX(), clipboard.getOrigin().getY(), clipboard.getOrigin().getZ());
    }

    @Override
    public org.bukkit.util.Vector getMinimum(Region region) {
        return new org.bukkit.util.Vector(region.getMinimumPoint().getX(), region.getMinimumPoint().getY(), region.getMinimumPoint().getZ());
    }

    @Override
    public org.bukkit.util.Vector getMaximum(Region region) {
        return new org.bukkit.util.Vector(region.getMaximumPoint().getX(), region.getMaximumPoint().getY(), region.getMaximumPoint().getZ());
    }

    @Override
    public org.bukkit.util.Vector applyTransform(org.bukkit.util.Vector vector, Transform transform) {
        Vector v = new Vector(vector.getX(), vector.getY(), vector.getZ());
        v = transform.apply(v);
        return new org.bukkit.util.Vector(v.getX(), v.getY(), v.getZ());
    }

    @Override
    public NodeData.SchematicFormat getNativeFormat() {
        return NodeData.SchematicFormat.MCEDIT;
    }

    private static class SpongeSchematicReader implements ClipboardReader {

        private final NBTInputStream inputStream;
        private int schematicVersion = -1;

        SpongeSchematicReader(NBTInputStream inputStream) {
            Preconditions.checkNotNull(inputStream);
            this.inputStream = inputStream;
        }

        @Override
        public Clipboard read(WorldData worldData) throws IOException {
            CompoundTag schematicTag = this.getBaseTag();
            if (this.schematicVersion == 1) {
                return this.readSchematic(schematicTag);
            } else if (this.schematicVersion == 2) {
                return this.readSchematic(schematicTag);
            } else {
                throw new IOException("This schematic version is currently not supported");
            }
        }

        private CompoundTag getBaseTag() throws IOException {
            NamedTag rootTag = this.inputStream.readNamedTag();
            if (!rootTag.getName().equals("Schematic")) {
                throw new IOException("Tag 'Schematic' does not exist or is not first");
            } else {
                CompoundTag schematicTag = (CompoundTag)rootTag.getTag();
                Map<String, Tag> schematic = schematicTag.getValue();
                this.schematicVersion = (requireTag(schematic, "Version", IntTag.class)).getValue();
                return schematicTag;
            }
        }

        private BlockArrayClipboard readSchematic(CompoundTag schematicTag) throws IOException {
            IDConverter8 ids = new IDConverter8();

            Map<String, Tag> schematic = schematicTag.getValue();
            boolean v3Mode = false;

            if (schematic.size() == 1) {
                schematic = (requireTag(schematic, "Schematic", CompoundTag.class)).getValue();
                v3Mode = true;
            }

            int width = (requireTag(schematic, "Width", ShortTag.class)).getValue();
            int height = (requireTag(schematic, "Height", ShortTag.class)).getValue();
            int length = (requireTag(schematic, "Length", ShortTag.class)).getValue();
            IntArrayTag offsetTag = getTag(schematic, "Offset", IntArrayTag.class);
            int[] offsetParts;
            if (offsetTag != null) {
                offsetParts = offsetTag.getValue();
                if (offsetParts.length != 3)
                    throw new IOException("Invalid offset specified in schematic.");
            } else {
                offsetParts = new int[]{0, 0, 0};
            }

            BlockVector min = new BlockVector(offsetParts[0], offsetParts[1], offsetParts[2]);
            CompoundTag metadataTag = getTag(schematic, "Metadata", CompoundTag.class);
            Vector origin;
            CuboidRegion region;
            if (metadataTag != null && metadataTag.containsKey("WEOffsetX")) {
                Map<String, Tag> metadata = metadataTag.getValue();
                int offsetX = (requireTag(metadata, "WEOffsetX", IntTag.class)).getValue();
                int offsetY = (requireTag(metadata, "WEOffsetY", IntTag.class)).getValue();
                int offsetZ = (requireTag(metadata, "WEOffsetZ", IntTag.class)).getValue();
                BlockVector offset = new BlockVector(offsetX, offsetY, offsetZ);
                origin = min.subtract(offset);
                region = new CuboidRegion(min, min.add(width, height, length).subtract(BlockVector.ONE));
            } else {
                origin = min;
                region = new CuboidRegion(min, min.add(width, height, length).subtract(BlockVector.ONE));
            }

            Map<String, Tag> blockContainer = null;

            if (v3Mode) {
                blockContainer = getTag(schematic, "Blocks", CompoundTag.class).getValue();
            }

            Map<String, Tag> paletteObject = requireTag(v3Mode ? blockContainer : schematic, "Palette", CompoundTag.class).getValue();

            Map<Integer, BaseBlock> palette = new HashMap<>();
            ParserContext parserContext = new ParserContext();
            parserContext.setRestricted(false);
            parserContext.setPreferringWildcard(false);

            for(String palettePart : paletteObject.keySet()) {
                IDConverter8.BlockTypeID blockID = ids.getId(palettePart);
                palette.put(requireTag(paletteObject, palettePart, IntTag.class).getValue(), new BaseBlock(blockID.getBlockId(), blockID.getDataId()));
            }

            byte[] blocks = requireTag(v3Mode ? blockContainer : schematic, v3Mode ? "Data" : "BlockData", ByteArrayTag.class).getValue();
            Map<BlockVector, Map<String, Tag>> tileEntitiesMap = new HashMap<>();
            ListTag tileEntities = getTag(v3Mode ? blockContainer : schematic, "BlockEntities", ListTag.class);
            if (tileEntities == null) {
                tileEntities = getTag(v3Mode ? blockContainer : schematic, "TileEntities", ListTag.class);
            }

            if (tileEntities != null) {
                List<Map<String, Tag>> tileEntityTags = tileEntities.getValue().stream().map((tag) ->
                        (CompoundTag)tag
                ).map(CompoundTag::getValue).collect(Collectors.toList());

                BlockVector pt;
                Map<String, Tag> tileEntity;
                for(Iterator<Map<String, Tag>> var20 = tileEntityTags.iterator(); var20.hasNext(); tileEntitiesMap.put(pt, tileEntity)) {
                    tileEntity = var20.next();
                    int[] pos = requireTag(tileEntity, "Pos", IntArrayTag.class).getValue();
                    pt = new BlockVector(pos[0], pos[1], pos[2]);
                    Map<String, Tag> values = Maps.newHashMap(tileEntity);
                    values.put("x", new IntTag(pt.getBlockX()));
                    values.put("y", new IntTag(pt.getBlockY()));
                    values.put("z", new IntTag(pt.getBlockZ()));
                    values.put("id", values.get("Id"));
                    values.remove("Id");
                    values.remove("Pos");
                    tileEntity = values;
                }
            }

            BlockArrayClipboard clipboard = new BlockArrayClipboard(region);
            clipboard.setOrigin(origin);
            int index = 0;

            for(int i = 0; i < blocks.length; ++index) {
                int value = 0;
                int varintLength = 0;

                while(true) {
                    value |= (blocks[i] & 127) << varintLength++ * 7;
                    if (varintLength > 5) {
                        throw new IOException("VarInt too big (probably corrupted data)");
                    }

                    if ((blocks[i] & 128) != 128) {
                        ++i;
                        int y = index / (width * length);
                        int z = index % (width * length) / width;
                        int x = index % (width * length) % width;
                        BaseBlock block = palette.get(value);
                        BlockVector pt = new BlockVector(x, y, z);

                        try {
                            if (tileEntitiesMap.containsKey(pt)) {
                                block.setNbtData(new CompoundTag(tileEntitiesMap.get(pt)));
                                clipboard.setBlock(clipboard.getMinimumPoint().add(pt), block);
                            } else {
                                clipboard.setBlock(clipboard.getMinimumPoint().add(pt), block);
                            }
                            break;
                        } catch (WorldEditException var30) {
                            throw new IOException("Failed to load a block in the schematic");
                        }
                    }

                    ++i;
                }
            }

            return clipboard;
        }

        private static <T extends Tag> T requireTag(Map<String, Tag> items, String key, Class<T> expected) throws IOException {
            if (!items.containsKey(key)) {
                throw new IOException("Schematic file is missing a \"" + key + "\" tag");
            } else {
                Tag tag = items.get(key);
                if (!expected.isInstance(tag)) {
                    throw new IOException(key + " tag is not of tag type " + expected.getName());
                } else {
                    return expected.cast(tag);
                }
            }
        }

        private static <T extends Tag> T getTag(Map<String, Tag> items, String key, Class<T> expected) {
            if (!items.containsKey(key)) {
                return null;
            } else {
                Tag test = items.get(key);
                return !expected.isInstance(test) ? null : expected.cast(test);
            }
        }
    }
}
