using System;
using System.Collections.Generic;
using System.IO;

namespace LabOpt;

public static class LabOptPatchFunctions
{
    public static void SetRootId(ref LabComponent lab, int rootId)
    {
        lab.pcId = rootId;
    }

    public static void SetRootLabIdForStacking(FactorySystem factorySystem, int labId, int nextEntityId)
    {
        var labPool = factorySystem.labPool;
        var factory = factorySystem.factory;
        var rootId = labPool[labId].pcId;
        if (rootId <= 0)
        {
            var radiusBear = factory.planet.radius + 2.0f;
            if (factory.entityPool[labPool[labId].entityId].pos.sqrMagnitude > radiusBear * radiusBear)
                return;
            rootId = labId;
        }
        ref var rootLab = ref labPool[rootId];
        var needSetFunction = rootLab.researchMode || rootLab.recipeId > 0;
        var entitySignPool = needSetFunction ? factorySystem.factory.entitySignPool : null;
        var targetLabId = factory.entityPool[nextEntityId].labId;
        do
        {
            ref var targetLab = ref labPool[targetLabId];
            targetLab.pcId = rootId;
            // LabOptPatch.Logger.LogDebug($"Set rootLabId of lab {targetLabId} to {rootId}, {needSetFunction}");
            if (needSetFunction)
            {
                SetFunctionInternal(ref targetLab, rootLab.researchMode, rootLab.recipeId, rootLab.techId, entitySignPool, labPool);
            }
        } while ((targetLabId = labPool[targetLabId].nextLabId) > 0);
    }

    public static void SetRootLabIdOnLoading(FactorySystem factorySystem)
    {
        var labCursor = factorySystem.labCursor;
        var labPool = factorySystem.labPool;
        var parentDict = new Dictionary<int, int>();
        for (var id = 1; id < labCursor; id++)
        {
            if (labPool[id].id != id) continue;
            ref var lab = ref labPool[id];
            if (lab.researchMode)
            {
                var len = lab.matrixIncServed.Length;
                for (var i = 0; i < len; i++)
                {
                    if (lab.matrixIncServed[i] < 0)
                    {
                        lab.matrixIncServed[i] = 0;
                    }
                }
            }
            else if (lab.recipeId > 0)
            {
                var len = lab.incServed.Length;
                for (var i = 0; i < len; i++)
                {
                    if (lab.incServed[i] < 0)
                    {
                        lab.incServed[i] = 0;
                    }
                }
            }
            if (lab.nextLabId != 0) parentDict[lab.nextLabId] = id;
        }
        foreach (var pair in parentDict)
        {
            var rootId = pair.Value;
            while (parentDict.TryGetValue(rootId, out var parentId)) rootId = parentId;
            ref var targetLab = ref labPool[pair.Key];
            targetLab.pcId = rootId;
            // LabOptPatch.Logger.LogDebug($"Set rootLabId of lab {pair.Key} to {rootId}");
            AssignRootLabValues(ref labPool[rootId], ref targetLab);
        }
    }

