using UnityEngine;
using UnityEngine.AI;
using Ice.Extensions;

/// TODO: It would be a great post-jam excercise to refactor this into a more
/// reasonable state machine, it serves as a good example of complexity but
/// not too much. I don't yet have a standard solution for this.
[RequireComponent(typeof(NavMeshAgent))]
public class GolemController : MonoBehaviour {
  public Sprite icon;
  public GameObject golemRagdoll;

  [Header("Behavior")]
  public float minIdleTime = 3f;
  public float maxIdleTime = 10f;
  public float minWanderDist = 3f;
  public float maxWanderDist = 6f;
  public float minEatTime = 7f;
  public float maxEatTime = 15f;
  public float eatChance = 0.5f;
  public float playerAwarenessDistance = 10f;
  public float playerCommandRange = 12f;
  public float eatAwarenessDistance = 15f;
  public float watchTime = 5f;
  public float stayTime = 30f;

  [Header("Nav tweaks")]
  public float baseStoppingDistance = 0.5f;
  public float foodStoppingDistance = 1f;
  public float playerStoppingDistance = 2.5f;

  [Header("References")]
  public Renderer[] golemRenderers;
  public HeadLookController lookController;
  public Animator animator;

  private PlayerController player;
  private float playerDistance;
  private Transform playerTransform;
  private NavMeshAgent agent;
  private Flags flags;

  // Timers.
  private float giveUpFollowTimer;
  private float giveUpWatchTimer;
  private float playerWatchFoodTimer;
  private float idleTimer;
  private float eatTimer;
  private float lastHeardCommandTimer;
  private float fearTimer;
  private float giveUpStayTimer;

  private Vector3 lastPosition;
  private Vector3 fearOrigin;

  private enum State {
    IDLE,
    SEEK_POSITION,
    SEEK_FOOD,
    WATCHING_PLAYER,
    STAY,
    FOLLOW_PLAYER,
    AVOID_PLAYER,
    FEAR_OBJECT,
    EATING,
    EATING_PLAYER_OFFER
  }
  private State state = State.IDLE;

  private bool awareOfPlayer;
  private bool awareOfPlayerFood;
  private bool ateLast;

  private void Awake() {
    player = GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerController>();
    playerTransform = player.transform;
    agent = GetComponent<NavMeshAgent>();
    flags = Flags.Instance;
  }

