package mindustry.entities.comp;

import arc.math.*;
import arc.math.geom.*;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.entities.*;
import mindustry.entities.bullet.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
import mindustry.type.*;

import static mindustry.Vars.*;

@Component
abstract class WeaponsComp implements Teamc, Posc, Rotc, Velc, Statusc{
    @Import float x, y, rotation, reloadMultiplier;
    @Import Vec2 vel;
    @Import UnitType type;

    /** temporary weapon sequence number */
    static int sequenceNum = 0;

    /** weapon mount array, never null */
    @SyncLocal WeaponMount[] mounts = {};
    @ReadOnly transient float aimX, aimY;
    @ReadOnly transient boolean isRotate;
    boolean isShooting;
    float ammo;

    float ammof(){
        return ammo / type.ammoCapacity;
    }

    void setWeaponRotation(float rotation){
        for(WeaponMount mount : mounts){
            mount.rotation = rotation;
        }
    }

    void setupWeapons(UnitType def){
        mounts = new WeaponMount[def.weapons.size];
        for(int i = 0; i < mounts.length; i++){
            mounts[i] = new WeaponMount(def.weapons.get(i));
        }
    }

    void controlWeapons(boolean rotateShoot){
        controlWeapons(rotateShoot, rotateShoot);
    }

    void controlWeapons(boolean rotate, boolean shoot){
        for(WeaponMount mount : mounts){
            mount.rotate = rotate;
            mount.shoot = shoot;
        }
        isRotate = rotate;
        isShooting = shoot;
    }

    void aim(Position pos){
        aim(pos.getX(), pos.getY());
    }

    /** Aim at something. This will make all mounts point at it. */
    void aim(float x, float y){
        Tmp.v1.set(x, y).sub(this.x, this.y);
        if(Tmp.v1.len() < type.aimDst) Tmp.v1.setLength(type.aimDst);

        x = Tmp.v1.x + this.x;
        y = Tmp.v1.y + this.y;

        for(WeaponMount mount : mounts){
            mount.aimX = x;
            mount.aimY = y;
        }

        aimX = x;
        aimY = y;
    }

    boolean canShoot(){
        return true;
    }

    @Override
    public void remove(){
        for(WeaponMount mount : mounts){
            if(mount.bullet != null){
                mount.bullet.time = mount.bullet.lifetime - 10f;
                mount.bullet = null;
            }
        }
    }

    /** Update shooting and rotation for this unit. */
    @Override
    public void update(){
        boolean can = canShoot();

        for(WeaponMount mount : mounts){
            Weapon weapon = mount.weapon;
            mount.reload = Math.max(mount.reload - Time.delta * reloadMultiplier, 0);

            float weaponRotation = this.rotation - 90 + (weapon.rotate ? mount.rotation : 0);
            float mountX = this.x + Angles.trnsx(this.rotation - 90, weapon.x, weapon.y),
                mountY = this.y + Angles.trnsy(this.rotation - 90, weapon.x, weapon.y);
            float shootX = mountX + Angles.trnsx(weaponRotation, weapon.shootX, weapon.shootY),
                shootY = mountY + Angles.trnsy(weaponRotation, weapon.shootX, weapon.shootY);
            float shootAngle = weapon.rotate ? weaponRotation + 90 : Angles.angle(shootX, shootY, mount.aimX, mount.aimY) + (this.rotation - angleTo(mount.aimX, mount.aimY));

            //update continuous state
            if(weapon.continuous && mount.bullet != null){
                if(!mount.bullet.isAdded() || mount.bullet.time >= mount.bullet.lifetime || mount.bullet.type != weapon.bullet){
                    mount.bullet = null;
                }else{
                    mount.bullet.rotation(weaponRotation + 90);
                    mount.bullet.set(shootX, shootY);
                    vel.add(Tmp.v1.trns(rotation + 180f, mount.bullet.type.recoil));
                }
            }else{
                //heat decreases when not firing
                mount.heat = Math.max(mount.heat - Time.delta * reloadMultiplier / mount.weapon.cooldownTime, 0);
            }

            //flip weapon shoot side for alternating weapons at half reload
            if(weapon.otherSide != -1 && weapon.alternate && mount.side == weapon.flipSprite &&
                mount.reload + Time.delta > weapon.reload/2f && mount.reload <= weapon.reload/2f){
                mounts[weapon.otherSide].side = !mounts[weapon.otherSide].side;
                mount.side = !mount.side;
            }

            //rotate if applicable
            if(weapon.rotate && (mount.rotate || mount.shoot) && can){
                float axisX = this.x + Angles.trnsx(this.rotation - 90,  weapon.x, weapon.y),
                    axisY = this.y + Angles.trnsy(this.rotation - 90,  weapon.x, weapon.y);

                mount.targetRotation = Angles.angle(axisX, axisY, mount.aimX, mount.aimY) - this.rotation;
                mount.rotation = Angles.moveToward(mount.rotation, mount.targetRotation, weapon.rotateSpeed * Time.delta);
            }else if(!weapon.rotate){
                mount.rotation = 0;
                mount.targetRotation = angleTo(mount.aimX, mount.aimY);
            }

            //shoot if applicable
            if(mount.shoot && //must be shooting
                can && //must be able to shoot
                (ammo > 0 || !state.rules.unitAmmo || team().rules().infiniteAmmo) && //check ammo
                (!weapon.alternate || mount.side == weapon.flipSprite) &&
                //TODO checking for velocity this way isn't entirely correct
                (vel.len() >= mount.weapon.minShootVelocity || (net.active() && !isLocal())) && //check velocity requirements
                mount.reload <= 0.0001f && //reload has to be 0
                Angles.within(weapon.rotate ? mount.rotation : this.rotation, mount.targetRotation, mount.weapon.shootCone) //has to be within the cone
            ){
                shoot(mount, shootX, shootY, mount.aimX, mount.aimY, shootAngle, Mathf.sign(weapon.x));

                mount.reload = weapon.reload;

                ammo--;
                if(ammo < 0) ammo = 0;
            }
        }
    }

