// ----------------------------------------------------------------------
// Wiring Bypass Kit
// ----------------------------------------------------------------------
//
// This is just a pile of electronics maintenance equipment. The usual
// stuff is here, like electrical tape, wire cutters, etc.
//
// There's also a meter that looks like it was made out of some beat
// up piece of equipment, and held together with duct tape.
//

const HDLD_KIRI_JUMPERCABLES = "jmp";

const jumperCablesRaycastRange = 48; // Same range as the DERP.
const jumperCablesFixRange     = 48; // Maximum distance to a broken linedef to fix, from the raycast hit.

class JumperCablesMapLoader : EventHandler
{
    Map<int, int> originalLinedefSpecials2;
    Array<JumperCablesDeployed> deployedCables;
    bool worldScanCompleted;

    static JumperCablesMapLoader Get()
    {
        JumperCablesMapLoader loader;
        loader = JumperCablesMapLoader(EventHandler.Find("JumperCablesMapLoader"));
        return loader;
    }

    override void WorldUnloaded(WorldEvent e)
    {
        JumperCablesMapLoader loader = Get();

        // Clear out anything from the last level.
        deployedCables.Clear();
        loader.originalLinedefSpecials2.Clear();
        worldScanCompleted = false;
    }

    override void WorldTick()
    {
        JumperCablesMapLoader loader = Get();

        if(!loader.worldScanCompleted) {

            // Go through all the linedefs and make note of what they're
            // supposed to do.
            for(int k = 0; k < level.Lines.size(); k++) {
                Line line = level.Lines[k];
                if(line.special != 0) {
                    loader.originalLinedefSpecials2.Insert(line.index(), line.special);
                }
            }

            loader.worldScanCompleted = true;
        }
    }

    override void WorldLineActivated(WorldEvent e)
    {
        // Is this line affected by a battery?
        for(int cableIndex = 0; cableIndex < deployedCables.size(); cableIndex++) {
            if(deployedCables[cableIndex].lineDefIndex == e.ActivatedLine.index()) {
                // Yes. Deplete the battery.
                deployedCables[cableIndex].DecrementBattery();
                break;
            }
        }
    }

    void RegisterCables(JumperCablesDeployed cableActor)
    {
        if(deployedCables.Find(cableActor) == deployedCables.size()) {
            deployedCables.Push(cableActor);
        }
    }

    void UnregisterCables(JumperCablesDeployed cableActor)
    {
        int index = deployedCables.find(cableActor);
        if(index != deployedCables.size()) {
            deployedCables.delete(index);
        }
    }
}

class JumperCablesDeployed : HDUPK
{
    int lineDefIndex;
    int charges;

    default
    {
		radius 2;
        height 4;
        +SpriteAngle;
        Scale 0.4;
    }

    states
    {
    spawn:
        JMPR CABD -1;
        stop;
    }

    override void PostBeginPlay()
    {
        bNoGravity = true;

        // Register us with the global event handler so we can
        // respond to the linedef getting used.
        JumperCablesMapLoader loader = JumperCablesMapLoader.Get();
        loader.RegisterCables(self);

        UpdateSprite();
    }

    void SetWallSprite(Vector2 lineDir)
    {
        bFlatSprite = false;
        bWallSprite = true;
        A_SetAngle(atan2(lineDir) + 90);
    }

    void SetFlatSprite(float angle)
    {
        bFlatSprite = true;
        bWallSprite = false;
        A_SetAngle(angle);
    }

    void UpdateSprite()
    {
        // Charge levels correspond to battery sprite charge levels.
		if(charges > 13) {
            frame = 0;
        } else if(charges > 6) {
            frame = 1;
        } else if(charges > 0) {
            frame = 2;
        } else {
            frame = 3;
        }
    }

    void DecrementBattery()
    {
        charges -= 1;
        // Console.printf("battery used, now: %d", charges);
        if(charges <= 0) {
            charges = 0;
            DespawnToBattery();
        }

        UpdateSprite();
    }

