using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;

/// <summary>
/// GAME 1303 Tier 3 — Switch between characters on ONE player object. Roach Invasion
/// (4 insects with different HP and abilities), Spark of Corruption (4 party members).
///
/// Design: rather than four separate player prefabs (which fight over the camera,
/// PlayerInput, RespawnManager...), the player is a single object carrying every ability
/// component. A CharacterDefinition says which abilities are on, what the stats are, and
/// which Animator Controller to show. Swapping = apply a different definition.
///
/// SETUP
///  1. Player has PlayerController2D v1.1, Health v1.1, Animator, SpriteRenderer, and
///     every ability component any character might use (DoubleJump, Glide, Dash,
///     WallSlideJump, LadderClimber, MeleeAttack, ProjectileShooter...). Add this script.
///  2. Create a CharacterDefinition asset per character and drag them into Characters.
///  3. Input: default "Previous" / "Next" actions (Q/E, gamepad bumpers) cycle characters.
///     Optional: number keys via the Input System "Player" map — add actions named
///     "Character1"... and they'll be picked up automatically (OnCharacter1 etc.).
///  4. Separate Health Per Character: ON for Roach Invasion (each insect keeps its own HP;
///     a dead insect can't be selected). OFF for shared health.
///  5. OnCharacterChanged(definition) -> HUD portrait/name. Swap Cooldown prevents
///     spamming. Can't swap mid-dash/launch (MovementLocked).
///  6. Make sure the ability names in each definition match the component class names exactly.
/// </summary>
public class CharacterSwapper : MonoBehaviour
{
    public List<CharacterDefinition> characters = new List<CharacterDefinition>();
    public int startIndex = 0;
    public float swapCooldown = 0.3f;
    public bool separateHealthPerCharacter = true;
    public bool allowSwapInAir = true;

    [Header("Managed ability components (auto-filled from the object if empty)")]
    public List<MonoBehaviour> abilityComponents = new List<MonoBehaviour>();

    [Header("Events")]
    public UnityEvent<CharacterDefinition> OnCharacterChanged;
    public UnityEvent<int> OnCharacterIndexChanged;

    public int CurrentIndex { get; private set; } = -1;
    public CharacterDefinition Current => CurrentIndex >= 0 ? characters[CurrentIndex] : null;

    private PlayerController2D controller;
    private Health health;
    private Animator anim;
    private SpriteRenderer sr;
    private Rigidbody2D rb;
    private CameraTargetAssigner camAssigner;
    private float cooldown;
    private int[] storedHealth;

    // Type names we consider "abilities" when auto-filling
    static readonly string[] KnownAbilities =
        { "DoubleJump", "Glide", "Dash", "Sprint", "WallSlideJump", "LadderClimber", "GrapplingHook",
          "MagnetPull", "RailRide", "MomentumLauncher", "MeleeAttack", "ProjectileShooter" };

    void Awake()
    {
        controller = GetComponent<PlayerController2D>();
        health = GetComponent<Health>();
        anim = GetComponent<Animator>();
        sr = GetComponentInChildren<SpriteRenderer>();
        rb = GetComponent<Rigidbody2D>();
        camAssigner = FindFirstObjectByType<CameraTargetAssigner>();

        if (abilityComponents.Count == 0)
            foreach (var mb in GetComponents<MonoBehaviour>())
                if (System.Array.IndexOf(KnownAbilities, mb.GetType().Name) >= 0) abilityComponents.Add(mb);

        storedHealth = new int[characters.Count];
        for (int i = 0; i < characters.Count; i++) storedHealth[i] = characters[i].maxHealth;
    }

    void Start()
    {
        if (characters.Count > 0) Apply(Mathf.Clamp(startIndex, 0, characters.Count - 1), true);
    }

    void Update() { cooldown -= Time.deltaTime; }

