// • ▌ ▄ ·.  ▄▄▄·  ▄▄ • ▪   ▄▄· ▄▄▄▄·  ▄▄▄·  ▐▄▄▄  ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀  █▪▀▀▀ ▀  ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀  ▀  ▀ ▀▀  █▪ ▀▀▀
//      Magicbane Emulator Project © 2013 - 2022
//                www.magicbane.com

package engine.gameManager;

import engine.loot.ModTableEntry;
import engine.loot.ModTypeTableEntry;
import engine.loot.WorkOrder;
import engine.mbEnums;
import engine.net.DispatchMessage;
import engine.net.client.msg.ItemProductionMsg;
import engine.objects.*;
import engine.powers.EffectsBase;
import engine.powers.poweractions.AbstractPowerAction;
import org.pmw.tinylog.Logger;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public enum ForgeManager implements Runnable {

    FORGE_MANAGER;

    private static final BlockingQueue<WorkOrder> forge = new DelayQueue<>();
    public static final AtomicInteger wordOrderCounter = new AtomicInteger(0);
    public static final HashMap<Integer, Item> inMemoryItemLookup = new HashMap<>();
    public static final HashMap<NPC, ArrayList<Item>> vendorItemLookup = new HashMap<>();
    public static final HashMap<NPC, ArrayList<WorkOrder>> vendorWorkOrderLookup = new HashMap<>();
    public static final HashMap<Item, WorkOrder> itemWorkOrderLookup = new HashMap<>();

    @Override

    public void run() {

        WorkOrder workOrder = null;

        while (true) {

            try {
                workOrder = forge.take();
            } catch (Exception e) {
                Logger.error(e);
            }

            // Completed or canceled work orders are not re-enqueued

            if (Objects.requireNonNull(workOrder).runCanceled || workOrder.runCompleted)
                continue;

            Logger.info("item forged:" + workOrder.workOrderID + " (" + workOrder.total_produced + "/" + workOrder.total_to_produce + ")");

            if (workOrder.total_produced >= workOrder.total_to_produce) {

                // Complete this workOrder.

                for (Item workOrderItem : workOrder.cooking) {
                    ItemProductionMsg outMsg = new ItemProductionMsg(workOrder.vendor.building, workOrder.vendor, workOrderItem, mbEnums.ProductionActionType.CONFIRM_PRODUCE, true);
                    DispatchMessage.dispatchMsgToInterestArea(workOrder.vendor, outMsg, mbEnums.DispatchChannel.SECONDARY, 700, false, false);
                }

                Logger.info("workOrder has completed: " + workOrder.workOrderID);
                workOrder.runCompleted = true;
            }

            if (workOrder.runCompleted == true)
                continue;

            // Persist items that were currently cooking

            persistItems(workOrder);

            // Create new set in memory items

            forgeItems(workOrder);

            // enQueue this workOrder again; back into the oven
            // until all items for this workOrder are completed.

            workOrder.completionTime = System.currentTimeMillis() + workOrder.rollingDuration;
            forge.add(workOrder);
            Logger.info(workOrder.toString());
        }
    }

    public static void start() {

        Thread forgeManager;
        forgeManager = new Thread(FORGE_MANAGER);

        forgeManager.setName("Forge Manager");
        forgeManager.start();
    }

    public static int submit(WorkOrder workOrder) {

        int validation_result = ForgeManager.validate(workOrder);

        if (validation_result != 0)
            return validation_result;

        workOrder.workOrderID = wordOrderCounter.incrementAndGet();
        workOrder.rollingDuration = ForgeManager.calcRollingDuration(workOrder);
        workOrder.completionTime = System.currentTimeMillis() + workOrder.rollingDuration;
        workOrder.slots_used = calcAvailableSlots(workOrder);

        // Cost to execute this workOrder

        workOrder.production_cost = calcProductionCost(workOrder);

        // Set total cost for this production run

        workOrder.production_cost_total.putAll(workOrder.production_cost);
        workOrder.production_cost_total.forEach((key, value) -> workOrder.production_cost_total.compute(key, (k, v) -> v * workOrder.total_to_produce));

        workOrder.total_produced = 0;
        workOrder.total_to_produce *= workOrder.slots_used;

        // Create in-memory items and add to collections

        forgeItems(workOrder);

        Logger.info(workOrder.toString());
        forge.add(workOrder);

        return validation_result;
    }

    public static int validate(WorkOrder workOrder) {

        int validation_result = 0;

        ItemTemplate template = ItemTemplate.templates.get(workOrder.templateID);

        if (!workOrder.vendor.charItemManager.hasRoomInventory(template.item_wt))
            return 30;  //30: That person cannot carry that item

        if (!workOrder.vendor.getItemModTable().contains((template.modTable)))
            return 59;   //59: This hireling does not have this formula

        if (!calcCostOverrun(workOrder).isEmpty())
            return 10;     //18: You can't really afford that

        // Forge must be protected in order to access warehouse.

        if (calcProductionCost(workOrder).size() > 1)
            if (!workOrder.vendor.building.protectionState.equals(mbEnums.ProtectionState.PROTECTED))
                return 193;     //193: Production denied: This building must be protected to gain access to warehouse

        return validation_result;
    }

    public static long calcRollingDuration(WorkOrder workOrder) {

        float rollingDuration;

        rollingDuration = workOrder.vendor.getBuilding().getRank() * -5L + 40;
        rollingDuration = TimeUnit.MINUTES.toMillis((long) rollingDuration);
        rollingDuration *= Float.parseFloat(ConfigManager.MB_PRODUCTION_RATE.getValue());

        ItemTemplate template = ItemTemplate.templates.get(workOrder.templateID);

        // Bane circles

        if (template.item_bane_rank > 0)
            rollingDuration = (long) template.item_bane_rank * 60 * 60 * 3 * 1000 * Float.parseFloat(ConfigManager.MB_PRODUCTION_RATE.getValue());

        return (long) rollingDuration;
    }

    public static int calcAvailableSlots(WorkOrder workOrder) {

        // Slots available in a forge are based on the npc rank

        int availableSlots = workOrder.vendor.getRank();

        // Slots currently used up by the npc workOrders

        for (WorkOrder npcWorkOrder : ForgeManager.vendorWorkOrderLookup.get(workOrder.vendor))
            availableSlots = availableSlots - npcWorkOrder.slots_used;

        // Single item rolls are msg_size of 0;

        if (availableSlots > 0 && workOrder.multiple_slot_request == 0)
            availableSlots = 1;

        return availableSlots;
    }

    public static HashMap<mbEnums.ResourceType, Integer> calcProductionCost(WorkOrder workOrder) {

        // Calculate the production cost for a single run of this workOrder

        HashMap<mbEnums.ResourceType, Integer> production_cost = new HashMap<>();
        ItemTemplate template = ItemTemplate.templates.get(workOrder.templateID);

        // Add gold and resource costs from template

        production_cost.put(mbEnums.ResourceType.GOLD, template.item_value);
        production_cost.putAll(template.item_resource_cost);

        // Calculate cost of prefix and suffix

        if (workOrder.prefixToken != 0) {
            EffectsBase prefix = PowersManager.getEffectByToken(workOrder.prefixToken);
            EffectsBase prefixValue = PowersManager.getEffectByIDString(prefix.getIDString() + 'A');
            production_cost.putAll(prefixValue.getResourcesForEffect());
        }

        if (workOrder.suffixToken != 0) {
            EffectsBase suffix = PowersManager.getEffectByToken(workOrder.suffixToken);
            EffectsBase suffixValue = PowersManager.getEffectByIDString(suffix.getIDString() + 'A');
            production_cost.putAll(suffixValue.getResourcesForEffect());
        }

        return production_cost;
    }

    public static HashMap<mbEnums.ResourceType, Integer> calcCostOverrun(WorkOrder workOrder) {

        HashMap<mbEnums.ResourceType, Integer> costMap = new HashMap<>();
        Warehouse warehouse;

        // See if we can meet gold only requirements and early exit

        if (workOrder.production_cost_total.size() == 1) {
            if (workOrder.vendor.building.getStrongboxValue() > workOrder.production_cost_total.get(mbEnums.ResourceType.GOLD))
                return costMap;
        }

        // Gold deficit exists so a warehouse is required

        warehouse = workOrder.vendor.building.getCity() == null ? null : workOrder.vendor.building.getCity().warehouse;

        if (warehouse == null)
            return workOrder.production_cost_total;

        // Method returns a map of resourceType that a transaction overdrafts.

        HashMap<mbEnums.ResourceType, Integer> overflowMap = new HashMap<>();

        for (mbEnums.ResourceType resourceType : workOrder.production_cost_total.keySet()) {

            int debit = warehouse.resources.get(resourceType) - workOrder.production_cost_total.get(resourceType);

            // Locked resources are always unavailable

            if (debit < 0 || warehouse.locked.contains(resourceType))
                overflowMap.put(resourceType, debit);
        }

        return overflowMap;
    }

    public static Item forgeItem(WorkOrder workOrder) {

        ItemTemplate template = ItemTemplate.templates.get(workOrder.templateID);

        Item forgedItem = new Item(workOrder.templateID);
        forgedItem.objectUUID = MobLoot.lastNegativeID.getAndDecrement();
        forgedItem.containerType = mbEnums.ItemContainerType.FORGE;
        forgedItem.ownerID = workOrder.vendor.getObjectUUID();
        forgedItem.setDateToUpgrade(workOrder.completionTime);

        // Give prefix and suffix to this item if random rolled

        if (workOrder.prefixToken == 0)
            forgedItem.prefixToken = calcRandomMod(workOrder.vendor, mbEnums.ItemModType.PREFIX, template.modTable);
        else
            forgedItem.prefixToken = workOrder.prefixToken;

        if (workOrder.suffixToken == 0)
            forgedItem.suffixToken = calcRandomMod(workOrder.vendor, mbEnums.ItemModType.SUFFIX, template.modTable);
        else
            forgedItem.suffixToken = workOrder.suffixToken;

        // Forged random rolled items are unidentified until completed

        if (workOrder.prefixToken == 0 && workOrder.suffixToken == 0)
            forgedItem.flags.remove(mbEnums.ItemFlags.Identified);

        return forgedItem;
    }

    public static void persistItems(WorkOrder workOrder) {

        ArrayList<Item> toRemove = new ArrayList<>();
        Item completed_item;

        for (Item workOrderItem : workOrder.cooking) {

            toRemove.add(workOrderItem);

            ItemProductionMsg outMsg = new ItemProductionMsg(workOrder.vendor.building, workOrder.vendor, workOrderItem, mbEnums.ProductionActionType.CONFIRM_SETPRICE, true);
            DispatchMessage.dispatchMsgToInterestArea(workOrder.vendor, outMsg, mbEnums.DispatchChannel.SECONDARY, 700, false, false);
            ItemProductionMsg outMsg2 = new ItemProductionMsg(workOrder.vendor.building, workOrder.vendor, workOrderItem, mbEnums.ProductionActionType.CONFIRM_DEPOSIT, true);
            DispatchMessage.dispatchMsgToInterestArea(workOrder.vendor, outMsg2, mbEnums.DispatchChannel.SECONDARY, 700, false, false);

            completed_item = DbManager.ItemQueries.PERSIST(workOrderItem);

            workOrder.vendor.charItemManager.addItemToInventory(completed_item);
        }
            // Remove the fake negativeID item from all collections.
            // set the items as 'completed'

        for (Item memoryItem : toRemove) {
            workOrder.cooking.remove(memoryItem);
            inMemoryItemLookup.remove(memoryItem.objectUUID);
            vendorItemLookup.get(workOrder.vendor).remove(memoryItem);
            itemWorkOrderLookup.remove(memoryItem);
        }

    }
    public static void forgeItems(WorkOrder workOrder) {

        for (int i = 0; i < workOrder.slots_used; ++i) {

            Item forged_item = forgeItem(workOrder);

            // Add item to in-memory caches

            workOrder.cooking.add(forged_item);

            inMemoryItemLookup.put(forged_item.objectUUID, forged_item);
            vendorItemLookup.computeIfAbsent(workOrder.vendor, k -> new ArrayList<>());
            vendorItemLookup.get(workOrder.vendor).add(forged_item);
            vendorWorkOrderLookup.get(workOrder.vendor).add(workOrder);
            itemWorkOrderLookup.put(forged_item, workOrder);

            // Update NPC window

            ItemProductionMsg outMsg = new ItemProductionMsg(workOrder.vendor.building, workOrder.vendor, forged_item, mbEnums.ProductionActionType.CONFIRM_PRODUCE, true);
            DispatchMessage.dispatchMsgToInterestArea(workOrder.vendor, outMsg, mbEnums.DispatchChannel.SECONDARY, 700, false, false);

            Logger.info("Forging item: " + forged_item.objectUUID + " (" + forged_item.templateID + ") " + forged_item.template.item_base_name);
            workOrder.total_produced = workOrder.total_produced + 1;
        }
    }

    public static int calcRandomMod(NPC vendor, mbEnums.ItemModType itemModType, int modTable) {

        int modifier = 0;
        ModTypeTableEntry modTypeTableEntry = null;
        ModTableEntry modRollEntry = null;
        int modifierRoll;

        switch (itemModType) {
            case PREFIX:
                modifier = vendor.getModTypeTable().get(vendor.getItemModTable().indexOf(modTable));
                modTypeTableEntry = ModTypeTableEntry.rollTable(modifier, ThreadLocalRandom.current().nextInt(1, 100 + 1));
                break;
            case SUFFIX:
                modifier = vendor.getModSuffixTable().get(vendor.getItemModTable().indexOf(modTable));
                modTypeTableEntry = ModTypeTableEntry.rollTable(modifier, ThreadLocalRandom.current().nextInt(1, 100 + 1));
                break;
        }

        if (modTypeTableEntry == null)
            return 0;

        modifierRoll = ThreadLocalRandom.current().nextInt(1, 100 + 1);

        if (modifierRoll < 80) {
            modifierRoll = LootManager.TableRoll(vendor.getLevel(), false);
            modRollEntry = ModTableEntry.rollTable(modTypeTableEntry.modTableID, modifierRoll);
        }

        if (modRollEntry != null) {
            AbstractPowerAction abstractPowerAction = PowersManager.getPowerActionByIDString(modRollEntry.action);
            modifier = abstractPowerAction.getEffectsBase().getToken();
        }

        return modifier;
    }
}