    private static void AssignRootLabValues(ref LabComponent rootLab, ref LabComponent thisLab)
    {
        int len;
        if (rootLab.researchMode)
        {
            len = Math.Min(rootLab.matrixServed.Length, thisLab.matrixServed.Length);
            for (var i = 0; i < len; i++)
            {
                if (thisLab.matrixServed[i] > 0)
                {
                    rootLab.matrixServed[i] += thisLab.matrixServed[i];
                    thisLab.matrixServed[i] = 0;
                }
                if (thisLab.matrixIncServed[i] > 0)
                {
                    rootLab.matrixIncServed[i] += thisLab.matrixIncServed[i];
                    thisLab.matrixIncServed[i] = 0;
                }
            }
        }
        else if (rootLab.recipeId > 0)
        {
            len = Math.Min(rootLab.produced.Length, thisLab.produced.Length);
            for (var i = 0; i < len; i++)
            {
                if (thisLab.produced[i] <= 0) continue;
                rootLab.produced[i] += thisLab.produced[i];
                thisLab.produced[i] = 0;
            }
            len = Math.Min(rootLab.served.Length, thisLab.served.Length);
            for (var i = 0; i < len; i++)
            {
                if (thisLab.served[i] > 0)
                {
                    rootLab.served[i] += thisLab.served[i];
                    thisLab.served[i] = 0;
                }
                if (thisLab.incServed[i] > 0)
                {
                    rootLab.incServed[i] += thisLab.incServed[i];
                    thisLab.incServed[i] = 0;
                }
            }
        }
        thisLab.needs = rootLab.needs;
        thisLab.requires = rootLab.requires;
        thisLab.requireCounts = rootLab.requireCounts;
        thisLab.products = rootLab.products;
        thisLab.productCounts = rootLab.productCounts;
        thisLab.produced = rootLab.produced;
        thisLab.served = rootLab.served;
        thisLab.incServed = rootLab.incServed;
        thisLab.matrixPoints = rootLab.matrixPoints;
        thisLab.matrixServed = rootLab.matrixServed;
        thisLab.matrixIncServed = rootLab.matrixIncServed;
        thisLab.techId = rootLab.techId;
    }

    public static void LabExportZero(ref LabComponent lab, BinaryWriter w)
    {
        if (lab.matrixMode)
        {
            w.Write(lab.timeSpend);
            w.Write(lab.extraTimeSpend);
            w.Write(lab.requires.Length);
            foreach (var n in lab.requires)
            {
                w.Write(n);
            }
            w.Write(lab.requireCounts.Length);
            foreach (var n in lab.requireCounts)
            {
                w.Write(n);
            }
            w.Write(lab.served.Length);
            for (var i = 0; i < lab.served.Length; i++)
            {
                w.Write(0);
            }
            w.Write(lab.incServed.Length);
            for (var i = 0; i < lab.incServed.Length; i++)
            {
                w.Write(0);
            }
            w.Write(lab.needs.Length);
            for (var i = 0; i < lab.needs.Length; i++)
            {
                w.Write(0);
            }
            w.Write(lab.products.Length);
            foreach (var n in lab.products)
            {
                w.Write(n);
            }
            w.Write(lab.productCounts.Length);
            foreach (var n in lab.productCounts)
            {
                w.Write(n);
            }
            w.Write(lab.produced.Length);
            for (var i = 0; i < lab.produced.Length; i++)
            {
                w.Write(0);
            }
        }
        if (lab.researchMode)
        {
            w.Write(lab.matrixPoints.Length);
            foreach (var n in lab.matrixPoints)
            {
                w.Write(n);
            }
            w.Write(lab.matrixServed.Length);
            for (var i = 0; i < lab.matrixServed.Length; i++)
            {
                w.Write(0);
            }
            w.Write(lab.needs.Length);
            for (var i = 0; i < lab.needs.Length; i++)
            {
                w.Write(0);
            }
            w.Write(lab.matrixIncServed.Length);
            for (var i = 0; i < lab.matrixIncServed.Length; i++)
            {
                w.Write(0);
            }
        }
    }
    public static uint InternalUpdateAssembleNew(ref LabComponent lab, float power, int[] productRegister, int[] consumeRegister)
    {
        if (power < 0.1f)
        {
            return 0U;
        }

        if (lab.extraTime >= lab.extraTimeSpend)
        {
            var len = lab.products.Length;
            lock (lab.produced)
            {
                if (len == 1)
                {
                    lab.produced[0] += lab.productCounts[0];
                    lock (productRegister)
                    {
                        productRegister[lab.products[0]] += lab.productCounts[0];
                    }
                }
                else
                {
                    for (var i = 0; i < len; i++)
                    {
                        lab.produced[i] += lab.productCounts[i];
                        lock (productRegister)
                        {
                            productRegister[lab.products[i]] += lab.productCounts[i];
                        }
                    }
                }
            }

            lab.extraTime -= lab.extraTimeSpend;
        }

        if (lab.time >= lab.timeSpend)
        {
            lab.replicating = false;
            var len = lab.products.Length;
            lock (lab.produced)
            {
                if (len == 1)
                {
                    if (lab.produced[0] + lab.productCounts[0] > 30)
                    {
                        return 0U;
                    }

                    lab.produced[0] += lab.productCounts[0];
                    lock (productRegister)
                    {
                        productRegister[lab.products[0]] += lab.productCounts[0];
                    }
                }
                else
                {
                    for (var j = 0; j < len; j++)
                    {
                        if (lab.produced[j] + lab.productCounts[j] > 30)
                        {
                            return 0U;
                        }
                    }

                    for (var k = 0; k < len; k++)
                    {
                        lab.produced[k] += lab.productCounts[k];
                        lock (productRegister)
                        {
                            productRegister[lab.products[k]] += lab.productCounts[k];
                        }
                    }
                }
            }

            lab.extraSpeed = 0;
            lab.speedOverride = lab.speed;
            lab.extraPowerRatio = 0;
            lab.time -= lab.timeSpend;
        }

        if (!lab.replicating)
        {
            var len = lab.requireCounts.Length;
            int incLevel;
            if (len > 0)
            {
                var served = lab.served;
                lock (served)
                {
                    for (var l = 0; l < len; l++)
                    {
                        if (served[l] >= lab.requireCounts[l] && served[l] != 0) continue;
                        lab.time = 0;
                        return 0U;
                    }

                    incLevel = 10;
                    for (var m = 0; m < len; m++)
                    {
                        var splittedIncLevel = lab.split_inc_level(ref served[m], ref lab.incServed[m], lab.requireCounts[m]);
                        if (splittedIncLevel < incLevel) incLevel = splittedIncLevel;
                        if (served[m] == 0)
                        {
                            lab.incServed[m] = 0;
                        }
                        lock (consumeRegister)
                        {
                            consumeRegister[lab.requires[m]] += lab.requireCounts[m];
                        }
                    }

                    if (incLevel < 0)
                    {
                        incLevel = 0;
                    }
                }
            }
            else
            {
                incLevel = 0;
            }

            if (lab.productive && !lab.forceAccMode)
            {
                lab.extraSpeed = (int)(lab.speed * Cargo.incTableMilli[incLevel] * 10.0 + 0.1);
                lab.speedOverride = lab.speed;
                lab.extraPowerRatio = Cargo.powerTable[incLevel];
            }
            else
            {
                lab.extraSpeed = 0;
                lab.speedOverride = (int)(lab.speed * (1.0 + Cargo.accTableMilli[incLevel]) + 0.1);
                lab.extraPowerRatio = Cargo.powerTable[incLevel];
            }

            lab.replicating = true;
        }

        switch (lab.replicating)
        {
            case true when lab.time < lab.timeSpend && lab.extraTime < lab.extraTimeSpend:
                lab.time += (int)(power * lab.speedOverride);
                lab.extraTime += (int)(power * lab.extraSpeed);
                break;
            case false:
                return 0U;
        }

        return (uint)(lab.products[0] - LabComponent.matrixIds[0] + 1);
    }

