package dk.itu.real.ooe.services;

import com.google.protobuf.Empty;
import dk.itu.real.ooe.Minecraft;
import dk.itu.real.ooe.Minecraft.*;
import dk.itu.real.ooe.MinecraftServiceGrpc.MinecraftServiceImplBase;
import io.grpc.stub.StreamObserver;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.event.CauseStackManager;
import org.spongepowered.api.world.server.ChunkManager;
import org.spongepowered.api.world.server.ServerWorld;
import org.spongepowered.api.world.server.ServerLocation;
import org.spongepowered.api.world.DefaultWorldKeys;
import org.spongepowered.api.world.BlockChangeFlags;
import org.spongepowered.api.block.BlockState;
import org.spongepowered.api.block.BlockType;
import org.spongepowered.api.block.BlockTypes;
import org.spongepowered.api.tag.BlockTypeTags;
import org.spongepowered.api.tag.Tag;
import org.spongepowered.api.util.Direction;
import org.spongepowered.api.data.Keys;
import org.spongepowered.api.data.BlockStateKeys;
import org.spongepowered.api.registry.RegistryTypes;
import org.spongepowered.api.registry.DefaultedRegistryReference;
import org.spongepowered.api.entity.Entity;
import org.spongepowered.api.entity.EntityTypes;
import org.spongepowered.api.scheduler.Task;
import org.spongepowered.math.vector.Vector3d;
import org.spongepowered.plugin.PluginContainer;
import net.kyori.adventure.text.Component;
import java.lang.reflect.Field;
import java.util.*;


public class MinecraftService extends MinecraftServiceImplBase {

    private final PluginContainer plugin;

    private final Communication communication;

    // private Entity observer = null;

    private final Map<String, String> blockNamesToBlockTypes = new HashMap<>(); // minecraft:dirt --> DIRT

    private final Map<Tag<BlockType>, String> blockTagNamesToBlockTags = new HashMap<>(); // minecraft:mineable_pickaxe --> MINEABLE_PICKAXE

    public MinecraftService(PluginContainer plugin) throws IllegalAccessException {
        this.plugin = plugin;
        this.communication = new Communication();
        blockTypesList(blockNamesToBlockTypes);
        blockTagsList(blockTagNamesToBlockTags);

    }

    public void blockTypesList(Map<String, String> map) throws IllegalAccessException {
        for (Field field : BlockTypes.class.getFields()) {
            BlockType blockType = ((DefaultedRegistryReference<BlockType>) field.get(null)).get();
            String key = blockType.key(RegistryTypes.BLOCK_TYPE).formatted();
            String value = field.getName();
            map.put(key, value);
        }
    }

    public void blockTagsList(Map<Tag<BlockType>, String> map) throws IllegalAccessException {
        for (Field field : BlockTypeTags.class.getFields()) {
            Tag<BlockType> blockTypeTag = (Tag<BlockType>) field.get(null);
            String value = field.getName() + "_TAG";
            map.put(blockTypeTag, value);
        }
    }

    @Override
    public void spawnBlocks(Blocks request, StreamObserver<Empty> responseObserver) {
        plugin.logger().info("spawnBlocks");
        Task task = Task.builder().execute(() -> {
            ServerWorld world = Sponge.server().worldManager().world(DefaultWorldKeys.DEFAULT).get();
            for (Block block : request.getBlocksList()) {
                try {
                    BlockType blockType = ((DefaultedRegistryReference<BlockType>) BlockTypes.class.getField(block.getType().toString()).get(null)).get();
                    Point pos = block.getPosition();
                    Orientation orientation = block.getOrientation();
                    ServerLocation location = world.location(pos.getX(), pos.getY(), pos.getZ());
                    // location.setBlockType(blockType);
                    BlockState state = blockType.defaultState();
                    if (state.supports(Keys.DIRECTION)) {
                        state = state.with(Keys.DIRECTION, Direction.valueOf(orientation.toString())).get();
                    }
                    state = setPersistence(state);
                    location.setBlock(state,BlockChangeFlags.DEFAULT_PLACEMENT);
                } catch (IllegalStateException | NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e ){
                    plugin.logger().info(e.getMessage());
                }
            }
            responseObserver.onNext(Empty.getDefaultInstance());
            responseObserver.onCompleted();
        }
        ).plugin(plugin).build();
        Sponge.server().scheduler().submit(task);
    }

