package emu.grasscutter.command.commands;

import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.GadgetData;
import emu.grasscutter.data.excels.ItemData;
import emu.grasscutter.data.excels.MonsterData;
import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.EntityType;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.world.Scene;
import emu.grasscutter.utils.Position;
import lombok.Setter;

import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.regex.Pattern;

import static emu.grasscutter.command.CommandHelpers.*;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
import static emu.grasscutter.utils.Language.translate;

@Command(
    label = "spawn",
    aliases = {"drop"},
    usage = {
        "<itemId> [x<amount>] [blk<blockId>] [grp<groupId>] [cfg<configId>] <x> <y> <z>",
        "<gadgetId> [x<amount>] [state<state>] [maxhp<maxhp>] [hp<hp>(0 for infinite)] [atk<atk>] [def<def>] [blk<blockId>] [grp<groupId>] [cfg<configId>] <x> <y> <z>",
        "<monsterId> [x<amount>] [lv<level>] [ai<aiId>] [maxhp<maxhp>] [hp<hp>(0 for infinite)] [atk<atk>] [def<def>] [blk<blockId>] [grp<groupId>] [cfg<configId>] <x> <y> <z>"},
    permission = "server.spawn",
    permissionTargeted = "server.spawn.others")
public final class SpawnCommand implements CommandHandler {
    private static final Map<Pattern, BiConsumer<SpawnParameters, Integer>> intCommandHandlers = Map.ofEntries(
        Map.entry(lvlRegex, SpawnParameters::setLvl),
        Map.entry(amountRegex, SpawnParameters::setAmount),
        Map.entry(stateRegex, SpawnParameters::setState),
        Map.entry(blockRegex, SpawnParameters::setBlockId),
        Map.entry(groupRegex, SpawnParameters::setGroupId),
        Map.entry(configRegex, SpawnParameters::setConfigId),
        Map.entry(maxHPRegex, SpawnParameters::setMaxHP),
        Map.entry(hpRegex, SpawnParameters::setHp),
        Map.entry(defRegex, SpawnParameters::setDef),
        Map.entry(atkRegex, SpawnParameters::setAtk),
        Map.entry(aiRegex, SpawnParameters::setAi)
    );

    @Override
    public void execute(Player sender, Player targetPlayer, List<String> args) {
        SpawnParameters param = new SpawnParameters();

        parseIntParameters(args, param, intCommandHandlers);

        // At this point, first remaining argument MUST be the id and the rest the pos
        if (args.size() < 1) {
            sendUsageMessage(sender);  // Reachable if someone does `/give lv90` or similar
            throw new IllegalArgumentException();
        }
        switch (args.size()) {
            case 4:
                try {
                    float x, y, z;
                    x = Float.parseFloat(args.get(1));
                    y = Float.parseFloat(args.get(2));
                    z = Float.parseFloat(args.get(3));
                    param.pos = new Position(x, y, z);
                } catch (NumberFormatException ignored) {
                    CommandHandler.sendMessage(sender, translate(sender, "commands.execution.argument_error"));
                }  // Fallthrough
            case 1:
                try {
                    param.id = Integer.parseInt(args.get(0));
                } catch (NumberFormatException ignored) {
                    CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.entityId"));
                }
                break;
            default:
                sendUsageMessage(sender);
                return;
        }

        MonsterData monsterData = GameData.getMonsterDataMap().get(param.id);
        GadgetData gadgetData = GameData.getGadgetDataMap().get(param.id);
        ItemData itemData = GameData.getItemDataMap().get(param.id);
        if (monsterData == null && gadgetData == null && itemData == null) {
            CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.entityId"));
            return;
        }

        param.scene = targetPlayer.getScene();

        if (param.scene.getEntities().size() + param.amount > GAME_OPTIONS.sceneEntityLimit) {
            param.amount = Math.max(Math.min(GAME_OPTIONS.sceneEntityLimit - param.scene.getEntities().size(), param.amount), 0);
            CommandHandler.sendMessage(sender, translate(sender, "commands.spawn.limit_reached", param.amount));
            if (param.amount <= 0) {
                return;
            }
        }

        double maxRadius = Math.sqrt(param.amount * 0.2 / Math.PI);
        if (param.pos == null) {
            param.pos = targetPlayer.getPosition();
        }

        for (int i = 0; i < param.amount; i++) {
            Position pos = GetRandomPositionInCircle(param.pos, maxRadius).addY(3);
            GameEntity entity = null;
            if (itemData != null) {
                entity = createItem(itemData, param, pos);
            }
            if (gadgetData != null) {
                pos.addY(-3);
                entity = createGadget(gadgetData, param, pos, targetPlayer);
            }
            if (monsterData != null) {
                entity = createMonster(monsterData, param, pos);
            }
            applyCommonParameters(entity, param);

            param.scene.addEntity(entity);
        }
        CommandHandler.sendMessage(sender, translate(sender, "commands.spawn.success", param.amount, param.id));
    }