    public static void SetFunctionManually(ref LabComponent lab, bool researchMode, int recpId, int techId, SignData[] signPool, LabComponent[] labPool)
    {
        var rootLabId = lab.pcId;
        if (rootLabId > 0)
        {
            ref var rootLab = ref labPool[rootLabId];
            if (researchMode != rootLab.researchMode)
            {
                SetFunctionManually(ref rootLab, researchMode, recpId, techId, signPool, labPool);
            }
            else if (researchMode)
            {
                if (techId != rootLab.techId)
                {
                    SetFunctionManually(ref rootLab, true, recpId, techId, signPool, labPool);
                }
            }
            else
            {
                if (recpId != rootLab.recipeId)
                {
                    SetFunctionManually(ref rootLab, false, recpId, techId, signPool, labPool);
                }
            }
        }
        else
        {
            SetFunctionInternal(ref lab, researchMode, recpId, techId, signPool, labPool);
            var thisId = lab.id;
            var needs = lab.needs;
            var nextLabId = lab.nextLabId;
            while (nextLabId > 0)
            {
                ref var nextLab = ref labPool[nextLabId];
                if (nextLab.pcId != thisId) break;
                if (needs != nextLab.needs)
                    SetFunctionInternal(ref nextLab, researchMode, recpId, techId, signPool, labPool);
                nextLabId = nextLab.nextLabId;
            }
        }
    }