  private void Update() {
    // Update timers.
    idleTimer -= Time.deltaTime;
    eatTimer -= Time.deltaTime;
    lastHeardCommandTimer -= Time.deltaTime;
    playerWatchFoodTimer -= Time.deltaTime;
    giveUpFollowTimer -= Time.deltaTime;
    giveUpWatchTimer -= Time.deltaTime;
    fearTimer -= Time.deltaTime;

    // Check for player awareness.
    playerDistance = Vector3.Distance(playerTransform.position, transform.position);
    var withinAwareness = playerDistance <= playerAwarenessDistance;
    if (!awareOfPlayer && withinAwareness) {
      HandlePlayerEnteredAwareness();
    } else if (awareOfPlayer && !withinAwareness && state != State.FOLLOW_PLAYER) {
      HandlePlayerLeftAwareness();
    }

    // Check for player food awareness.
    if (awareOfPlayer && IsPlayerOfferingRedCrystal()) {
      if (!awareOfPlayerFood) {
        awareOfPlayerFood = true;
        HandleAwareOfPlayerHoldingFood();
      }
    } else {
      awareOfPlayerFood = false;
    }


    bool atDestination = Vector3.Distance(transform.position, agent.destination) <= agent.stoppingDistance;

    // Per-frame state logic.
    switch (state) {
      case State.IDLE:
        if (idleTimer < 0f) {
          HandleIdleTimerFinished();
        }
        break;
      case State.SEEK_POSITION:
        if (atDestination) {
          HandleArriveDestination();
        }
        break;
      case State.SEEK_FOOD:
        if (atDestination) {
          HandleArriveDestination();
        }
        break;
      case State.FOLLOW_PLAYER:
        agent.SetDestination(playerTransform.position);
        // Make sure if the golem is level zero we never stop following player
        // so that we can always give food.
        if (giveUpFollowTimer < 0f && flags.GolemBondLevel > 0) {
          Debug.Log("Golem giving up following player!");
          EatOrMove();
        }
        if (atDestination) {
          HandleArrivePlayer();
        }
        break;
      case State.AVOID_PLAYER:
        var avoidanceTarget = FindAvoidanceTarget(playerTransform.position);
        if (avoidanceTarget.HasValue) {
          agent.SetDestination(avoidanceTarget.Value);
        }
        // Face the player and back away.
        SmoothRotateTowards(playerTransform.position);
        break;
      case State.FEAR_OBJECT:
        var fearAvoidanceTarget = FindAvoidanceTarget(fearOrigin);
        if (fearAvoidanceTarget.HasValue) {
          agent.SetDestination(fearAvoidanceTarget.Value);
        }
        // Face the fear target.
        SmoothRotateTowards(fearOrigin);
        if (fearTimer < 0f) {
          HandleFearOver();
        }
        break;
      case State.WATCHING_PLAYER:
        SmoothRotateTowards(playerTransform.position, 1f);
        if (IsPlayerOfferingRedCrystal() && playerWatchFoodTimer < 0f) {
          FollowPlayer();
        }
        // Make sure if the golem is level zero we never stop watching player
        // so that we can always give food.
        if (giveUpWatchTimer < 0f && flags.GolemBondLevel > 0) {
          Debug.Log("Golem giving up watching player!");
          EatOrMove();
        }
        break;
      case State.STAY:
        SmoothRotateTowards(playerTransform.position, 1f);
        if (giveUpStayTimer < 0f) {
          Debug.Log("Golem giving up stay!");
          Idle();
        }
        break;
      case State.EATING_PLAYER_OFFER:
        SmoothRotateTowards(agent.destination);
        if (eatTimer < 0f) {
          HandleEatRedCrystal();
        }
        break;
      case State.EATING:
        SmoothRotateTowards(agent.destination);
        if (eatTimer < 0f) {
          HandleEatTimerFinished();
        }
        break;
      default:
        Debug.LogError("Golem: Need update case for " + state);
        break;
    }


    // Update animation state.
    animator.SetFloat("speed", agent.velocity.magnitude);
    animator.SetBool("eating", state == State.EATING);
  }

  private void LateUpdate() {
    if (transform.position != lastPosition) {
      giveUpFollowTimer = 5f;
    }
    lastPosition = transform.position;
  }


  // External game events.
  public void HandleSwitch(Switch sw) {
    sw.Press();
  }
  public void HandleLeaveSwitch(Switch sw) {
    sw.Unpress();
  }

  public void HandleSmashed(SmashingRock rock) {
    var obj = Instantiate(golemRagdoll, transform.position, transform.rotation);
    obj.GetComponent<Rigidbody>().AddForce(rock.transform.forward * 100f, ForceMode.Impulse);
    foreach (var r in golemRenderers) {
      r.enabled = false;
    }
  }

  public void HandleGolemTrigger(GolemTrigger trigger) {
    if (trigger.DisableOtherTrigger != null) {
      trigger.DisableOtherTrigger.Triggered = true;
    } else if (trigger.SetBondLevel > 0) {
      flags.LevelUpGolemBond(trigger.SetBondLevel);
    } else if (trigger.ForceEat) {
      TryFindFoodAndEat(trigger.EatDistance);
    } else if (trigger.ForceFear) {
      // Check for food override.
      if (IsPlayerOfferingRedCrystal() && trigger.PlayerFoodOverridesFear) {
        trigger.Triggered = true;
        return;
      }

      // If we were following the player, show a message.
      if (state == State.FOLLOW_PLAYER) {
        UI.Instance.ShowInfo(Messages.GolemIsAfraid);
      }

      Debug.Log("Forcing fear on golem.");
      FearObject(trigger.gameObject);
    }
  }