    void DespawnToBattery()
    {
        // Re-break the linedef.
        level.Lines[lineDefIndex].special = 0;

        // Spawn a battery, with the same level of charge.
        HDMagAmmo.SpawnMag(self, "HDBattery", self.charges);

        // Spawn some smoke.
        int smokeCount = random(1, 4);
        float smokeVel = 20.0;
        for(int k = 0; k < smokeCount; k++) {
            A_SpawnItemEx(
                "HDSmokeChunk",
                0.0, 0.0, 0.0,
                frandom(-smokeVel, smokeVel),
                frandom(-smokeVel, smokeVel),
                frandom(-smokeVel, smokeVel));
        }

        // Spawn a bunch of sparks.
        int sparkAmount = random(3,6);
        for(int k = 0; k < sparkAmount; k++) {
            SpawnSpark();
        }

        JumperCablesMapLoader loader = JumperCablesMapLoader.Get();
        loader.UnregisterCables(self);

        Destroy();
    }

    override bool OnGrab(Actor grabber)
    {
        DespawnToBattery();
        return false;
    }

    void SpawnSpark()
    {
        float sparkSpeed = 5.0;
        float sparkOffset = 2.0;
        A_StartSound("misc/arccrackle", CHAN_AUTO);
        A_SpawnParticle(
            "white",
            SPF_RELATIVE | SPF_FULLBRIGHT,
            random(1, 10), // lifetime
            frandom(1, 7), // size
            0, // angle
            // offset...
            frandom(-sparkOffset, sparkOffset),
            frandom(-sparkOffset, sparkOffset),
            frandom(-sparkOffset, sparkOffset),
            // vel...
            frandom(-sparkSpeed, sparkSpeed),
            frandom(-sparkSpeed, sparkSpeed),
            frandom(-sparkSpeed, sparkSpeed));
    }

    override void Tick()
    {
        if(frandom(0.0, 1.0) < 0.02) {
            SpawnSpark();
        }

        // Despawn to battery if the sector gets re-destroyed.
        if(level.lines[lineDefIndex].special == 0)
        {
            DespawnToBattery();
        }
    }
}

class JumperCablesUsable : HDWeapon
{
    int lastIndicatorStatus;

    default
    {
		+weapon.wimpy_weapon;
        +inventory.invbar;
        +hdweapon.droptranslation;
        +hdweapon.fitsinbackpack;
        hdweapon.barrelsize 0,0,0;
		weapon.selectionorder 1014;

		scale 0.6;
		inventory.icon "JMPKA0";
		inventory.pickupmessage "Picked up a wiring bypass kit.";
		inventory.pickupsound "derp/crawl";
		translation 0;
		tag "Wiring Bypass Kit";

		hdweapon.refid HDLD_KIRI_JUMPERCABLES;
	}

	override double WeaponBulk()
    {
		return 7;
	}

	override bool AddSpareWeapon(actor newowner)
    {
        return AddSpareWeaponRegular(newowner);
    }

    override hdweapon GetSpareWeapon(actor newowner,bool reverse,bool doselect)
    {
        return GetSpareWeaponRegular(newowner,reverse,doselect);
    }

	override void DrawSightPicture(
		HDStatusBar statusBar,
        HDWeapon weapon,
        HDPlayerPawn playerPawn,
		bool sightbob,
        Vector2 bob,
        double fov,
        bool scopeview,
        Actor hpc,
        String whichdot)
    {
        // Switch reticle colors depending on the LED status.
        String filename = String.Format(
            "graphics/jumpercables/jmpr_reticle%d.png",
            lastIndicatorStatus + 1);

        statusBar.drawImage(
            filename,
            (0, 0) + bob,
            statusBar.DI_SCREEN_CENTER | statusBar.DI_ITEM_TOP,
			scale : (0.5, 0.5));
    }