    public static void SetFunctionInternal(ref LabComponent lab, bool researchMode, int recpId, int techId, SignData[] signPool, LabComponent[] labPool)
    {
        // LabOptPatch.Logger.LogDebug($"SetFunctionInternal: id={lab.id} root={(int)RootLabIdField.GetValue(lab)} research={researchMode} recp={recpId} tech={techId}");
        lab.replicating = false;
        lab.time = 0;
        lab.hashBytes = 0;
        lab.extraHashBytes = 0;
        lab.extraTime = 0;
        lab.extraSpeed = 0;
        lab.extraPowerRatio = 0;
        lab.productive = false;
        if (researchMode)
        {
            lab.forceAccMode = false;
            lab.researchMode = true;
            lab.recipeId = 0;
            lab.techId = 0;
            lab.timeSpend = 0;
            lab.extraTimeSpend = 0;
            lab.requires = null;
            lab.requireCounts = null;
            lab.served = null;
            lab.incServed = null;
            lab.products = null;
            lab.productCounts = null;
            lab.produced = null;
            lab.productive = true;
            var rootLabId = lab.pcId;
            if (rootLabId > 0)
            {
                ref var rootLab = ref labPool[rootLabId];
                lab.needs = rootLab.needs;
                lab.matrixPoints = rootLab.matrixPoints;
                lab.matrixServed = rootLab.matrixServed;
                lab.matrixIncServed = rootLab.matrixIncServed;
                lab.techId = rootLab.techId;
            }
            else
            {
                if (lab.needs == null || lab.needs.Length != LabComponent.matrixIds.Length)
                {
                    lab.needs = new int[LabComponent.matrixIds.Length];
                }

                Array.Copy(LabComponent.matrixIds, lab.needs, LabComponent.matrixIds.Length);
                if (lab.matrixPoints == null)
                {
                    lab.matrixPoints = new int[LabComponent.matrixIds.Length];
                }
                else
                {
                    Array.Clear(lab.matrixPoints, 0, lab.matrixPoints.Length);
                }

                lab.matrixServed ??= new int[LabComponent.matrixIds.Length];

                lab.matrixIncServed ??= new int[LabComponent.matrixIds.Length];

                TechProto techProto = LDB.techs.Select(techId);
                if (techProto != null && techProto.IsLabTech)
                {
                    lab.techId = techId;
                    for (var i = 0; i < techProto.Items.Length; i++)
                    {
                        var index = techProto.Items[i] - LabComponent.matrixIds[0];
                        if (index >= 0 && index < lab.matrixPoints.Length)
                        {
                            lab.matrixPoints[index] = techProto.ItemPoints[i];
                        }
                    }
                }
            }
            signPool[lab.entityId].iconId0 = (uint)lab.techId;
            signPool[lab.entityId].iconType = lab.techId == 0 ? 0U : 3U;
            return;
        }
        lab.researchMode = false;
        lab.recipeId = 0;
        lab.techId = 0;
        lab.matrixPoints = null;
        lab.matrixServed = null;
        lab.matrixIncServed = null;
        RecipeProto recipeProto = null;
        if (recpId > 0)
        {
            recipeProto = LDB.recipes.Select(recpId);
        }
        if (recipeProto != null && recipeProto.Type == ERecipeType.Research)
        {
            lab.recipeId = recipeProto.ID;
            lab.speed = 10000;
            lab.speedOverride = lab.speed;
            lab.timeSpend = recipeProto.TimeSpend * 10000;
            lab.extraTimeSpend = recipeProto.TimeSpend * 100000;
            lab.productive = recipeProto.productive;
            lab.forceAccMode &= lab.productive;
            var rootLabId = lab.pcId;
            if (rootLabId > 0)
            {
                ref var rootLab = ref labPool[rootLabId];
                lab.needs = rootLab.needs;
                lab.requires = rootLab.requires;
                lab.requireCounts = rootLab.requireCounts;
                lab.products = rootLab.products;
                lab.productCounts = rootLab.productCounts;
                lab.produced = rootLab.produced;
                lab.served = rootLab.served;
                lab.incServed = rootLab.incServed;
            }
            else
            {
                if (lab.needs == null || lab.needs.Length != 6)
                    lab.needs = new int[6];
                else
                    Array.Clear(lab.needs, 0, 6);
                lab.requires ??= new int[recipeProto.Items.Length];
                Array.Copy(recipeProto.Items, lab.requires, lab.requires.Length);
                lab.requireCounts ??= new int[recipeProto.ItemCounts.Length];
                Array.Copy(recipeProto.ItemCounts, lab.requireCounts, lab.requireCounts.Length);
                if (lab.served == null)
                    lab.served = new int[lab.requireCounts.Length];
                else
                    Array.Clear(lab.served, 0, lab.served.Length);
                if (lab.incServed == null)
                    lab.incServed = new int[lab.requireCounts.Length];
                else
                    Array.Clear(lab.incServed, 0, lab.incServed.Length);
                lab.products ??= new int[recipeProto.Results.Length];
                Array.Copy(recipeProto.Results, lab.products, lab.products.Length);
                lab.productCounts ??= new int[recipeProto.ResultCounts.Length];
                Array.Copy(recipeProto.ResultCounts, lab.productCounts, lab.productCounts.Length);
                if (lab.produced == null)
                    lab.produced = new int[lab.productCounts.Length];
                else
                    Array.Clear(lab.produced, 0, lab.produced.Length);
            }
        }
        else
        {
            lab.forceAccMode = false;
            lab.recipeId = 0;
            lab.speed = 0;
            lab.speedOverride = 0;
            lab.timeSpend = 0;
            lab.extraTimeSpend = 0;
            lab.requires = null;
            lab.requireCounts = null;
            lab.served = null;
            lab.incServed = null;
            lab.needs = null;
            lab.products = null;
            lab.productCounts = null;
            lab.produced = null;
        }
        signPool[lab.entityId].iconId0 = (uint)lab.recipeId;
        signPool[lab.entityId].iconType = lab.recipeId == 0 ? 0U : 2U;
    }