  public void TryHearingCommand(int commandId) {
    if (playerDistance <= playerCommandRange) {
      switch (commandId) {
        case 1:
          FollowPlayer();
          break;
        case 2:
          Stay();
          break;
        default:
          break;
      }
    }
  }


  // State events.

  private void HandleEatRedCrystal() {
    flags.LevelUpGolemBond(1);
    flags.HoldingRedCrystal = false;
    flags.FoundAnyRedCrystal = true;
    Idle();
  }

  private void HandleArrivePlayer() {
    Debug.Log("[golem] HandleArrivePlayer");
    if (IsPlayerOfferingRedCrystal()) {
      animator.SetTrigger("eatingPlayerOffer");
      eatTimer = 1.5f;
      state = State.EATING_PLAYER_OFFER;
      agent.isStopped = true;
    } else {
      // If we got to the player and they arent offering a red crystal,
      // just watch them.
      WatchPlayer();
    }
  }

  private void HandleFearOver() {
    WatchPlayer();
  }

  private void HandleIdleTimerFinished() {
    Debug.Log("[golem] HandleIdleTimerFinished");
    EatOrMove();
  }

  private void HandleEatTimerFinished() {
    Debug.Log("[golem] HandleEatTimerFinished");
    Idle();
  }

  private void HandleLastHeardCommandTimerFinished() {
    Debug.Log("[golem] HandleLastHeardCommandTimerFinished");
  }

  private void HandleArriveDestination() {
    Debug.Log("[golem] HandleArriveDestination");

    // If we were seeking food, transition to eating.
    if (state == State.SEEK_FOOD) {
      Eat();
    } else {
      Idle();
    }
  }

  private void HandlePlayerEnteredAwareness() {
    if (awareOfPlayer) {
      // Do nothing if we're already aware.
      return;
    }

    if (state == State.FEAR_OBJECT || state == State.STAY) {
      // States that take precedence.
      return;
    }

    Debug.Log("[golem] HandlePlayerEnteredAwareness");
    awareOfPlayer = true;
    lookController.LookTarget = playerTransform;

    if (flags.GolemBondLevel > 0 || flags.HoldingRedCrystal) {
      WatchPlayer();
    } else {
      AvoidPlayer();
    }
  }

  private void HandlePlayerLeftAwareness() {
    if (state == State.FEAR_OBJECT || state == State.STAY) {
      // States that take precedence.
    }

    Debug.Log("[golem] HandlePlayerLeftAwareness");
    awareOfPlayer = false;
    lookController.LookTarget = null;

    // Only react if we were already focusing on the player.
    var wereFocusingOnPlayer =
      state == State.FOLLOW_PLAYER || state == State.AVOID_PLAYER || state == State.WATCHING_PLAYER;

    if (wereFocusingOnPlayer) {
      Idle();
    }

  }

  private void HandleAwareOfPlayerHoldingFood() {
    Debug.Log("[golem] HandlePlayerIsHoldingFood");

    if (flags.GolemBondLevel < 1) {
      // If we're not yet bonded, cautiously watch.
      playerWatchFoodTimer = 1f;
    } else {
      // Otherwise follow player immediately.
      playerWatchFoodTimer = 0f;
    }

  }


  // Executors.

  private void EatOrMove() {
    // Check for eating spots.
    var ate = TryFindFoodAndEat(eatAwarenessDistance);
    if (ate) {
      ateLast = true;
    } else {
      SeekRandomPosition();
      ateLast = false;
    }
  }

  private void Idle() {
    state = State.IDLE;
    agent.isStopped = true;
    idleTimer = Random.Range(minIdleTime, maxIdleTime);
  }

