package newhorizon.expand.block.production.factory;

import arc.Core;
import arc.graphics.g2d.Draw;
import arc.graphics.g2d.Fill;
import arc.graphics.g2d.TextureRegion;
import arc.math.Mathf;
import arc.math.geom.Geometry;
import arc.math.geom.Point2;
import arc.math.geom.Vec2;
import arc.struct.EnumSet;
import arc.struct.IntSeq;
import arc.struct.Seq;
import arc.util.Eachable;
import arc.util.Nullable;
import arc.util.Time;
import arc.util.Tmp;
import arc.util.io.Reads;
import arc.util.io.Writes;
import mindustry.content.Fx;
import mindustry.entities.Effect;
import mindustry.entities.units.BuildPlan;
import mindustry.game.Team;
import mindustry.gen.Building;
import mindustry.gen.Sounds;
import mindustry.graphics.Layer;
import mindustry.graphics.Pal;
import mindustry.input.Placement;
import mindustry.logic.LAccess;
import mindustry.type.*;
import mindustry.world.Block;
import mindustry.world.Tile;
import mindustry.world.blocks.liquid.Conduit;
import mindustry.world.blocks.payloads.Payload;
import mindustry.world.blocks.payloads.PayloadConveyor;
import mindustry.world.blocks.production.GenericCrafter;
import mindustry.world.blocks.units.UnitAssembler;
import mindustry.world.draw.DrawBlock;
import mindustry.world.draw.DrawDefault;
import mindustry.world.meta.BlockFlag;
import mindustry.world.meta.Stat;
import mindustry.world.meta.StatUnit;
import mindustry.world.meta.StatValues;
import newhorizon.expand.BasicMultiBlock;
import newhorizon.expand.block.inner.LinkBlock;

import static mindustry.Vars.*;

public class MultiBlockCrafter extends BasicMultiBlock {
    public @Nullable ItemStack outputItem;
    public @Nullable ItemStack[] outputItems;
    public @Nullable LiquidStack outputLiquid;
    public @Nullable LiquidStack[] outputLiquids;

    public boolean dumpExtraLiquid = true;
    public boolean ignoreLiquidFullness = false;

    public Seq<int[]> outputItemDirection = new Seq<>();
    public Seq<int[]> outputLiquidDirection = new Seq<>();

    public float craftTime = 80;
    public Effect craftEffect = Fx.none;
    public Effect updateEffect = Fx.none;
    public float updateEffectChance = 0.04f;
    public float updateEffectSpread = 4f;
    public float warmupSpeed = 0.019f;

    public DrawBlock drawer = new DrawDefault();

    public MultiBlockCrafter(String name) {
        super(name);

        update = true;
        solid = true;
        hasItems = true;
        sync = true;
        
        ambientSound = Sounds.loopMachine;
        ambientSoundVolume = 0.03f;

        flags = EnumSet.of(BlockFlag.factory);
    }

    public void addOutputItemDirection(int xOffset, int yOffset, Item item) {
        outputItemDirection.add(new int[]{xOffset, yOffset, item.id});
    }

    public void addOutputLiquidDirection(int xOffset, int yOffset, Liquid liquid) {
        outputLiquidDirection.add(new int[]{xOffset, yOffset, liquid.id});
    }

    public void enableRotate(){
        rotate = true;
        rotateDraw = true;
        drawArrow = false;
        quickRotate = false;
        allowDiagonal = false;
    }

    @Override
    public void setStats() {
        stats.timePeriod = craftTime;
        super.setStats();

        if((hasItems && itemCapacity > 0) || outputItems != null) stats.add(Stat.productionTime, craftTime / 60f, StatUnit.seconds);
        if(outputItems != null) stats.add(Stat.output, StatValues.items(craftTime, outputItems));
        if(outputLiquids != null) stats.add(Stat.output, StatValues.liquids(1f, outputLiquids));

        stats.remove(Stat.size);
        stats.add(Stat.size, "@x@", getMaxSize(size, 0).x, getMaxSize(size, 0).y);
    }

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

