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

package engine.loot;

import engine.gameManager.DbManager;
import engine.gameManager.ForgeManager;
import engine.mbEnums;
import engine.objects.Item;
import engine.objects.ItemTemplate;
import engine.objects.NPC;
import engine.objects.Warehouse;
import org.json.JSONArray;
import org.json.JSONObject;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

public class WorkOrder implements Delayed {

    // MB Dev notes:
    // Class defines a Forge rolling request made through a vendor
    // interaction; submitted to the ForgeManager singleton for completion.
    //
    // A workOrder once created will last until all items are either
    // completed or junked.  They are persisted in the table dyn_workorders.

    public int workOrderID;
    public NPC vendor;
    public AtomicInteger slots_used = new AtomicInteger(0);
    public int total_to_produce;
    public int total_produced;
    public boolean multiple_slot_request;
    public HashMap<mbEnums.ResourceType, Integer> production_cost = new HashMap<>();
    public HashMap<mbEnums.ResourceType, Integer> production_cost_total = new HashMap<>();
    public int templateID;
    public String item_name_override;
    public int prefixToken;
    public int suffixToken;
    public long rollingDuration;
    public long completionTime;
    public AtomicBoolean runCompleted = new AtomicBoolean(false);

    // This collection is serialized to the vendor rolling window in ManageNPCMsg.

    public ConcurrentHashMap.KeySetView<Item, Boolean> cooking = ConcurrentHashMap.newKeySet();

    public WorkOrder() {

    }

    public WorkOrder(JSONObject jsonWorkOrder) {

        // This constructor is used to load workOrders from disk
        // during bootstrap.  (dyn_workorders)

        this.workOrderID = jsonWorkOrder.getInt("workOrderID");
        this.vendor = NPC.getNPC(jsonWorkOrder.getInt("vendor"));
        this.slots_used.set(jsonWorkOrder.getInt("slots_used"));
        this.total_to_produce = jsonWorkOrder.getInt("total_to_produce");
        this.total_produced = jsonWorkOrder.getInt("total_produced");
        this.multiple_slot_request = jsonWorkOrder.getBoolean("multiple_slot_request");
        this.templateID = jsonWorkOrder.getInt("templateID");
        this.item_name_override = jsonWorkOrder.getString("item_name_override");
        this.prefixToken = jsonWorkOrder.getInt("prefixToken");
        this.suffixToken = jsonWorkOrder.getInt("suffixToken");
        this.rollingDuration = jsonWorkOrder.getLong("rollingDuration");
        this.completionTime = jsonWorkOrder.getLong("completionTime");
        this.runCompleted.set(jsonWorkOrder.getBoolean("runCompleted"));

        // Vendor sanity check.  Might have been deleted

        if (this.vendor == null)
            return;

        JSONObject productionCostMap = jsonWorkOrder.getJSONObject("production_cost");

        for (String key : productionCostMap.keySet()) {
            mbEnums.ResourceType resourceType = mbEnums.ResourceType.valueOf(key);
            int value = productionCostMap.getInt(key);
            this.production_cost.put(resourceType, value);
        }

        JSONObject productionTotalCostMap = jsonWorkOrder.getJSONObject("production_cost_total");

        for (String key : productionTotalCostMap.keySet()) {
            mbEnums.ResourceType resourceType = mbEnums.ResourceType.valueOf(key);
            int value = productionTotalCostMap.getInt(key);
            this.production_cost_total.put(resourceType, value);
        }

        // Reconstruct cooking items

        JSONArray tokenList = jsonWorkOrder.getJSONArray("cookingTokens");

        for (Object o : tokenList) {
            int prefix = ((JSONArray) o).getInt(0);
            int suffix = ((JSONArray) o).getInt(1);

            Item cookingItem = ForgeManager.forgeItem(this);
            cookingItem.prefixToken = prefix;
            cookingItem.suffixToken = suffix;
            cookingItem.setDateToUpgrade(this.completionTime);
        }
    }

    public static int validate(WorkOrder workOrder) {

        // Validate that a workOrder can be completed by both
        // the vendor and the forge.

        int validation_result = 0;

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

        if (workOrder.vendor.getBuilding() == null)
            return 58; //58: The formula is beyond the means of this facility

        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 (!Warehouse.calcCostOverrun(workOrder).isEmpty())
            return 10;     //18: You can't really afford that

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

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

        return validation_result;
    }