  private bool TryFindFoodAndEat(float distance) {
    // Check for eating spots.
    var colliders = Physics.OverlapSphere(transform.position, distance, LayerMask.GetMask("EatingSpot"));
    if (colliders.Length > 0 && (Random.value < eatChance || !ateLast)) {
      // Choose a random eating spot.
      var spot = colliders.RandomElement().transform;
      SeekFood(spot.position);
      return true;
    }
    return false;
  }

  private void Eat() {
    state = State.EATING;
    agent.isStopped = true;
    eatTimer = Random.Range(minEatTime, maxEatTime);
  }

  private void SeekRandomPosition() {
    // Try 3 times.
    for (int i = 0; i < 3; i++) {
      var dist = Random.Range(minWanderDist, maxWanderDist);
      var dir = Random.insideUnitSphere * dist;
      NavMeshHit hit;
      if (NavMesh.SamplePosition(transform.position + dir, out hit, dist, 1)) {
        SeekPosition(hit.position);
        return;
      }
    }
  }

  private void SeekPosition(Vector3 position) {
    state = State.SEEK_POSITION;
    agent.stoppingDistance = baseStoppingDistance;
    agent.updateRotation = true;
    agent.isStopped = false;
    agent.SetDestination(position);
  }

  private void SeekFood(Vector3 position) {
    state = State.SEEK_FOOD;
    agent.stoppingDistance = foodStoppingDistance;
    agent.updateRotation = true;
    agent.isStopped = false;
    agent.SetDestination(position);
  }

  private void FollowPlayer() {
    giveUpFollowTimer = 5f;
    lookController.LookTarget = playerTransform;
    awareOfPlayer = true;
    state = State.FOLLOW_PLAYER;
    agent.stoppingDistance = playerStoppingDistance;
    agent.updateRotation = true;
    agent.isStopped = false;
    agent.SetDestination(playerTransform.position);
  }

  private void AvoidPlayer() {
    UI.Instance.ShowInfo(Messages.GolemIsAfraid);
    state = State.AVOID_PLAYER;
    agent.stoppingDistance = baseStoppingDistance;
    agent.updateRotation = false;
    agent.isStopped = false;
  }

  private void FearObject(GameObject fearTarget) {
    lookController.LookTarget = fearTarget.transform;
    fearTimer = 1f;
    fearOrigin = fearTarget.transform.position;
    state = State.FEAR_OBJECT;
    agent.stoppingDistance = baseStoppingDistance;
    agent.updateRotation = false;
    agent.isStopped = false;
  }

  private void WatchPlayer() {
    giveUpWatchTimer = watchTime;
    state = State.WATCHING_PLAYER;
    agent.stoppingDistance = baseStoppingDistance;
    agent.updateRotation = false;
    agent.isStopped = true;
    agent.SetDestination(playerTransform.position);
  }

  private void Stay() {
    lookController.LookTarget = playerTransform;
    giveUpStayTimer = stayTime;
    state = State.STAY;
    agent.stoppingDistance = baseStoppingDistance;
    agent.updateRotation = false;
    agent.isStopped = true;
    agent.SetDestination(playerTransform.position);
  }


  // Helpers.

  private bool IsPlayerOfferingRedCrystal() {
    return flags.HoldingRedCrystal && player.IsOffering;
  }

  private void SmoothRotateTowards(Vector3 position, float speed = 5f) {
    var towards = (position - transform.position).normalized;
    if (towards == Vector3.zero) {
      return;
    }
    var targetRotation = Quaternion.LookRotation(towards, Vector3.up);
    transform.rotation = Quaternion.Slerp(
      transform.rotation, targetRotation, speed * Time.deltaTime);
  }

  private Vector3? FindAvoidanceTarget(Vector3 avoidancePosition) {
    // Select a point away from the position we're trying to avoid.
    var towards = (avoidancePosition - transform.position).normalized;
    var distance = 5f;
    NavMeshHit hit;
    if (NavMesh.SamplePosition(transform.position - towards, out hit, distance, 1)) {
      return hit.position;
    }
    return null;
  }
}