    public static int InsertIntoLab(PlanetFactory factory, int labId, int itemId, byte itemCount, byte itemInc, ref byte remainInc, int[] needs)
    {
        var factorySystem = factory.factorySystem;
        var served = factorySystem.labPool[labId].served;
        var incServed = factorySystem.labPool[labId].incServed;
        var matrixServed = factorySystem.labPool[labId].matrixServed;
        var matrixIncServed = factorySystem.labPool[labId].matrixIncServed;
        var len = needs.Length;
        for (var i = 0; i < len; i++)
        {
            if (needs[i] != itemId) continue;
            if (served != null)
            {
                lock (served)
                {
                    served[i] += itemCount;
                    incServed[i] += itemInc;
                }
                remainInc = 0;
                return itemCount;
            }
            if (matrixServed != null)
            {
                lock (matrixServed)
                {
                    matrixServed[i] += 3600 * itemCount;
                    matrixIncServed[i] += 3600 * itemInc;
                }
                remainInc = 0;
                return itemCount;
            }
        }
        return 0;
    }

    public static int PickFromLab(PlanetFactory factory, int labId, int filter, int[] needs)
    {
        var factorySystem = factory.factorySystem;
        var products = factorySystem.labPool[labId].products;
        if (products == null) return 0;
        var produced = factorySystem.labPool[labId].produced;
        if (produced == null) return 0;
        lock (produced)
        {
            for (var j = 0; j < products.Length; j++)
            {
                if (produced[j] <= 0 || products[j] <= 0 ||
                    (filter != 0 && filter != products[j]) ||
                    (needs != null && needs[0] != products[j] && needs[1] != products[j] &&
                     needs[2] != products[j] && needs[3] != products[j] &&
                     needs[4] != products[j] && needs[5] != products[j])) continue;
                produced[j]--;
                return products[j];
            }
        }
        return 0;
    }
}
