/*
 *   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.fastasyncworldedit.core.extent.clipboard.io.FastSchematicReaderV2;
import com.fastasyncworldedit.core.extent.clipboard.io.FastSchematicReaderV3;
import com.sk89q.jnbt.NBTInputStream;
import com.sk89q.worldedit.EmptyClipboardException;
import com.sk89q.worldedit.bukkit.WorldEditPlugin;
import com.sk89q.worldedit.extension.platform.Actor;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.extent.clipboard.io.*;
import com.sk89q.worldedit.extent.clipboard.io.sponge.SpongeSchematicV1Reader;
import com.sk89q.worldedit.extent.clipboard.io.sponge.SpongeSchematicV2Reader;
import com.sk89q.worldedit.extent.clipboard.io.sponge.SpongeSchematicV3Reader;
import com.sk89q.worldedit.math.Vector3;
import com.sk89q.worldedit.math.transform.Transform;
import com.sk89q.worldedit.regions.Region;
import com.sk89q.worldedit.session.ClipboardHolder;
import de.steamwar.sql.NoClipboardException;
import de.steamwar.sql.NodeData;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import org.enginehub.linbus.stream.LinBinaryIO;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.logging.Level;

public class WorldEditWrapper {
    public static final WorldEditWrapper impl = new WorldEditWrapper();

    public InputStream getPlayerClipboard(Player player) {
        return WorldEditWrapper.getPlayerClipboard(player, (outputStream, clipboard, clipboardHolder) -> {
            ClipboardWriter writer = BuiltInClipboardFormat.FAST_V3.getWriter(outputStream);
            writer.write(clipboard);
            writer.close();
        });
    }

    public void setPlayerClipboard(Player player, Clipboard clipboard) {
        Actor actor = WorldEditWrapper.getWorldEditPlugin().wrapCommandSender(player);
        WorldEditWrapper.getWorldEditPlugin().getWorldEdit().getSessionManager().get(actor).setClipboard(new ClipboardHolder(clipboard));
    }

    public Clipboard getClipboard(NodeData data) throws IOException {
        ResetableInputStream is = new ResetableInputStream(data.schemData(false));
        for (ClipboardFormat clipboardFormat : ClipboardFormats.getAll()) {
            FilterInputStream fis = new FilterInputStream(is) {
                @Override
                public void close() throws IOException {
                    // Ignore close call!
                }
            };
            boolean canBeRead = clipboardFormat.isFormat(fis);
            is.reset();
            if (!canBeRead) continue;
            return clipboardFormat.load(is);
        }
        throw new IOException("No clipboard found");
    }

    private static final Function<InputStream, ClipboardReader> FastV3 = FastSchematicReaderV3::new;
    @SuppressWarnings("removal")
    private static final Function<InputStream, ClipboardReader> FastV2 = inputStream -> new FastSchematicReaderV2(new NBTInputStream(inputStream));
    @SuppressWarnings("removal")
    private static final Function<InputStream, ClipboardReader> McEdit = inputStream -> new MCEditSchematicReader(new NBTInputStream(inputStream));
    private static final Function<InputStream, ClipboardReader> SpongeV3 = inputStream -> new SpongeSchematicV3Reader(LinBinaryIO.read(new DataInputStream(inputStream)));
    private static final Function<InputStream, ClipboardReader> SpongeV2 = inputStream -> new SpongeSchematicV2Reader(LinBinaryIO.read(new DataInputStream(inputStream)));
    private static final Function<InputStream, ClipboardReader> SpongeV1 = inputStream -> new SpongeSchematicV1Reader(LinBinaryIO.read(new DataInputStream(inputStream)));

    private static final Function<InputStream, ClipboardReader>[] READERS = new Function[]{
            FastV3,
            FastV2,
            SpongeV3,
            SpongeV2,
            SpongeV1,
            McEdit
    };

    public Clipboard getClipboard(InputStream inputStream) throws IOException {
        ResetableInputStream is = new ResetableInputStream(inputStream);
        for (Function<InputStream, ClipboardReader> reader : READERS) {
            FilterInputStream fis = new FilterInputStream(is) {
                @Override
                public void close() throws IOException {
                    // Ignore close call!
                }
            };
            try {
                return reader.apply(fis).read();
            } catch (Exception e) {
                is.reset();
            }
        }
        is.close();
        throw new IOException("No clipboard found");
    }

    private static class ResetableInputStream extends InputStream {

        private InputStream inputStream;
        private int pointer = 0;
        private List<Integer> list = new ArrayList<>();

        public ResetableInputStream(InputStream in) {
            this.inputStream = in;
        }

        @Override
        public int read() throws IOException {
            if (pointer >= list.size()) {
                int data = inputStream.read();
                list.add(data);
                pointer++;
                return data;
            }
            int data = list.get(pointer);
            pointer++;
            return data;
        }

        @Override
        public void reset() throws IOException {
            pointer = 0;
        }

        @Override
        public void close() throws IOException {
            list.clear();
            pointer = -1;
        }
    }

    public org.bukkit.util.Vector getOrigin(Clipboard clipboard) {
        return new org.bukkit.util.Vector(clipboard.getOrigin().x(), clipboard.getOrigin().y(), clipboard.getOrigin().z());
    }

    public Vector getMinimum(Region region) {
        return new Vector(region.getMinimumPoint().x(), region.getMinimumPoint().y(), region.getMinimumPoint().z());
    }

    public Vector getMaximum(Region region) {
        return new Vector(region.getMaximumPoint().x(), region.getMaximumPoint().y(), region.getMaximumPoint().z());
    }

    public Vector applyTransform(Vector vector, Transform transform) {
        Vector3 v = Vector3.at(vector.getX(), vector.getY(), vector.getZ());
        v = transform.apply(v);
        return new org.bukkit.util.Vector(v.x(), v.y(), v.z());
    }

    public NodeData.SchematicFormat getNativeFormat() {
        return NodeData.SchematicFormat.SPONGE_V3;
    }

    static WorldEditPlugin getWorldEditPlugin() {
        return (WorldEditPlugin) Bukkit.getPluginManager().getPlugin("WorldEdit");
    }

    static InputStream getPlayerClipboard(Player player, SchematicWriter consumer) {
        ClipboardHolder clipboardHolder;
        try {
            clipboardHolder = WorldEditWrapper.getWorldEditPlugin().getSession(player).getClipboard();
        } catch (EmptyClipboardException e) {
            throw new NoClipboardException();
        }

        Clipboard clipboard = clipboardHolder.getClipboard();
        if (clipboard == null) throw new NoClipboardException();

        PipedOutputStream outputStream = new PipedOutputStream();
        PipedInputStream inputStream;
        try {
            inputStream = new PipedInputStream(outputStream, 4096);
        } catch (IOException e) {
            throw new SecurityException("Could not init piped input stream", e);
        }

        new Thread(() -> {
            try {
                consumer.write(outputStream, clipboard, clipboardHolder);
            } catch (Exception e) {
                Core.getInstance().getLogger().log(Level.SEVERE, "Could not write schematic", e);
            }
            try {
                outputStream.close();
            } catch (IOException e) {
                Core.getInstance().getLogger().log(Level.SEVERE, "Could not close schem writer", e);
            }
        }, "SchemWriter").start();

        return inputStream;
    }

    interface SchematicWriter {
        void write(OutputStream outputStream, Clipboard clipboard, ClipboardHolder holder) throws IOException;
    }
}