        //set up liquid bars for liquid outputs
        if(outputLiquids != null && outputLiquids.length > 0){
            //no need for dynamic liquid bar
            removeBar("liquid");

            //then display output buffer
            for(var stack : outputLiquids){
                addLiquidBar(stack.liquid);
            }
        }
    }

    @Override
    public void load(){
        super.load();
        drawer.load(this);
    }

    @Override
    public void loadIcon() {
        super.loadIcon();
        uiIcon = Core.atlas.find(name + "-icon", name);
    }

    @Override
    public void init(){
        if(outputItems == null && outputItem != null) outputItems = new ItemStack[]{outputItem};
        if(outputLiquids == null && outputLiquid != null) outputLiquids = new LiquidStack[]{outputLiquid};
        if(outputLiquid == null && outputLiquids != null && outputLiquids.length > 0) outputLiquid = outputLiquids[0];

        outputsLiquid = outputLiquids != null;

        if(outputItems != null) hasItems = true;
        if(outputLiquids != null) hasLiquids = true;

        super.init();
    }

    @Override
    public void drawOverlay(float x, float y, int rotation) {
        super.drawOverlay(x, y, rotation);
        for (int[] packed: outputItemDirection){
            MultiBlock.calculateRotatedOffsetPosition(Tmp.p1, packed[0], packed[1], size, 1, rotation);
            Item item = content.item(packed[2]);
            Draw.rect(item.fullIcon,
                    x - (size + 1) % 2f * 4f + Tmp.p1.x * tilesize,
                    y - (size + 1) % 2f * 4f + Tmp.p1.y * tilesize,
                    6f, 6f
            );
        }

        for (int[] packed: outputLiquidDirection){
            MultiBlock.calculateRotatedOffsetPosition(Tmp.p1, packed[0], packed[1], size, 1, rotation);
            Liquid liquid = content.liquid(packed[2]);
            Draw.rect(liquid.fullIcon,
                    x - (size + 1) % 2f * 4f + Tmp.p1.x * tilesize,
                    y - (size + 1) % 2f * 4f + Tmp.p1.y * tilesize,
                    8f, 8f
            );
        }
    }

    @Override
    public void drawPlanRegion(BuildPlan plan, Eachable<BuildPlan> list){
        drawer.drawPlan(this, plan, list);
    }

    @Override
    public TextureRegion[] icons(){
        return drawer.finalIcons(this);
    }

    @Override
    public boolean outputsItems(){
        return outputItems != null;
    }

    @Override
    public void getRegionsToOutline(Seq<TextureRegion> out){
        drawer.getRegionsToOutline(this, out);
    }

    public class AdaptCrafterBuild extends BasicMultiBuilding {
        public float progress;
        public float totalProgress;
        public float warmup;

        public boolean isValidOutputItemBuilding(Building other, Item item){
            if (outputItemDirection.isEmpty()) return true;
            for (int[] packed: outputItemDirection){
                if (item.id == packed[2]){
                    MultiBlock.calculateRotatedOffsetPosition(Tmp.p1, packed[0], packed[1], block.size, 1, rotation);
                    Building b = world.build(Tmp.p1.x + tileX(), Tmp.p1.y + tileY());
                    if (b == other || b instanceof LinkBlock.LinkBuild lb && lb.linkBuild == other) {
                        return true;
                    }
                }
            }
            return false;
        }

        public boolean isValidLiquidOutputBuilding(Building other, Liquid liquid){
            if (outputLiquidDirection.isEmpty()) return true;
            for (int[] packed: outputLiquidDirection){
                if (liquid.id == packed[2]){
                    MultiBlock.calculateRotatedOffsetPosition(Tmp.p1, packed[0], packed[1], block.size, 1, rotation);
                    Building b = world.build(Tmp.p1.x, Tmp.p1.y);
                    if (b == other || b instanceof LinkBlock.LinkBuild lb && lb.linkBuild == other) {
                        return true;
                    }
                }
            }
            return false;
        }

        @Override
        public boolean canDump(Building to, Item item) {
            return super.canDump(to, item) && isValidOutputItemBuilding(to, item);
        }

        @Override
        public boolean canDumpLiquid(Building to, Liquid liquid) {
            return super.canDumpLiquid(to, liquid) && isValidLiquidOutputBuilding(to, liquid);
        }

        @Override
        public void draw(){
            drawer.draw(this);
        }

        @Override
        public void drawLight(){
            super.drawLight();
            drawer.drawLight(this);
        }

        @Override
        public boolean shouldConsume(){
            if(outputItems != null){
                for(var output : outputItems){
                    if(items.get(output.item) + output.amount > itemCapacity){
                        return false;
                    }
                }
            }
            if(outputLiquids != null && !ignoreLiquidFullness){
                boolean allFull = true;
                for(var output : outputLiquids){
                    if(liquids.get(output.liquid) >= liquidCapacity - 0.001f){
                        if(!dumpExtraLiquid){
                            return false;
                        }
                    }else{
                        //if there's still space left, it's not full for all liquids
                        allFull = false;
                    }
                }

                //if there is no space left for any liquid, it can't reproduce
                if(allFull){
                    return false;
                }
            }

            return enabled;
        }

        @Override
        public void updateTile(){
            super.updateTile();
            if(efficiency > 0){
                progress += getProgressIncrease(craftTime);
                warmup = Mathf.approachDelta(warmup, warmupTarget(), warmupSpeed);
                if(outputLiquids != null){
                    float inc = getProgressIncrease(1f);
                    for(var output : outputLiquids){
                        handleLiquid(this, output.liquid, Math.min(output.amount * inc, liquidCapacity - liquids.get(output.liquid)));
                    }
                }
                if(wasVisible && Mathf.chanceDelta(updateEffectChance)){
                    updateEffect.at(x + Mathf.range(size * updateEffectSpread), y + Mathf.range(size * updateEffectSpread));
                }
            }else warmup = Mathf.approachDelta(warmup, 0f, warmupSpeed);
            totalProgress += warmup * Time.delta;
            if(progress >= 1f) craft();
            dumpOutputs();
        }

        @Override
        public float getProgressIncrease(float baseTime){
            if(ignoreLiquidFullness){
                return super.getProgressIncrease(baseTime);
            }

            //limit progress increase by maximum amount of liquid it can produce
            float scaling = 1f, max = 1f;
            if(outputLiquids != null){
                max = 0f;
                for(var s : outputLiquids){
                    float value = (liquidCapacity - liquids.get(s.liquid)) / (s.amount * edelta());
                    scaling = Math.min(scaling, value);
                    max = Math.max(max, value);
                }
            }

            //when dumping excess take the maximum value instead of the minimum.
            return super.getProgressIncrease(baseTime) * (dumpExtraLiquid ? Math.min(max, 1f) : scaling);
        }

        public float warmupTarget(){
            return 1f;
        }

        @Override
        public float warmup(){
            return warmup;
        }

        @Override
        public float totalProgress(){
            return totalProgress;
        }

        public void craft(){
            consume();

            if(outputItems != null){
                for(var output : outputItems){
                    for(int i = 0; i < output.amount; i++){
                        offload(output.item);
                    }
                }
            }

            if(wasVisible){
                craftEffect.at(x, y);
            }
            progress %= 1f;
        }

        public void dumpOutputs() {
            boolean timer = timer(timerDump, dumpTime / timeScale);
            if(outputItems != null && timer) {
                for(ItemStack output : outputItems){
                    dump(output.item);
                }
            }

            if(outputLiquids != null){
                for (LiquidStack liquid : outputLiquids) {
                    dumpLiquid(liquid.liquid, 2f);
                }
            }
        }

        @Override
        public double sense(LAccess sensor){
            if(sensor == LAccess.progress) return progress();
            //attempt to prevent wild total liquid fluctuation, at least for crafters
            if(sensor == LAccess.totalLiquids && outputLiquid != null) return liquids.get(outputLiquid.liquid);
            return super.sense(sensor);
        }

        @Override
        public float progress(){
            return Mathf.clamp(progress);
        }

        @Override
        public int getMaximumAccepted(Item item){
            return itemCapacity;
        }

        @Override
        public boolean shouldAmbientSound(){
            return efficiency > 0;
        }

        @Override
        public void write(Writes write){
            super.write(write);
            write.f(progress);
            write.f(warmup);
        }

        @Override
        public void read(Reads read, byte revision){
            super.read(read, revision);
            progress = read.f();
            warmup = read.f();
        }
    }
}