    @Override
    public void readCube(Cube cube, StreamObserver<BlocksInfo> responseObserver) {
        plugin.logger().info("readCube");
        Task task = Task.builder().execute(() -> {
            ServerWorld world = Sponge.server().worldManager().world(DefaultWorldKeys.DEFAULT).get();
            responseObserver.onNext(readBlockInfoInCube(world,cube));
            responseObserver.onCompleted();
        }
        ).plugin(plugin).build();
        Sponge.server().scheduler().submit(task);
    }

    @Override
    public void readCubeList(Cubes cubes, StreamObserver<BlocksInfoList> responseObserver) {
        plugin.logger().info("readCubeList");
        Task task = Task.builder().execute(() -> {
            ServerWorld world = Sponge.server().worldManager().world(DefaultWorldKeys.DEFAULT).get();
            BlocksInfoList.Builder builder = BlocksInfoList.newBuilder();
            for (Cube cube : cubes.getCubesList()) {
                builder.addBlocksInfos(readBlockInfoInCube(world,cube));
            }
            responseObserver.onNext(builder.build());
            responseObserver.onCompleted();
        }
        ).plugin(plugin).build();
        Sponge.server().scheduler().submit(task);
    }

    @Override
    public void fillCube(FillCubeRequest request, StreamObserver<Empty> responseObserver) {
        plugin.logger().info("fillCube");
        Task task = Task.builder().execute(() -> {
            ServerWorld world = Sponge.server().worldManager().world(DefaultWorldKeys.DEFAULT).get();
            executeFillCubeRequest(world, request);
            responseObserver.onNext(Empty.getDefaultInstance());
            responseObserver.onCompleted();
        }
        ).plugin(plugin).build();
        Sponge.server().scheduler().submit(task);
    }

    @Override
    public void fillCubeList(FillCubeRequests requestList, StreamObserver<Empty> responseObserver) {
        plugin.logger().info("fillCubeList");
        Task task = Task.builder().execute(() -> {
            ServerWorld world = Sponge.server().worldManager().world(DefaultWorldKeys.DEFAULT).get();
            for(FillCubeRequest request : requestList.getFillCubeRequestsList())
                executeFillCubeRequest(world, request);
            responseObserver.onNext(Empty.getDefaultInstance());
            responseObserver.onCompleted();
        }
        ).plugin(plugin).build();
        Sponge.server().scheduler().submit(task);
    }

