package mindustry.entities.comp;

import arc.graphics.*;
import arc.math.*;
import arc.math.geom.*;
import arc.util.*;
import mindustry.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.entities.*;
import mindustry.entities.EntityCollisions.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import mindustry.world.blocks.environment.*;

import static mindustry.Vars.*;

@Component
abstract class LegsComp implements Posc, Rotc, Hitboxc, Unitc{
    private static final Vec2 straightVec = new Vec2();

    @Import float x, y, rotation, speedMultiplier;
    @Import UnitType type;
    @Import Team team;
    @Import boolean disarmed;

    transient Leg[] legs = {};
    transient float totalLength;
    transient float moveSpace;
    transient float baseRotation;
    transient Floor lastDeepFloor;
    transient Vec2 curMoveOffset = new Vec2();

    @Replace
    @Override
    public SolidPred solidity(){
        return ignoreSolids() ? null : type.allowLegStep ? EntityCollisions::legsSolid : EntityCollisions::solid;
    }

    @Override
    @Replace
    public Floor drownFloor(){
        return lastDeepFloor;
    }

    @Override
    public void add(){
        resetLegs();
    }

    @Override
    public void unloaded(){
        resetLegs(1f);
    }

    @MethodPriority(-1)
    @Override
    public void destroy(){
        if(!isAdded() || Vars.headless) return;

        float legExplodeRad = type.legRegion.height  / 4f / 1.45f;

        //create effects for legs being destroyed
        for(int i = 0; i < legs.length; i++){
            Leg l = legs[i];

            Vec2 base = legOffset(Tmp.v1, i).add(x, y);

            Tmp.v2.set(l.base).sub(l.joint).inv().setLength(type.legExtension);

            for(Vec2 vec : new Vec2[]{base, l.joint, l.base}){
                Damage.dynamicExplosion(vec.x, vec.y, 0f, 0f, 0f, legExplodeRad, state.rules.damageExplosions, false, team, type.deathExplosionEffect);
            }

            Fx.legDestroy.at(base.x, base.y, 0f, new LegDestroyData(base.cpy(), l.joint, type.legRegion));
            Fx.legDestroy.at(l.joint.x, l.joint.y, 0f, new LegDestroyData(l.joint.cpy().add(Tmp.v2), l.base, type.legBaseRegion));

        }
    }

    public void resetLegs(){
        resetLegs(type.legLength);
    }

    public void resetLegs(float legLength){
        int count = type.legCount;

        this.legs = new Leg[count];

        if(type.lockLegBase){
            baseRotation = rotation;
        }

        for(int i = 0; i < legs.length; i++){
            Leg l = new Leg();

            float dstRot = legAngle(i);
            Vec2 baseOffset = legOffset(Tmp.v5, i).add(x, y);

            l.joint.trns(dstRot, legLength/2f).add(baseOffset);
            l.base.trns(dstRot, legLength).add(baseOffset);

            legs[i] = l;
        }
        totalLength = Mathf.random(100f);
    }

