using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Text;
using UnityEngine;

public interface Action
{
    int cost { get; }
    IEnumerator Execute(DroneController dc);
}

public class Move : Action
{
    public int cost { get { return Mathf.RoundToInt(distance) * 1; } }
    public float distance;

    public Move(float distance)
    {
        this.distance = distance;
    }

    public IEnumerator Execute(DroneController dc)
    {
        OutputController.instance.Log("advancing " + distance + " meters", OutputController.MessageType.Output);
        float totalDistanceMoved = 0f;
        while (totalDistanceMoved < distance)
        {
            // check collisions!
            var frac = totalDistanceMoved - (int)totalDistanceMoved;
            if ((frac > 0.8 || frac == 0) && (distance - totalDistanceMoved > 0.5f))
            {
                Vector3 target = dc.transform.position + dc.transform.rotation * Vector3.forward;

                if (!dc.world.IsValid(target) ||
                    (dc.world.blocked[dc.world.GetIndex(target)] &
                    (dc.tool == Tool.Grabbers ? World.BlockMask.ImpassableWithClaw : World.BlockMask.Impassable)
                    ) != 0)
                {
                    dc.transform.position = new Vector3(
                        Mathf.Round(dc.transform.position.x),
                        dc.transform.position.y,
                        Mathf.Round(dc.transform.position.z));

                    string err;
                    if (dc.world.IsValid(target) && dc.tool != Tool.Grabbers && (dc.world.blocked[dc.world.GetIndex(target)] & World.BlockMask.ImpassableWithClaw) == 0)
                        err = "Need grabber to traverse.";
                    else
                        err = "Path Blocked. Halting";

                    dc.FailAction(this, err, refund: Mathf.RoundToInt(distance - totalDistanceMoved));
                    yield break;
                }
            }
            
            float delta = dc.speed * Time.deltaTime;
            if (totalDistanceMoved + delta > distance)
                delta = distance - totalDistanceMoved;
            totalDistanceMoved += delta;
            var offset = dc.transform.rotation * new Vector3(0, 0, delta);

            dc.transform.position += offset;

            yield return new WaitForEndOfFrame();
        }
        dc.transform.position = new Vector3(
            Mathf.Round(dc.transform.position.x),
            dc.transform.position.y,
            Mathf.Round(dc.transform.position.z));
        dc.FinishAction();
    }
}

public class Turn : Action
{
    public int cost { get { return 1; } }
    public float degrees;
    public bool clockwise;

    public Turn(float degrees, bool clockwise)
    {
        this.degrees = degrees;
        this.clockwise = clockwise;
    }

    public IEnumerator Execute(DroneController dc)
    {
        OutputController.instance.Log("turning " + (clockwise ? "right" : "left"), OutputController.MessageType.Output);
        float totalDegreesTurned = 0f;
        while (totalDegreesTurned < degrees)
        {
            float delta = dc.turnSpeed * Time.deltaTime;
            if (totalDegreesTurned + delta > degrees)
                delta = degrees - totalDegreesTurned;
            totalDegreesTurned += delta;

            if (!clockwise)
                delta *= -1;

            dc.transform.Rotate(Vector3.up, delta);
            yield return new WaitForEndOfFrame();
        }
        dc.FinishAction();
    }
}

public class Pickup : Action
{
    public int cost { get { return 1; } }
    public Pickup()
    {

    }

    public IEnumerator Execute(DroneController dc)
    {
        Vector3 targetPos = dc.transform.position + dc.transform.rotation * Vector3.forward;
        Interactible target = dc.world.interactableMap[dc.world.GetIndex(targetPos)];

        if (target == null)
        {
            dc.FailAction(this, "No matter to pickup.");
            yield break;
        }

        if (!dc.canPickup)
        {
            dc.FailAction(this,"Unable to pickup any matter.");
            yield break;
        }

        if (!target.pickupable)
        {
            dc.FailAction(this, "Cannot detach these baryons from current bonds.");
            yield break;
        }

        OutputController.instance.Log("now holding " + target.name, OutputController.MessageType.Output);

        dc.holding = target;
        dc.FinishAction();
    }
}

public class Drop : Action
{
    public int cost { get { return 0; } }
    public Drop() { }
    public IEnumerator Execute(DroneController dc)
    {
        Vector3 targetPos = dc.transform.position + dc.transform.rotation * Vector3.forward;
        var world = dc.world;
        var index = world.GetIndex(targetPos);

        if ((world.blocked[index] & World.BlockMask.CannotDrop) != 0)
        {
            dc.FailAction(this, "Cannot drop matter here.");
            yield break;
        }

        world.interactableMap[index] = dc.holding;

        dc.holding = null;
        dc.FinishAction();
    }
}