    states
    {

    spawn:
		JMPP # 0 offset(0, -32);
        JMPK A -1;
		stop;

	select0:
		JMPP A 1;
        goto select0big;

    deselect0:
		JMPP A 1;
        goto deselect0big;

    // LCD overlay states.
    ledd_left_indicator:
        JMIL A 1 offset(0, -32); wait;
    ledd_right_indicator:
        JMIR A 1 offset(0, -32); wait;

	ready:
		JMPP # 1 {

            if(PressingFire()) {
                SetWeaponState("deploy");
            }
            A_WeaponReady(WRF_NOFIRE | WRF_ALLOWUSER3); // USER3 = MagManager.

            // Line trace to see what we might be inspecting.
            FLineTraceData linetraceData;
            DoLineTrace(HDPlayerPawn(invoker.owner), linetraceData);

            // Overlay setup.
			A_Overlay(355, "ledd_left_indicator");
			A_Overlay(356, "ledd_right_indicator");

            // Update displays.
            UpdateVisualsForLineTrace(
                HDPlayerPawn(invoker.owner),
                linetraceData);
        }
        goto readyend;

    readyend:
		#### # 0 A_ReadyEnd();
        #### # 0 A_Jump(256,"ready");

    deploy:
        // Lower it down...
        JMPP A 1 offset(0, 16);
        JMPP A 1 offset(0, 32);
        JMPP A 1 offset(0, 48);
        JMPP A 1 offset(0, 64);
        JMPP A 1 offset(0, 72);
        JMPP A 1 offset(0, 96);
        JMPP A 1 offset(0, 128);

        // Play some sounds.
        JMPP A 0 A_JumpIf(!pressingfire(),"ready");
		JMPP A 4 A_StartSound("weapons/pismagclick",CHAN_WEAPON);
		JMPP A 2 A_StartSound("derp/crawl",CHAN_WEAPON,CHANF_OVERLAP);

        // Actual cable attachment happens here.
		JMPP A 1 {
            AttachCables(HDPlayerPawn(invoker.owner));
        }

        // Raise it back up...
        JMPP A 1 offset(0, 128);
        JMPP A 1 offset(0, 96);
        JMPP A 1 offset(0, 72);
        JMPP A 1 offset(0, 64);
        JMPP A 1 offset(0, 48);
        JMPP A 1 offset(0, 32);
        JMPP A 1 offset(0, 16);
        JMPP A 1 offset(0, 0);

		JMPP A 1 {
            A_WeaponReady(WRF_NOFIRE | WRF_ALLOWRELOAD | WRF_ALLOWUSER4);
        }

        goto ready;