    @Override
    public void update(){
        if(Mathf.dst(deltaX(), deltaY()) > 0.001f){
            baseRotation = Angles.moveToward(baseRotation, Mathf.angle(deltaX(), deltaY()), type.rotateSpeed);
        }

        if(type.lockLegBase){
            baseRotation = rotation;
        }

        float legLength = type.legLength;

        //set up initial leg positions
        if(legs.length != type.legCount){
            resetLegs();
        }

        float moveSpeed = type.legSpeed;
        int div = Math.max(legs.length / type.legGroupSize, 2);
        moveSpace = legLength / 1.6f / (div / 2f) * type.legMoveSpace;
        //TODO should move legs even when still, based on speed. also, to prevent "slipping", make sure legs move when they are too far from their destination
        totalLength += type.legContinuousMove ? type.speed * speedMultiplier * Time.delta : Mathf.dst(deltaX(), deltaY());

        float trns = moveSpace * 0.85f * type.legForwardScl;

        //rotation + offset vector
        boolean moving = moving();
        Vec2 moveOffset = !moving ? Tmp.v4.setZero() : Tmp.v4.trns(Angles.angle(deltaX(), deltaY()), trns);
        //make it smooth, not jumpy
        moveOffset = curMoveOffset.lerpDelta(moveOffset, 0.1f);

        lastDeepFloor = null;
        int deeps = 0;

        for(int i = 0; i < legs.length; i++){
            float dstRot = legAngle(i);
            Vec2 baseOffset = legOffset(Tmp.v5, i).add(x, y);
            Leg l = legs[i];

            //TODO is limiting twice necessary?
            l.joint.sub(baseOffset).clampLength(type.legMinLength * legLength/2f, type.legMaxLength * legLength/2f).add(baseOffset);
            l.base.sub(baseOffset).clampLength(type.legMinLength * legLength, type.legMaxLength * legLength).add(baseOffset);

            float stageF = (totalLength + i*type.legPairOffset) / moveSpace;
            int stage = (int)stageF;
            int group = stage % div;
            boolean move = i % div == group;
            boolean side = i < legs.length/2;
            //back legs have reversed directions
            boolean backLeg = Math.abs((i + 0.5f) - legs.length/2f) <= 0.501f;
            if(backLeg && type.flipBackLegs) side = !side;
            if(type.flipLegSide) side = !side;

            l.moving = move;
            l.stage = moving ? stageF % 1f : Mathf.lerpDelta(l.stage, 0f, 0.1f);

            Tile tile = Vars.world.tileWorld(l.base.x, l.base.y);
            Color floorColor = tile == null ? Color.clear : tile.getFloorColor();
            Floor floor = tile == null ? Blocks.air.asFloor() : tile.floor();

            if(floor.isDeep()){
                deeps ++;
                lastDeepFloor = floor;
            }

            if(l.group != group){

                //create effect when transitioning to a group it can't move in
                if(!move && (moving || !type.legContinuousMove) && i % div == l.group){
                    if(!headless && !inFogTo(player.team())){
                        if(floor.isLiquid){
                            floor.walkEffect.at(l.base.x, l.base.y, type.rippleScale, floorColor);
                            floor.walkSound.at(x, y, 1f, floor.walkSoundVolume);
                        }else{
                            Fx.unitLandSmall.at(l.base.x, l.base.y, type.rippleScale, floorColor);
                            type.stepSound.at(l.base.x, l.base.y, type.stepSoundPitch + Mathf.range(type.stepSoundPitchRange), type.stepSoundVolume);
                        }

                        //shake when legs contact ground
                        if(type.stepShake > 0){
                            Effect.shake(type.stepShake, type.stepShake, l.base);
                        }
                    }

                    if(type.legSplashDamage > 0 && !disarmed){
                        Damage.damage(team, l.base.x, l.base.y, type.legSplashRange, type.legSplashDamage * state.rules.unitDamage(team), false, true);

                        if(tile != null && tile.block().unitMoveBreakable){
                            ConstructBlock.deconstructFinish(tile, tile.block(), self());
                        }
                    }
                }

                l.group = group;
            }

            //leg destination
            Vec2 legDest = Tmp.v1.trns(dstRot, legLength * type.legLengthScl).add(baseOffset).add(moveOffset);
            //join destination
            Vec2 jointDest = Tmp.v2;
            InverseKinematics.solve(legLength/2f, legLength/2f, Tmp.v6.set(l.base).sub(baseOffset), side, jointDest);
            jointDest.add(baseOffset);
            Tmp.v6.set(baseOffset).lerp(l.base, 0.5f);

            if(move){
                float moveFract = stageF % 1f;

                l.base.lerpDelta(legDest, moveFract);
                l.joint.lerpDelta(jointDest, moveFract / 2f);
            }

            l.joint.lerpDelta(jointDest, moveSpeed / 4f);

            //limit again after updating
            l.joint.sub(baseOffset).clampLength(type.legMinLength * legLength/2f, type.legMaxLength * legLength/2f).add(baseOffset);
            l.base.sub(baseOffset).clampLength(type.legMinLength * legLength, type.legMaxLength * legLength).add(baseOffset);
        }

        //when at least 1 leg is touching land, it can't drown
        if(deeps != legs.length || !floorOn().isDeep()){
            lastDeepFloor = null;
        }
    }

    Vec2 legOffset(Vec2 out, int index){
        out.trns(defaultLegAngle(index), type.legBaseOffset);

        if(type.legStraightness > 0){
            straightVec.trns(defaultLegAngle(index) - baseRotation, type.legBaseOffset);
            straightVec.y = Mathf.sign(straightVec.y) * type.legBaseOffset * type.legStraightLength;
            straightVec.rotate(baseRotation);
            out.lerp(straightVec, type.baseLegStraightness);
        }

        return out;
    }

    /** @return outwards facing angle of leg at the specified index. */
    float legAngle(int index){
        if(type.legStraightness > 0){
            return Mathf.slerp(defaultLegAngle(index), (index >= legs.length/2 ? -90 : 90f) + baseRotation, type.legStraightness);
        }
        return defaultLegAngle(index);
    }

    float defaultLegAngle(int index){
        return baseRotation + 360f / legs.length * index + (360f / legs.length / 2f);
    }

}