public class Process : Action
{
    public int cost { get { return 20; } }
    public Process()
    {

    }

    public IEnumerator Execute(DroneController dc)
    {
        if (dc.holding == null)
        {
            dc.FailAction(this, "No matter held.");
            yield break;
        }

        var baryons = dc.holding;

        if (dc.joules > dc.jouleCapacity && baryons.joules > 0)
        {
            dc.FailAction(this, "Battery bank cannot hold more energy.");
            yield break;
        }

        dc.holding = null;
        dc.joules += baryons.joules;
        if (dc.joules > dc.jouleCapacity)
            dc.joules = dc.jouleCapacity;
        GameObject.Destroy(baryons.gameObject);
        dc.FinishAction();
    }
}

public class Inspect : Action
{
    public int cost { get { return 1; } }

    public IEnumerator Execute(DroneController dc)
    {
        Vector3 targetPos = dc.transform.position + dc.transform.rotation * Vector3.forward;
        if (!dc.world.IsValid(targetPos))
        {
            dc.FailAction(this, "No matter in range.");
            yield break;
        }

        Interactible target = dc.world.interactableMap[dc.world.GetIndex(targetPos)];

        if (target == null)
        {
            dc.FailAction(this, "No matter in range.");
            yield break;
        }

        OutputController.instance.Log(target.name, OutputController.MessageType.Output);
        OutputController.instance.Log(target.joules + "kJ", OutputController.MessageType.Output);
        if (target.cableUser)
            OutputController.instance.Log("can be drained", OutputController.MessageType.Output);
        if (target.crushable)
            OutputController.instance.Log("can be destroyed", OutputController.MessageType.Output);
        dc.FinishAction();
    }
}

public class LightToggle : Action
{
    public int cost { get { return 0; } }

    public IEnumerator Execute(DroneController dc)
    {
        if (!dc.canToggleLight)
        {
            dc.FailAction(this, "CPU does not allow toggling light");
            yield break;
        }

        var lights = dc.GetComponentsInChildren<Light>(true).Where(l => l.CompareTag("Headlight"));
        foreach (var l in lights)
        {
            l.enabled = !l.enabled;
            OutputController.instance.Log("headlamp turned " + (l.enabled ? "on" : "off"), OutputController.MessageType.Output);
        }
        dc.FinishAction();
        yield break;
    }
}

public class HyperJump : Action
{
    public int cost { get
        {
            if (destConfig == null)
                return 0;
            if (curConfig == null)
                return (int)Math.Pow(destConfig.distanceFromCore, 2);

            return (int)Math.Pow(destConfig.distanceFromCore - curConfig.distanceFromCore, 2);
        } }

    int destinationIndex;
    WorldGenConfig destConfig;
    WorldGenConfig curConfig;

    public HyperJump(int destinationIndex, int currentWorldIndex)
    {
        this.destinationIndex = destinationIndex;
        destConfig = Game.instance.worlds[destinationIndex].config;
        curConfig = Game.instance.worlds[currentWorldIndex].config;
    }

    public IEnumerator Execute(DroneController dc)
    {
        var destination = Game.instance.GetWorld(destinationIndex);

        if (destination == null)
        {
            dc.FailAction(this, "No destination selected");
            yield break;
        }
        yield return new WaitForEndOfFrame();
        yield return new WaitForEndOfFrame();
        yield return new WaitForEndOfFrame();

        Vector3 targetPos = new Vector3(0, -1997, 0);
        foreach (var warpPoint in destination.warpTiles)
        {
            if (!destination.IsOccupied(warpPoint))
            {
                targetPos = destination.GetPosition(warpPoint);
                break;
            }
        }
        if (targetPos.y == -1997)
        {
            OutputController.instance.Log("Target blocked by drones", OutputController.MessageType.Error);
            dc.FailAction(this, "");
            yield break;
        }

        OutputController.instance.Log("moving " + dc.name + " to " + destination.name, OutputController.MessageType.Output);

        dc.transform.position = targetPos;
        if (dc == DroneController.active)
            World.currentWorldIndex = destinationIndex;

        Game.instance.currentView = GameView.DroneControl;
        dc.FinishAction();
    }
}

public class JumpUI : Action
{
    public int cost { get { return 0; } }
    public JumpUI() { }

    public IEnumerator Execute(DroneController dc)
    {
        DroneController.active = dc;
        Game.instance.currentView = GameView.WarpSelection;
        dc.actionQueue.Clear();
        dc.FinishAction();
        yield break;
    }
}