    private void shoot(WeaponMount mount, float x, float y, float aimX, float aimY, float rotation, int side){
        Weapon weapon = mount.weapon;

        float baseX = this.x, baseY = this.y;
        boolean delay = weapon.firstShotDelay + weapon.shotDelay > 0f;

        (delay ? weapon.chargeSound : weapon.shootSound).at(x, y, Mathf.random(0.8f, 1.0f));

        BulletType ammo = weapon.bullet;
        float lifeScl = ammo.scaleVelocity ? Mathf.clamp(Mathf.dst(x, y, aimX, aimY) / ammo.range()) : 1f;

        sequenceNum = 0;
        if(delay){
            Angles.shotgun(weapon.shots, weapon.spacing, rotation, f -> {
                Time.run(sequenceNum * weapon.shotDelay + weapon.firstShotDelay, () -> {
                    if(!isAdded()) return;
                    mount.bullet = bullet(weapon, x + this.x - baseX, y + this.y - baseY, f + Mathf.range(weapon.inaccuracy), lifeScl);
                });
                sequenceNum++;
            });
        }else{
            Angles.shotgun(weapon.shots, weapon.spacing, rotation, f -> mount.bullet = bullet(weapon, x, y, f + Mathf.range(weapon.inaccuracy), lifeScl));
        }

        boolean parentize = ammo.keepVelocity;

        if(delay){
            Time.run(weapon.firstShotDelay, () -> {
                if(!isAdded()) return;

                vel.add(Tmp.v1.trns(rotation + 180f, ammo.recoil));
                Effect.shake(weapon.shake, weapon.shake, x, y);
                mount.heat = 1f;
                weapon.shootSound.at(x, y, Mathf.random(0.8f, 1.0f));
            });
        }else{
            vel.add(Tmp.v1.trns(rotation + 180f, ammo.recoil));
            Effect.shake(weapon.shake, weapon.shake, x, y);
            mount.heat = 1f;
        }

        weapon.ejectEffect.at(x, y, rotation * side);
        ammo.shootEffect.at(x, y, rotation, parentize ? this : null);
        ammo.smokeEffect.at(x, y, rotation, parentize ? this : null);
        apply(weapon.shootStatus, weapon.shootStatusDuration);
    }

    private Bullet bullet(Weapon weapon, float x, float y, float angle, float lifescl){
        float xr = Mathf.range(weapon.xRand);

        return weapon.bullet.create(this, team(),
        x + Angles.trnsx(angle, 0, xr),
        y + Angles.trnsy(angle, 0, xr),
        angle, (1f - weapon.velocityRnd) + Mathf.random(weapon.velocityRnd), lifescl);
    }
}