    public static boolean withdrawWorkOrderCost(WorkOrder workOrder) {

        HashMap<mbEnums.ResourceType, Integer> modified_production_cost = new HashMap<>(workOrder.production_cost_total);

        if (workOrder.vendor.building.getCity() == null)
            return false;

        int strongbox = workOrder.vendor.building.getStrongboxValue();

        // Early exit if strongbox can cover gold only roll.

        if (workOrder.production_cost_total.size() == 1 && workOrder.production_cost_total.get(mbEnums.ResourceType.GOLD) <= strongbox) {
            workOrder.vendor.building.setStrongboxValue(strongbox - workOrder.production_cost_total.get(mbEnums.ResourceType.GOLD));
            return true;
        }

        // Warehouse is required after this point

        Warehouse warehouse = workOrder.vendor.building.getCity().warehouse;

        if (warehouse == null)
            return false;

        // Take from strongbox first and deduct from production cost total

        if (workOrder.production_cost_total.get(mbEnums.ResourceType.GOLD) <= strongbox) {
            workOrder.vendor.building.setStrongboxValue(strongbox - workOrder.production_cost_total.get(mbEnums.ResourceType.GOLD));
            modified_production_cost.put(mbEnums.ResourceType.GOLD, 0);
        } else {
            int overflowAmount = workOrder.production_cost_total.get(mbEnums.ResourceType.GOLD) - strongbox;
            workOrder.vendor.building.setStrongboxValue(0);
            modified_production_cost.put(mbEnums.ResourceType.GOLD, overflowAmount);
        }

        // Deduct total production cost from warehouse

        modified_production_cost.forEach((key, value) -> warehouse.resources.put(key, warehouse.resources.get(key) - value));
        DbManager.WarehouseQueries.UPDATE_WAREHOUSE(warehouse);

        return true;
    }


    public static JSONObject toJson(WorkOrder workOrder) {

        // Workorders are persisted in JSON format.

        JSONObject jsonWorkOrder = new JSONObject();

        jsonWorkOrder.put("workOrderID", workOrder.workOrderID);
        jsonWorkOrder.put("vendor", workOrder.vendor.getObjectUUID());
        jsonWorkOrder.put("slots_used", workOrder.slots_used.get());
        jsonWorkOrder.put("total_to_produce", workOrder.total_to_produce);
        jsonWorkOrder.put("total_produced", workOrder.total_produced);
        jsonWorkOrder.put("multiple_slot_request", workOrder.multiple_slot_request);
        jsonWorkOrder.put("production_cost", workOrder.production_cost);
        jsonWorkOrder.put("production_cost_total", workOrder.production_cost_total);
        jsonWorkOrder.put("templateID", workOrder.templateID);
        jsonWorkOrder.put("item_name_override", workOrder.item_name_override);
        jsonWorkOrder.put("prefixToken", workOrder.prefixToken);
        jsonWorkOrder.put("suffixToken", workOrder.suffixToken);
        jsonWorkOrder.put("rollingDuration", workOrder.rollingDuration);
        jsonWorkOrder.put("completionTime", workOrder.completionTime);
        jsonWorkOrder.put("runCompleted", workOrder.runCompleted.get());

        ArrayList<Integer[]> cookingTokens = new ArrayList<>();

        for (Item item : workOrder.cooking)
            cookingTokens.add(new Integer[]{item.prefixToken, item.suffixToken});

        jsonWorkOrder.put("cookingTokens", cookingTokens);

        return jsonWorkOrder;
    }

    public String toString() {

        LocalDateTime localDateTime = Instant.ofEpochMilli(this.completionTime)
                .atZone(ZoneId.systemDefault()).toLocalDateTime();
        Duration duration = Duration.ofMillis(this.rollingDuration);

        String outSTring = "\r\nwordOrderID: " + this.workOrderID + "\r\n" +
                "vendor: " + this.vendor.getObjectUUID() + "\r\n" +
                "slots_used: " + this.slots_used.get() + "\r\n" +
                "total_to_produce: " + this.total_to_produce + "\r\n" +
                "total_produced: " + this.total_produced + "\r\n" +
                "templateID: " + this.templateID + "\r\n" +
                "item_name_override: " + this.item_name_override + "\r\n" +
                "prefixToken: " + this.prefixToken + "\r\n" +
                "suffixToken: " + this.suffixToken + "\r\n" +
                "rollingDuration: " + duration + "\r\n" +
                "completionTime: " + localDateTime + "\r\n" +
                "runCompleted: " + this.runCompleted.get() + "\r\n" +
                "productionCost: " + this.production_cost.toString() + "\r\n" +
                "totalProductionCost:: " + this.production_cost_total.toString();

        return outSTring;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        long timeRemaining = completionTime - System.currentTimeMillis();
        return unit.convert(timeRemaining, TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        return Long.compare(this.completionTime, ((WorkOrder) o).completionTime);
    }

    @Override
    public boolean equals(Object obj) {

        if (this == obj)
            return true;

        if (obj == null || getClass() != obj.getClass())
            return false;

        WorkOrder otherWorkOrder = (WorkOrder) obj;
        return workOrderID == otherWorkOrder.workOrderID;
    }

    @Override
    public int hashCode() {
        return Objects.hash(workOrderID);
    }

}