    @Override
    public void tagBlockTypes(Minecraft.BlockTypes request, StreamObserver<BlockTags> responseObserver) {
        plugin.logger().info("tagBlockTypes");
        Task task = Task.builder().execute(() -> {
            BlockTags.Builder builder = BlockTags.newBuilder();
            for (Minecraft.BlockType blockType : request.getBlockTypesList()) {
                Collection<Tag<BlockType>> tags;
                try {
                    tags = ((DefaultedRegistryReference<BlockType>) BlockTypes.class.getField(blockType.toString()).get(null))
                            .get().tags();
                } catch (NoSuchFieldException | IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
                StringBuilder tagList = new StringBuilder();
                tagList.append("TAGS = [");
                for (var tag : tags) {
                    tagList.append(blockTagNamesToBlockTags.get(tag));
                    tagList.append(",\n");
                }
                tagList.append("]");
                builder.addBlockTags(BlockTag.newBuilder()
                    .setTag(tagList.toString())
                    .build());
            }
            responseObserver.onNext(builder.build());
            responseObserver.onCompleted();
        }
        ).plugin(plugin).build();
        Sponge.server().scheduler().submit(task);
    }

    @Override
    public void signsWithText(SignDescriptors request, StreamObserver<Empty> responseObserver) {
        plugin.logger().info("signsWithText");
        Task task = Task.builder().execute(() -> {
            ServerWorld world = Sponge.server().worldManager().world(DefaultWorldKeys.DEFAULT).get();
            for (SignDescriptor signDescriptor : request.getSignDescriptorsList()) {
                try {
                    Block block = signDescriptor.getBlock();
                    BlockType blockType = ((DefaultedRegistryReference<BlockType>) BlockTypes.class.getField(block.getType().toString()).get(null)).get();
                    int lineLength = getSignLineLength(blockType);

                    Point pos = block.getPosition();
                    Orientation orientation = block.getOrientation();
                    String text = signDescriptor.getText();
                    ServerLocation location = world.location(pos.getX(), pos.getY(), pos.getZ());
                    BlockState state = blockType.defaultState();
                    if (state.supports(Keys.DIRECTION)) {
                        state = state.with(Keys.DIRECTION, Direction.valueOf(orientation.toString())).get();
                    }
                    location.setBlock(state);

                    var entity = location.blockEntity().get();
                    if (entity.supports(Keys.SIGN_LINES)) {
                        var lines = linesToComponents(diveToLines(text, lineLength));
                        if (lines.size() > 4) {
                            plugin.logger().info("Sign text too long. Omitting the rest:");
                            lines = lines.subList(0, 4);
                        }
                        entity.offer(Keys.SIGN_LINES, lines);
                    }

                } catch (IllegalStateException | NoSuchFieldException | SecurityException | IllegalArgumentException |
                         IllegalAccessException e) {
                    plugin.logger().info(e.getMessage());
                }
            }
            responseObserver.onNext(Empty.getDefaultInstance());
            responseObserver.onCompleted();
        }
        ).plugin(plugin).build();
        Sponge.server().scheduler().submit(task);
    }

    @Override
    public void sendMessage(Minecraft.Message message, StreamObserver<Empty> responseObserver) {
        plugin.logger().info("sendMessage");
        Sponge.server().broadcastAudience().sendMessage(Component.text(message.getText()));
        responseObserver.onNext(Empty.getDefaultInstance());
        responseObserver.onCompleted();
    }

    @Override
    public void readResponse(com.google.protobuf.Empty request, StreamObserver<Minecraft.Message> responseObserver) {
        plugin.logger().info("readResponse");
        Minecraft.Message.Builder builder = Minecraft.Message.newBuilder();
        String message = this.communication.read();
        builder.setText(message);
        responseObserver.onNext(builder.build());
        responseObserver.onCompleted();
    }

    // TODO Rework so it spawns all entities
    @Override
    public void placeObserver(Point point, StreamObserver<Empty> responseObserver) {
        plugin.logger().info("placeObserver");
        Task task = Task.builder().execute(() -> {
            ServerWorld world = Sponge.server().worldManager().world(DefaultWorldKeys.DEFAULT).get();

            ServerLocation location = world.location(point.getX(), point.getY(), point.getZ());
            Entity entity = world.createEntity(EntityTypes.PLAYER, location.position());
            world.spawnEntity(entity);

            responseObserver.onNext(Empty.getDefaultInstance());
            responseObserver.onCompleted();
        }
        ).plugin(plugin).build();
        Sponge.server().scheduler().submit(task);
    }

    // TODO Not implemented
//    @Override
//    public void loadChunk(Point point, StreamObserver<Empty> responseObserver) {
//        plugin.logger().info("loadChunk");
//        Task task = Task.builder().execute(() -> {
//            ServerWorld world = Sponge.server().worldManager().world(DefaultWorldKeys.DEFAULT).get();
//            ServerLocation location = world.location(point.getX(), point.getY(), point.getZ());
//            responseObserver.onNext(Empty.getDefaultInstance());
//            responseObserver.onCompleted();
//        }
//        ).plugin(plugin).build();
//        Sponge.server().scheduler().submit(task);
//    }

    //TODO: also read DIRECTION
    private BlockInfo readBlockInfo(ServerLocation location) {
        BlockInfo.Builder block = BlockInfo.newBuilder();

        BlockState state = location.block();
        block.setPosition(
            Point.newBuilder().
                setX(location.blockX()).
                setY(location.blockY()).
                setZ(location.blockZ()).
            build()
        );
        String name = state.type().key(RegistryTypes.BLOCK_TYPE).formatted();
        block.setType(Minecraft.BlockType.valueOf(blockNamesToBlockTypes.get(name)));
        state.get(Keys.POWER).ifPresent(block::setPower);
        location.get(Keys.IS_INDIRECTLY_POWERED).ifPresent(block::setIsPowered);
        state.get(Keys.DIRECTION).ifPresent(direction ->
                block.setOrientation(Orientation.valueOf(direction.toString()))
        );

        return block.build();
    }

    private BlocksInfo readBlockInfoInCube(ServerWorld world, Cube cube){
        BlocksInfo.Builder builder = BlocksInfo.newBuilder();
        Point min = cube.getMin();
        Point max = cube.getMax();
        for (int x = min.getX(); x <= max.getX(); x++) {
            for (int y = min.getY(); y <= max.getY(); y++) {
                for (int z = min.getZ(); z <= max.getZ(); z++) {
                    ServerLocation loc = world.location(x, y, z);
                    builder.addBlocks(readBlockInfo(loc));
                }
            }
        }
        return builder.build();
    }

    private void executeFillCubeRequest(ServerWorld world, FillCubeRequest request) {
        Cube c = request.getCube();
        Point min = c.getMin();
        Point max = c.getMax();
        BlockType type;
        try {
            type = ((DefaultedRegistryReference<BlockType>) BlockTypes.class.getField(request.getType().toString()).get(null)).get();
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        for (int x = min.getX(); x <= max.getX(); x++) {
            for (int y = min.getY(); y <= max.getY(); y++) {
                for (int z = min.getZ(); z <= max.getZ(); z++) {
                    world.location(x, y, z).setBlock(setPersistence(type.defaultState()));
                }
            }
        }
    }

    private List<Component> linesToComponents(Collection<String> lines){
        List<Component> result = new ArrayList<>();
        for (String line : lines){
            result.add(Component.text(line));
        }
        return  result;
    }

    private List<String> diveToLines(String text, int lineLength) {
        String[] substrings = text.split("\\s+");

        List<String> result = new ArrayList<>();
        StringBuilder currentString = new StringBuilder();
        int currentLength = 0;

        for (String substring : substrings) {
            // Check if adding the substring exceeds the length limit
            if (currentLength + substring.length() <= lineLength) {
                currentString.append(substring).append(" ");
                currentLength += substring.length() + 1; // Include space character
            } else {
                // Add the current string to the result list and start building a new string
                if (currentLength>0)
                    result.add(currentString.toString().trim()); // Trim to remove trailing space
                while (substring.length() > lineLength){
                    result.add(substring.substring(0,lineLength-1)+'-');
                    substring = substring.substring(lineLength-1);
                }
                currentString = new StringBuilder(substring).append(" ");
                currentLength = substring.length() + 1; // Include space character
            }
        }

        // Add the last string to the result list
        result.add(currentString.toString().trim()); // Trim to remove trailing space

        return result;
    }

    private int getSignLineLength(BlockType blockType) throws IllegalArgumentException {
        if (blockType.tags().contains(BlockTypeTags.SIGNS)) {
            return 15;
        } else if (blockType.tags().contains(BlockTypeTags.ALL_HANGING_SIGNS)) {
            return 10;
        } else {
            throw new IllegalArgumentException("Provided blocktype must have SIGNS or ALL_HANGING_SIGNS (ALL_SIGN) tag");
        }
    }

    public void sendResponse(String response){
        this.communication.send(response);
    }

    private BlockState setPersistence(BlockState state) {
        if (state.supports(BlockStateKeys.PERSISTENT)) {
            state =  state.with(BlockStateKeys.PERSISTENT, true).get();
        }
        return state;
    }

}