    // ---- Input ----
    void OnNext(InputValue v)     { if (v.isPressed) Cycle(+1); }
    void OnPrevious(InputValue v) { if (v.isPressed) Cycle(-1); }
    void OnCharacter1(InputValue v) { if (v.isPressed) TrySwap(0); }
    void OnCharacter2(InputValue v) { if (v.isPressed) TrySwap(1); }
    void OnCharacter3(InputValue v) { if (v.isPressed) TrySwap(2); }
    void OnCharacter4(InputValue v) { if (v.isPressed) TrySwap(3); }

    public void Cycle(int step)
    {
        if (characters.Count < 2) return;
        int idx = CurrentIndex;
        for (int tries = 0; tries < characters.Count; tries++)
        {
            idx = (idx + step + characters.Count) % characters.Count;
            if (IsSelectable(idx)) { TrySwap(idx); return; }
        }
    }

    public bool TrySwap(int index)
    {
        if (index < 0 || index >= characters.Count || index == CurrentIndex) return false;
        if (cooldown > 0f || !IsSelectable(index)) return false;
        if (controller != null && (controller.MovementLocked || !controller.InputEnabled)) return false;
        if (!allowSwapInAir && controller != null && !controller.IsGrounded) return false;
        Apply(index, false);
        return true;
    }

    bool IsSelectable(int index)
    {
        if (!separateHealthPerCharacter) return true;
        return storedHealth[index] > 0;
    }

    void Apply(int index, bool first)
    {
        // Save outgoing health
        if (!first && separateHealthPerCharacter && health != null && CurrentIndex >= 0)
            storedHealth[CurrentIndex] = health.CurrentHealth;

        CurrentIndex = index;
        var def = characters[index];
        cooldown = swapCooldown;

        // Look
        if (anim != null && def.animatorController != null) anim.runtimeAnimatorController = def.animatorController;
        else if (sr != null && def.idleSprite != null) sr.sprite = def.idleSprite;
        if (sr != null) sr.color = def.tint;

        // Stats
        if (controller != null)
        {
            controller.moveSpeed = def.moveSpeed;
            controller.jumpForce = def.jumpForce;
        }
        if (rb != null) rb.gravityScale = def.gravityScale;
        if (health != null)
        {
            if (separateHealthPerCharacter)
            {
                health.SetMaxHealth(def.maxHealth, false);
                health.SetCurrentHealth(storedHealth[index]);
            }
            else health.SetMaxHealth(def.maxHealth, first);
        }

        // Abilities
        foreach (var mb in abilityComponents)
        {
            if (mb == null) continue;
            mb.enabled = def.enabledAbilities.Contains(mb.GetType().Name);
        }

        // Camera stays on this object — nothing to do, but re-assert in case the
        // student's version swaps objects instead.
        camAssigner?.SetTarget(transform);

        if (!first && def.swapInSound != null) AudioManager.Instance?.PlaySFX(def.swapInSound);
        OnCharacterChanged?.Invoke(def);
        OnCharacterIndexChanged?.Invoke(index);
    }

    /// <summary>
    /// SEPARATE-HEALTH MODE: wire player Health.OnDied -> this (INSTEAD of RespawnManager.OnPlayerDied).
    /// Marks the current character dead and swaps to a living one. When nobody is left it
    /// calls RespawnManager itself. Wire RespawnManager.OnRespawned -> ReviveAll.
    /// </summary>
    public void OnCurrentCharacterDied()
    {
        if (!separateHealthPerCharacter) { RespawnManager.Instance?.OnPlayerDied(); return; }
        storedHealth[CurrentIndex] = 0;
        for (int i = 0; i < characters.Count; i++)
            if (storedHealth[i] > 0) { Apply(i, false); return; }
        RespawnManager.Instance?.OnPlayerDied();   // whole party is down
    }

    public void ReviveAll()
    {
        for (int i = 0; i < characters.Count; i++) storedHealth[i] = characters[i].maxHealth;
        if (CurrentIndex >= 0 && health != null) health.SetCurrentHealth(storedHealth[CurrentIndex]);
    }
}