package net.simon987.npcplugin;

import net.simon987.server.GameServer;
import net.simon987.server.assembly.*;
import net.simon987.server.event.ObjectDeathEvent;
import net.simon987.server.game.item.Item;
import net.simon987.server.game.item.ItemVoid;
import net.simon987.server.game.objects.Action;
import net.simon987.server.game.objects.ControllableUnit;
import net.simon987.server.game.objects.Direction;
import net.simon987.server.logging.LogManager;
import net.simon987.server.user.User;
import org.bson.Document;
import org.json.simple.JSONObject;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HackedNPC extends NonPlayerCharacter implements ControllableUnit {

    private static final int MEM_SIZE = GameServer.INSTANCE.getConfig().getInt("hacked_npc_mem_size");
    private static final boolean DIE_ON_NO_ENERGY = GameServer.INSTANCE.getConfig().getInt("hacked_npc_die_on_no_energy") != 0;

    private CPU cpu;
    /**
     * List of attached hardware, 'modules'
     */
    private Map<Integer, HardwareModule> hardwareAddresses = new HashMap<>();
    private Map<Class<? extends HardwareModule>, Integer> hardwareModules = new HashMap<>();

    private Action currentAction = Action.IDLE;
    private Action lastAction = Action.IDLE;
    private ArrayList<char[]> consoleMessagesBuffer = new ArrayList<>(30); //todo load from conf
    private ArrayList<char[]> lastConsoleMessagesBuffer = new ArrayList<>(30);

    HackedNPC(char[] program) {

        cpu = new CPU();

        cpu.setMemory(new Memory(MEM_SIZE));
        cpu.setHardwareHost(this);
        cpu.getMemory().write(cpu.getCodeSectionOffset(), program, 0, program.length);

        for (Object serialisedHw : (List) NpcPlugin.DEFAULT_HACKED_NPC.get("hardware")) {
            HardwareModule hardware = GameServer.INSTANCE.getRegistry().deserializeHardware((Document) serialisedHw, this);
            hardware.setCpu(cpu);
            attachHardware(hardware, ((Document) serialisedHw).getInteger("address"));
        }

        setTask(new ExecuteCpuTask());
    }

    public HackedNPC(Document document) {
        super(document);

        setHp(document.getInteger("hp"));
        setDirection(Direction.getDirection(document.getInteger("direction")));

        cpu = new CPU();
        cpu.setHardwareHost(this);
        cpu.setMemory(new Memory((Document) document.get("memory")));
        cpu.setRegisterSet(RegisterSet.deserialize((Document) document.get("registerSet")));

        ArrayList hardwareList = (ArrayList) document.get("hardware");

        for (Object serialisedHw : hardwareList) {
            HardwareModule hardware = GameServer.INSTANCE.getRegistry().deserializeHardware((Document) serialisedHw, this);
            hardware.setCpu(cpu);
            attachHardware(hardware, ((Document) serialisedHw).getInteger("address"));
        }

        setTask(new ExecuteCpuTask());
    }

    @Override
    public void update() {
        super.update();

        lastAction = currentAction;
        currentAction = Action.IDLE;

        lastConsoleMessagesBuffer = new ArrayList<>(consoleMessagesBuffer);
        consoleMessagesBuffer.clear();

        for (HardwareModule module : hardwareAddresses.values()) {
            module.update();
        }

        //Self-destroy when age limit is reached
        if (getAge() >= NonPlayerCharacter.LIFETIME) {
            setDead(true);
        }

        //Don't bother calling checkCompleted()
        getTask().tick(this);
    }

    @Override
    public void setKeyboardBuffer(ArrayList<Integer> kbBuffer) {
        LogManager.LOGGER.warning("Something went wrong here: Hacked NPC has no keyboard module" +
                "@HackedNPC::setKeyBoardBuffer()");
        Thread.dumpStack();
    }

    @Override
    public void setParent(User user) {
        LogManager.LOGGER.warning("Something went wrong here: Hacked NPC has no parent" +
                "@HackedNPC::setParent()");
        Thread.dumpStack();
    }

    @Override
    public User getParent() {
        LogManager.LOGGER.warning("Something went wrong here: Hacked NPC has no parent" +
                "@HackedNPC::getParent()");
        Thread.dumpStack();
        return null;
    }

    @Override
    public ArrayList<Integer> getKeyboardBuffer() {
        LogManager.LOGGER.warning("Something went wrong here: Hacked NPC has no keyboard module" +
                "@HackedNPC::getKeyBoardBuffer()");
        Thread.dumpStack();
        return null;
    }

    @Override
    public Memory getFloppyData() {
        LogManager.LOGGER.warning("Something went wrong here: Hacked NPC has floppy data." +
                "@HackedNPC::getFloppyData()");
        Thread.dumpStack();
        return null;
    }

    @Override
    public void setAction(Action action) {
        currentAction = action;
    }

    @Override
    public ArrayList<char[]> getConsoleMessagesBuffer() {
        return lastConsoleMessagesBuffer;
    }

    @Override
    public int getConsoleMode() {
        LogManager.LOGGER.warning("Something went wrong here: Hacked NPC has no console UI." +
                "@HackedNPC::getConsoleMode()");
        Thread.dumpStack();
        return 0;
    }

    @Override
    public CPU getCpu() {
        return cpu;
    }

    @Override
    public void giveItem(Item item) {
        //Overwrite item at current position
        ((NpcInventory) getHardware(NpcInventory.class)).putItem(item);
    }

    @Override
    public void attachHardware(HardwareModule hardware, int address) {
        hardwareAddresses.put(address, hardware);
        hardwareModules.put(hardware.getClass(), address);
    }

    @Override
    public void detachHardware(int address) {
        hardwareAddresses.remove(address);

        Class<? extends HardwareModule> toRemove = null;
        for (Class<? extends HardwareModule> clazz : hardwareModules.keySet()) {
            if (hardwareModules.get(clazz) == address) {
                toRemove = clazz;
            }
        }
        hardwareModules.remove(toRemove);
    }

    @Override
    public boolean hardwareInterrupt(int address, Status status) {
        HardwareModule hardware = hardwareAddresses.get(address);

        if (hardware != null) {
            hardware.handleInterrupt(status);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public int hardwareQuery(int address) {
        HardwareModule hardware = hardwareAddresses.get(address);

        if (hardware != null) {
            return hardware.getId();
        } else {
            return 0;
        }
    }

    public int getEnergy() {
        NpcBattery battery = (NpcBattery) getHardware(NpcBattery.class);
        return battery.getEnergy();
    }

    public void setEnergy(int energy) {
        NpcBattery battery = (NpcBattery) getHardware(NpcBattery.class);
        battery.setEnergy(energy);

        if (energy == 0 && DIE_ON_NO_ENERGY) {
            setDead(true);
        }
    }

    public boolean spendEnergy(int amount) {

        NpcBattery battery = (NpcBattery) getHardware(NpcBattery.class);

        if (battery.getEnergy() - amount < 0) {
            if (DIE_ON_NO_ENERGY) {
                setDead(true);
            }
            return false;
        } else {
            battery.setEnergy(battery.getEnergy() - amount);
            return true;
        }
    }

    @Override
    public Document mongoSerialise() {
        Document dbObject = super.mongoSerialise();

        dbObject.put("direction", getDirection().ordinal());
        dbObject.put("hp", getHp());
        dbObject.put("action", lastAction.ordinal());

        List<Document> hardwareList = new ArrayList<>();

        for (Integer address : hardwareAddresses.keySet()) {

            HardwareModule hardware = hardwareAddresses.get(address);

            Document serialisedHw = hardware.mongoSerialise();
            serialisedHw.put("address", address);
            hardwareList.add(serialisedHw);
        }

        dbObject.put("hardware", hardwareList);

        dbObject.put("memory", cpu.getMemory().mongoSerialise());

        dbObject.put("registerSet", cpu.getRegisterSet().mongoSerialise());
        return dbObject;
    }

    public void storeEnergy(int amount) {

        NpcBattery battery = (NpcBattery) getHardware(NpcBattery.class);
        battery.setEnergy(Math.min(battery.getEnergy() + amount, battery.getMaxEnergy()));
    }

    private HardwareModule getHardware(Class<? extends HardwareModule> clazz) {
        return hardwareAddresses.get(hardwareModules.get(clazz));
    }

    public void setMaxEnergy(int maxEnergy) {
        NpcBattery battery = (NpcBattery) getHardware(NpcBattery.class);
        battery.setMaxEnergy(maxEnergy);
    }

    public int getMaxEnergy() {
        NpcBattery battery = (NpcBattery) getHardware(NpcBattery.class);
        return battery.getMaxEnergy();
    }

    @Override
    public boolean sendMessage(char[] message) {
        return false;
    }

    @Override
    public void setCurrentAction(Action action) {
        currentAction = action;
    }

    @Override
    public Action getCurrentAction() {
        return currentAction;
    }

    @Override
    public JSONObject jsonSerialise() {
        JSONObject json = super.jsonSerialise();

        for (HardwareModule module : hardwareAddresses.values()) {
            JSONObject hwJson = module.jsonSerialise();
            if (hwJson != null) {
                json.put(module.getClass().getName(), hwJson);
            }
        }

        json.put("direction", getDirection().ordinal());
        NpcInventory inv = (NpcInventory) getHardware(NpcInventory.class);
        Item item = inv.getItem();
        json.put("heldItem", item == null ? new ItemVoid().getId() : item.getId());
        json.put("hp", getHp());
        json.put("action", lastAction.ordinal());

        return json;
    }

    @Override
    public boolean onDeadCallback() {

        getWorld().decUpdatable();

        if (getSettlement() != null && getSettlement().getNpcs() != null) {
            getSettlement().getNpcs().remove(this);
        }

        GameServer.INSTANCE.getEventDispatcher().dispatch(new ObjectDeathEvent(this));

        return false;
    }

    @Override
    public JSONObject debugJsonSerialise() {
        return jsonSerialise();
    }
}