	user3:
		#### # 0 A_MagManager("HDBattery");
		goto ready;

    }

    action void UpdateVisualsForLineTrace(HDPlayerPawn pawn, FLineTraceData linetraceData)
    {

        Line hitLine = linetraceData.hitLine;
        String particleColor = "darkred";
        Line brokenLine = LineTraceFindbrokenLine(linetraceData);

        // Set main frame (LEDs) based on raytrace status.
        if(brokenLine) {

            // Found somethine we can fix!
            particleColor = "green";
            player.getpsprite(PSP_WEAPON).frame = 1;

        } else {

            if(linetraceData.hitType == TRACE_HitNone) {

                // Didn't hit anything (yellow light).
                player.getpsprite(PSP_WEAPON).frame = 0;

            } else{

                // Hit something, but it's not broken (green light).
                player.getpsprite(PSP_WEAPON).frame = 2;

            }

        }

        // Set display overlay frames.

        // Default to random numbers.
        int frame_left = random(0, 15);
        int frame_right = random(0, 15);

        Line lineForDisplay = brokenLine;
        if(!lineForDisplay) {
            lineForDisplay = hitLine;
        }
        if(lineForDisplay) {
            JumperCablesMapLoader loader = JumperCablesMapLoader.Get();
            int originalSpecial = loader.originalLinedefSpecials2.Get(lineForDisplay.index());
            frame_right = originalSpecial % 16;
            frame_left = originalSpecial / 16;
        }
        player.getpsprite(355).frame = frame_left;
        player.getpsprite(356).frame = frame_right;

        // Play a sound if we just discovered a broken line.
        if(invoker.lastIndicatorStatus != player.getpsprite(PSP_WEAPON).frame) {
            if(invoker.lastIndicatorStatus != 1 && player.getpsprite(PSP_WEAPON).frame == 1) {
                A_StartSound("herp/beep", CHAN_WEAPON);
            }
        }

        // Set indicator status fo the HUD.
        invoker.lastIndicatorStatus = player.getpsprite(PSP_WEAPON).frame;

        // Debug particle display for the linetrace.
        if(hd_debug > 1) {
            if(linetraceData.hitType != TRACE_HitNone) {
                Vector3 hitPos = linetraceData.hitLocation - Vec3Offset(0, 0, 0);
                A_SpawnParticle(
                    particleColor,
                    0, 10, 2, 0,
                    hitPos.x,
                    hitPos.y,
                    hitPos.z);
            }
        }
    }

    action float Vec2Mag(Vector2 v)
    {
        return sqrt(v.x * v.x + v.y * v.y);
    }

    action float GetDistanceToLine(Line testLine, Vector3 position)
    {
        Vector2 pos2d;
        pos2d.x = position.x;
        pos2d.y = position.y;

        Vector2 origin = testLine.v1.p;

        // Get the normalized direction from v1 to v2.
        Vector2 direction = testLine.v2.p - origin;
        float direction_magnitude = Vec2Mag(direction);
        direction = direction / direction_magnitude;

        // Get the position relative to the origin.
        pos2d -= origin;

        // Project pos2d onto direction, to get a position along the line.
        double t = direction dot pos2d;

        // Clamp position along line to start/end.
        if(t < 0.0) {
            t = 0;
        }
        if(t > direction_magnitude) {
            t = direction_magnitude;
        }

        // Closest point on the line (as an offset from origin).
        Vector2 position_on_line = direction * t;

        Vector2 delta_to_line = position_on_line - pos2d;
        float final_distance = Vec2Mag(delta_to_line);

        return final_distance;
    }

    action void DoLineTrace(HDPlayerPawn pawn, FLineTraceData linetraceData)
    {
        pawn.LineTrace(
            pawn.angle,
            48,
            pawn.pitch,
            flags : TRF_THRUACTORS | TRF_ABSOFFSET,
            offsetz : pawn.gunPos.z - 1,
            offsetforward : pawn.gunPos.x,
            offsetside : pawn.gunPos.y,
            data : linetraceData);
    }

    action Line LineTraceFindbrokenLine(FLineTraceData linetraceData)
    {
        JumperCablesMapLoader loader = JumperCablesMapLoader.Get();

        // If we're placing this on a wall, then we'll just look at the wall's special.
        if(linetraceData.hitType == TRACE_HitWall) {

            if(linetraceData.hitLine) {

                Line hitLine = linetraceData.hitLine;

                if(loader.originalLinedefSpecials2.CheckKey(hitLine.index())) {

                    int originalSpecial = loader.originalLinedefSpecials2.Get(hitLine.index());
                    int currentSpecial = hitLine.special;

                    if(originalSpecial != currentSpecial)
                    {
                        return hitLine;
                    }
                }
            }
        }

        // If we hit a sector *or* got close enough to another linedef
        // attached to the sector that we're in, then let's find the
        // closest linedef within range that we know is damaged.
        if(linetraceData.hitType == TRACE_HitFloor ||
            linetraceData.hitType == TRACE_HitCeiling ||
            linetraceData.hitType == TRACE_HitWall)
        {
            Sector sec = linetraceData.hitSector;
            Line closestLineInRange = null;
            float closestLineDistance = jumperCablesFixRange;

            // Iterate through all linedefs connected to the sector.
            for(int secLineIndex = 0; secLineIndex < sec.lines.size(); secLineIndex++) {

                Line secLine = sec.lines[secLineIndex];

                // If this is currently special, we don't care about
                // it (it's working fine, presumably).
                if(secLine.special == 0) {

                    // If this *used* to be special, we DO care about it!
                    if(loader.originalLinedefSpecials2.CheckKey(secLine.index())) {

                        // Make sure it's actually changed.
                        int originalSpecial = loader.originalLinedefSpecials2.Get(secLine.index());
                        int currentSpecial = secLine.special;
                        if(originalSpecial != currentSpecial) {

                            // See if this is the closest one we've found so far.
                            float distanceToLine = GetDistanceToLine(
                                secLine, linetraceData.hitLocation);
                            if(distanceToLine <= closestLineDistance) {
                                closestLineInRange = secLine;
                                closestLineDistance = distanceToLine;
                            }
                        }
                    }
                }
            }

            // If we found something broken, then return the closest
            // thing we found.
            if(closestLineInRange) {
                return closestLineInRange;
            }

        }

        return null;
    }

    action void AttachCables(HDPlayerPawn pawn)
    {
        // Do we have enough ammo?
        HDMagAmmo batteryAmmo = HDMagAmmo(FindInventory("HDBattery"));
        if(!batteryAmmo || batteryAmmo.amount < 1) {
            invoker.owner.A_Log("You need a battery to use this.");
            return;
        }

        JumperCablesMapLoader loader = JumperCablesMapLoader.Get();
        FLineTraceData linetraceData;
        DoLineTrace(pawn, linetraceData);

        // Are we even close enough to a line?
        Line brokenLine = LineTraceFindbrokenLine(linetraceData);
        if(!brokenLine) {
            invoker.owner.A_Log("Not close enough to repair.");
            return;
        }

        if(!loader.originalLinedefSpecials2.CheckKey(brokenLine.index())) {
            invoker.owner.A_Log("No mechanism present to repair.");
            return;
        }

        // Check to see if it's something that actually needs fixing.
        int originalSpecial = loader.originalLinedefSpecials2.Get(brokenLine.index());
        int currentSpecial = brokenLine.special;
        if(originalSpecial == currentSpecial) {
            invoker.owner.A_Log("This is already functioning as normal.");
            return;
        }

        // Remove battery from inventory, but make sure we're actually
        // getting a charged one.
        int batteryCharges = batteryAmmo.TakeMag(true);
        if(batteryCharges < 1) {
            invoker.owner.A_Log("No charged batteries available to use.");
            batteryAmmo.AddAMag(batteryCharges); // Just give it back.
            return;
        }

        // Actually spawn the deployed item.
        JumperCablesDeployed deployed = JumperCablesDeployed(
            Spawn(
                "JumperCablesDeployed",
                linetraceData.hitLocation - linetraceData.hitDir * 4));

        deployed.lineDefIndex = brokenLine.index();
        deployed.bNoGravity = true;
        deployed.charges = batteryCharges;

        // Use a different kind of flat sprite and angle depending on
        // if we're attaching to wall or floor/ceiling.
        if(linetraceData.hitType == TRACE_HitFloor || linetraceData.hitType == TRACE_HitCeiling) {
            deployed.SetFlatSprite(invoker.owner.angle);
        } else {
            Vector2 lineDir = linetraceData.hitLine.v2.p - linetraceData.hitLine.v1.p;
            deployed.SetWallSprite(lineDir);
        }

        // Restore the damn line special already.
        brokenLine.special = originalSpecial;
    }

    override void DrawHUDStuff(HDStatusBar statusBar, HDWeapon weapon, HDPlayerPawn pawn)
    {
        // Copied and modified from ThunderBuster display.
		if(statusBar.hudlevel == 1) {
			statusBar.DrawBattery(
                -54, -4,
                statusBar.DI_SCREEN_CENTER_BOTTOM,
                reloadorder : true);
			statusBar.DrawNum(
                pawn.CountInv("HDBattery"),
                -46, -8,
                statusBar.DI_SCREEN_CENTER_BOTTOM);
		}
	}

    override String GetHelpText()
    {
        return WEPHELP_FIRE.."  Deploy\n";
    }
}