public class Render : Action
{
    public int cost { get { return 1; } }

    public Render() { }

    public IEnumerator Execute(DroneController dc)
    {
        var camera = dc.GetComponentInChildren<Camera>();
        camera.enabled = true;
        var mainCam = Camera.main;
        if (mainCam != null)
            mainCam.enabled = false;
        var shading = dc.GetComponentInChildren<DroneCameraShading>();
        shading.render = true;
        dc.FinishAction();
        if (dc.camClass == CameraClass.Single)
            yield break;
        yield return new WaitForSeconds(4);
        camera.enabled = false;
        if (mainCam != null)
            mainCam.enabled = true;
        yield break;
    }
}

public class Deposit : Action
{
    public int cost { get { return amount; } }
    public int amount;

    public Deposit(int amount)
    {
        this.amount = amount;
    }

    public IEnumerator Execute(DroneController dc)
    {
        if (dc.worldIndex != Game.instance.droneSpawnWorldIndex)
        {
            dc.FailAction(this, "Must be in Forge to interface with Cosmic AC");
            yield break;
        }
        OutputController.instance.Log("transferring " + amount + " kJ to Cosmic AC", OutputController.MessageType.Output);
        AC.instance.joules += amount;
        dc.FinishAction();
    }
}

public class Drain : Action
{
    public int cost
    {
        get
        {
            return 0;
        }
    }

    public IEnumerator Execute(DroneController dc)
    {
        Vector3 targetPos = dc.transform.position + dc.transform.rotation * Vector3.forward;
        Interactible target = dc.world.interactableMap[dc.world.GetIndex(targetPos)];

        if (target == null)
        {
            dc.FailAction(this, "No matter in range.");
            yield break;
        }

        if (target == null || (!target.cableUser && dc.tool != Tool.FusionReactor) || target.joules == 0)
        {
            dc.FailAction(this, "This matter does not possess electrical energy.");
            yield break;
        }

        if (dc.tool != Tool.Cable && dc.tool != Tool.FusionReactor)
        {
            dc.FailAction(this, "A cable is required to drain electrical energy.");
            yield break;
        }
        int amount = target.joules;
        if (amount > dc.jouleCapacity - dc.joules)
            amount = dc.jouleCapacity - dc.joules;

        OutputController.instance.Log("draining...", OutputController.MessageType.Output);
        yield return new WaitForSeconds(1);
        OutputController.instance.Log("drained " + amount + " kJ from " + target.name, OutputController.MessageType.Output);


        dc.joules += amount;
        target.joules -= amount;
        dc.FinishAction();
    }
}

public class Crush : Action
{
    public int cost { get { return 5; } }

    public IEnumerator Execute(DroneController dc)
    {
        Vector3 targetPos = dc.transform.position + dc.transform.rotation * Vector3.forward;
        Interactible target = dc.world.interactableMap[dc.world.GetIndex(targetPos)];

        if (target == null)
        {
            dc.FailAction(this, "No matter in range.");
            yield break;
        }

        if (!target.crushable)
        {
            dc.FailAction(this, "This matter cannot be destroyed");
            yield break;
        }

        if (dc.tool != Tool.Hammer)
        {
            dc.FailAction(this, "A hammer is required to randomize matter.");
            yield break;
        }

        OutputController.instance.Log("crushing...", OutputController.MessageType.Output);
        yield return new WaitForSeconds(1);
        OutputController.instance.Log("destroyed " + target.name, OutputController.MessageType.Output);
        
        dc.world.AddPrefab(target.crushedPrefab, target.transform.position, target.transform.rotation);
        dc.world.RemoveObject(target.gameObject);

        dc.FinishAction();
    }
}

public class Refuel : Action
{
    public int cost { get { return amount; } }

    int amount;

    public Refuel(int amount)
    {
        this.amount = amount;
    }

    public IEnumerator Execute(DroneController dc)
    {
        Vector3 targetPos = dc.transform.position + dc.transform.rotation * Vector3.forward;
        int targetTile = dc.world.GetIndex(targetPos);
        DroneController target = Game.instance.drones.FirstOrDefault(d =>
           d.worldIndex == dc.worldIndex && d.world.GetIndex(d.transform.position) == targetTile
       );

        if (dc.tool != Tool.Refueler)
        {
            dc.FailAction(this, "A refueler is required to transfer energy.");
            yield break;
        }

        if (target == null)
        {
            dc.FailAction(this, "No drone in range.");
            yield break;
        }

        target.joules += amount;
        OutputController.instance.Log("transfered " + amount + " kJ to " + target.name, OutputController.MessageType.Output);
        dc.FinishAction();
    }
}