    ;

    private EntityItem createItem(ItemData itemData, SpawnParameters param, Position pos) {
        return new EntityItem(param.scene, null, itemData, pos, 1, true);
    }

    private EntityMonster createMonster(MonsterData monsterData, SpawnParameters param, Position pos) {
        var entity = new EntityMonster(param.scene, monsterData, pos, param.lvl);
        if (param.ai != -1) {
            entity.setAiId(param.ai);
        }
        return entity;
    }

    private EntityBaseGadget createGadget(GadgetData gadgetData, SpawnParameters param, Position pos, Player targetPlayer) {
        EntityBaseGadget entity;
        if (gadgetData.getType() == EntityType.Vehicle) {
            entity = new EntityVehicle(param.scene, targetPlayer, param.id, 0, pos, targetPlayer.getRotation());
        } else {
            entity = new EntityGadget(param.scene, param.id, pos, targetPlayer.getRotation());
            if (param.state != -1) {
                ((EntityGadget) entity).setState(param.state);
            }
        }

        return entity;
    }

    private void applyCommonParameters(GameEntity entity, SpawnParameters param) {
        if (param.blockId != -1) {
            entity.setBlockId(param.blockId);
        }
        if (param.groupId != -1) {
            entity.setGroupId(param.groupId);
        }
        if (param.configId != -1) {
            entity.setConfigId(param.configId);
        }
        if (param.maxHP != -1) {
            entity.setFightProperty(FightProperty.FIGHT_PROP_MAX_HP, param.maxHP);
            entity.setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, param.maxHP);
        }
        if (param.hp != -1) {
            entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, param.hp == 0 ? Float.MAX_VALUE : param.hp);
        }
        if (param.atk != -1) {
            entity.setFightProperty(FightProperty.FIGHT_PROP_ATTACK, param.atk);
            entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_ATTACK, param.atk);
        }
        if (param.def != -1) {
            entity.setFightProperty(FightProperty.FIGHT_PROP_DEFENSE, param.def);
            entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_DEFENSE, param.def);
        }
    }

    private Position GetRandomPositionInCircle(Position origin, double radius) {
        Position target = origin.clone();
        double angle = Math.random() * 360;
        double r = Math.sqrt(Math.random() * radius * radius);
        target.addX((float) (r * Math.cos(angle))).addZ((float) (r * Math.sin(angle)));
        return target;
    }

    private static class SpawnParameters {
        @Setter public int id;
        @Setter public int lvl = 1;
        @Setter public int amount = 1;
        @Setter public int blockId = -1;
        @Setter public int groupId = -1;
        @Setter public int configId = -1;
        @Setter public int state = -1;
        @Setter public int hp = -1;
        @Setter public int maxHP = -1;
        @Setter public int atk = -1;
        @Setter public int def = -1;
        @Setter public int ai = -1;
        @Setter public Position pos = null;
        public Scene scene = null;
    }
}
