using System;
using System.Collections.Generic;
using UnityEngine;
using Drawing;
using Sirenix.OdinInspector;
using Sirenix.Serialization;
using Unity.Mathematics;
using UnityEngine.Serialization;

[Serializable]
public class EnvironmentObserver{
    enum LabelDrawingLocation{
        PlayerOffset,
        HitLocation,
        IntersectingLength,
    }

    enum ObserverGizmoDrawingCondition{
        Always,
        OnlyActive,
        Never
    }

    public enum CastType{
        Ray,
        BoxOverlap,
        SphereOverlap,
        BoxCast,
        SphereCast
    }
    
    [PropertySpace(0, 5), LabelWidth(60)]
    public string label;
    [PropertySpace(0, 10), LabelWidth(60)]
    public CastType castType;

    [Button(ButtonSizes.Large), GUIColor("@GetObserverStatusColorStatic(active, hit)"), PropertyOrder(-1), PropertySpace(5, 5)]
    public void Active(){
        active = !active;
    }
    
    [HideInInspector]
    public bool active;

    // Parameters for Cast cast types
    [FoldoutGroup("Settings")] public float length;
    [FoldoutGroup("Settings")] public Vector3 direction;
    [FoldoutGroup("Settings")] public Vector3 offset;
    [PropertySpace(0, 5), FoldoutGroup("Settings")] public LayerMask ignoreLayers = ~0;

    [ShowIfGroup("Settings/CastsOnly", VisibleIf = "@castType == CastType.SphereCast || castType == CastType.SphereOverlap")]
    [FoldoutGroup("Settings")] public float width;

    // Parameters for Overlap cast types
    [ShowIfGroup("Settings/3DOnly", VisibleIf = "@castType == CastType.BoxCast && castType != CastType.BoxOverlap")]    [FoldoutGroup("Settings")]
    public Vector3 size;
    
    [ShowIfGroup("Settings/3DOnly")]
    public Vector3 rotation;

    [HideInInspector]
    public RaycastHit hit;

    [HideInInspector]
    public Collider[] overlapHits;

    [FoldoutGroup("Text")]
    [BoxGroup("Text/Label")] public bool drawLabel;
    [ShowInInspector, SerializeField] [BoxGroup("Text/Label")] LabelDrawingLocation labelTextLocation;
    [BoxGroup("Text/Label")] public float labelSize;
    [BoxGroup("Text/Label")] public Vector3 labelLocationOffset;
    [BoxGroup("Text/Label")] public Vector3 labelRotationOffset;

    [BoxGroup("Text/Hit")] public bool drawHitName;
    [ShowInInspector, SerializeField] [BoxGroup("Text/Hit")] LabelDrawingLocation hitTextLocation;
    [BoxGroup("Text/Hit")] public float hitTextSize;
    [BoxGroup("Text/Hit")] public Vector3 hitLocationOffset;
    [BoxGroup("Text/Hit")] public Vector3 hitRotationOffset;
    [FoldoutGroup("Text"), SerializeField, ShowInInspector] ObserverGizmoDrawingCondition gizmoDrawingCondition;
    
    [SerializeReference, PropertySpace(5, 5)]
    public List<EnvironmentObserver> children;

    // NOTE: I had a ref for a RaycastHit here that would correspond to hit but idk if it's needed.
    public bool Evaluate(GameObject source){
        if (active) {
            // Remove player's layer from LayerMask.
            ignoreLayers -= source.layer;
            
            // Set some of the variables used later during casting
            Vector3 relativeStart = source.transform.position + offset;
            Vector3 relativeStartWithRotation = source.transform.position + source.transform.rotation * offset ;

            switch (castType) {
                case CastType.Ray:
                    Physics.Raycast(relativeStart, source.transform.rotation * direction, out hit, length, ignoreLayers);
                    break;
                case CastType.BoxOverlap:
                    overlapHits = Physics.OverlapBox(relativeStartWithRotation, size / 2f,
                        source.transform.rotation * Quaternion.Euler(rotation), ignoreLayers);
                    
                    if (overlapHits.Length > 0) {
                        return true;
                    };
                    
                    break; 
                case CastType.SphereOverlap:
                    break;
                case CastType.BoxCast:
                    // TODO: Make this not an if statement. Check that it works with NodeCanvas first
                    if (Physics.BoxCast(relativeStartWithRotation, size / 2f,
                            source.transform.rotation * Quaternion.Euler(rotation) * direction,
                            out hit, source.transform.rotation * Quaternion.Euler(rotation), length,
                            ignoreLayers)
                       ) {
                    };
                    break;
                case CastType.SphereCast:
                    // TODO: Make this not an if statement. Check that it works with NodeCanvas first
                    if (Physics.SphereCast(relativeStartWithRotation, width / 2f,
                            source.transform.rotation * Quaternion.Euler(rotation) * direction,
                            out hit, length,
                            ignoreLayers)
                       ) {
                    };
                    break;
            }

            if (hit.transform != null) {
                return true;
            }
        }
        return false;
    }
    
    public void DrawObserverGizmo(GameObject source, bool drawAnyways = false){
        if (!drawAnyways){
            if (gizmoDrawingCondition == ObserverGizmoDrawingCondition.Never || (gizmoDrawingCondition == ObserverGizmoDrawingCondition.OnlyActive ! & active)) {
                return;
            }
        }
        
        Vector3 relativeStart = source.transform.position + offset;
        Vector3 relativeStartWithRotation = source.transform.position + source.transform.rotation * (offset);
        
        // Setup the variables for boxcast, spherecast, etc
        // Create an offset start point for the center of the wirebox, since gizmos are drawn with their pivot in the center.
        Vector3 offsetWithRotationAndLength;
        if (direction == Vector3.zero) {
            offsetWithRotationAndLength = Quaternion.Euler(rotation) * (Vector3.forward * (length / 2));
        } else {
            offsetWithRotationAndLength = Quaternion.LookRotation(direction) * Quaternion.Euler(rotation) * (Vector3.forward * (length / 2));
        }
        
        Vector3 offsetFromCenter = relativeStartWithRotation + source.transform.rotation * offsetWithRotationAndLength;

        // Also create a rotation for use with the gizmos. Mainly just to shorten the lines
        Quaternion gizmosRotation;
                                    
        if (direction == Vector3.zero) {
            gizmosRotation = source.transform.rotation *  Quaternion.Euler(rotation);
        } else {
            gizmosRotation = source.transform.rotation *  Quaternion.LookRotation(direction) * Quaternion.Euler(rotation);
        }
        Vector3 firstBoxOffset = gizmosRotation * (Vector3.forward * size.z);

        Color gizmoColor = Evaluate(source) ? Color.green : Color.red;
        gizmoColor = active ? gizmoColor : Color.gray;
        
        using (Draw.ingame.WithColor(gizmoColor)){
            switch (castType) {
                case CastType.Ray:
                    Draw.ingame.Line(relativeStart, relativeStart + (source.transform.rotation * direction.normalized) * length);
                    break;
                case CastType.BoxOverlap:
                    Draw.ingame.WireBox(relativeStartWithRotation, source.transform.rotation * Quaternion.Euler(rotation), size);
                    break;
                case CastType.SphereCast:
                    Draw.ingame.SolidCircle(relativeStartWithRotation, relativeStartWithRotation - Camera.main.transform.position, width * 1, gizmoColor.Alpha(.5f));
                    Draw.ingame.WireCapsule(relativeStartWithRotation, relativeStartWithRotation + gizmosRotation * (Vector3.forward * (length - width / 2)), width);
                    break;
                case CastType.BoxCast:
                    // Draw the gizmos for the boxcast
                    Draw.ingame.WireBox(offsetFromCenter, gizmosRotation, new float3(size.x, size.y, length));
                    Draw.ingame.SolidBox(relativeStartWithRotation, gizmosRotation, size, gizmoColor.Alpha(.1f));
                    
                    Draw.ingame.WireBox(relativeStartWithRotation, gizmosRotation, size);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
            
            Draw.ingame.SolidCircle(relativeStartWithRotation, relativeStartWithRotation - Camera.main.transform.position, .4f);
            Draw.ingame.SolidCircle(hit.point, hit.point - Camera.main.transform.position, .4f);

            // Set up variables for label (not hit name)
            Vector3 labelStartPos = Vector3.zero;
            switch (labelTextLocation) {
                case LabelDrawingLocation.PlayerOffset:
                    labelStartPos = source.transform.position;
                    break;
                case LabelDrawingLocation.IntersectingLength:
                    labelStartPos = offsetFromCenter;
                    break;
                case LabelDrawingLocation.HitLocation:{
                    if (hit.transform != null) {
                        labelStartPos = hit.point;
                    }

                    break;
                }
            }
            
            // Draw label
            if (drawLabel) {
                Draw.ingame.Label3D(
                    labelStartPos + labelLocationOffset,
                    gizmosRotation * Quaternion.Euler(labelRotationOffset),
                    label,
                    labelSize,
                    LabelAlignment.MiddleLeft,
                    gizmoColor
                );
            }
          
            // Set up variables for hit name
            // Since the label is already drawn just use the previous startPos
            switch (labelTextLocation) {
                case LabelDrawingLocation.PlayerOffset:
                    labelStartPos = source.transform.position;
                    break;
                case LabelDrawingLocation.IntersectingLength:
                    labelStartPos = offsetFromCenter;
                    break;
                case LabelDrawingLocation.HitLocation:{
                    if (hit.transform != null) {
                        labelStartPos = hit.point;
                    }

                    break;
                }
            }
            
            // Draw hitname
            if (drawLabel) {
                Draw.ingame.Label3D(
                    labelStartPos + labelLocationOffset,
                    gizmosRotation * Quaternion.Euler(labelRotationOffset),
                    label,
                    hitTextSize,
                    LabelAlignment.MiddleLeft,
                    gizmoColor
                );
            }
        }
    }
    
    static Color GetObserverStatusColorStatic(bool active, RaycastHit hit){
        if (active) {
            if (hit.Equals(default(RaycastHit))) {
                return Color.green;
            }
            
            return Color.red;
        }

        return Color.gray;
    }
}

public class PlayerEnvironmentManager : MonoBehaviour{
    [OdinSerialize]
    public List<EnvironmentObserver> observers;
    
    void Start(){
        CheckDuplicateLabels(observers);
    }

    // TODO: Not working.
    void CheckDuplicateLabels(List<EnvironmentObserver> sourceList){
        foreach (EnvironmentObserver sourceObserver in observers) {
            foreach (EnvironmentObserver observer in sourceList) {
                if (sourceObserver == observer) {
                    continue;
                }

                if (sourceObserver.label == observer.label) {
                    Debug.LogError($"Duplicate label found in observer: {observer.label} is in use multiple times");
                }

                if (observer.children != null && observer.children.Count > 0) {
                    CheckDuplicateLabels(observer.children);
                }
            }
        }
    }

    public bool EvaluateFromString(string searchLabel, List<EnvironmentObserver> observerList = null){
        List<EnvironmentObserver> listToUse = observers;
        
        if (observerList != null) {
            listToUse = observerList;
        } 
        
        foreach (EnvironmentObserver observer in listToUse) {
            if (observer.label == searchLabel) {
                return observer.Evaluate(gameObject);
            }

            if (observer.children != null && observer.children.Count > 0) {
                foreach (EnvironmentObserver childObserver in observer.children) {
                    EvaluateFromString(searchLabel, childObserver.children);
                }
            }
        }

        return false;
    }
    
    public EnvironmentObserver FindObserverFromString(string searchLabel, List<EnvironmentObserver> observerList = null){
        List<EnvironmentObserver> listToUse = observers;
        
        if (observerList != null) {
            listToUse = observerList;
        } 
        
        foreach (EnvironmentObserver observer in listToUse) {
            if (observer.label == searchLabel) {
                return observer;
            }

            if (observer.children != null && observer.children.Count > 0) {
                foreach (EnvironmentObserver childObserver in observer.children) {
                    FindObserverFromString(searchLabel, childObserver.children);
                }
            }
        }

        return null;
    }

    void Update(){
    }
    
    void LateUpdate(){
        // Draw Gizmos
        foreach (EnvironmentObserver observer in observers) {
            observer.DrawObserverGizmo(gameObject);
            
            if (observer.children != null && observer.children.Count > 0) {
                foreach (EnvironmentObserver childObserver in observer.children) {
                    childObserver.DrawObserverGizmo(gameObject);
                }
            }
        }
        
        // Clear hit
        foreach (EnvironmentObserver observer in observers) {
            observer.hit = default;
            
            if (observer.children != null && observer.children.Count > 0) {
                foreach (EnvironmentObserver childObserver in observer.children) {
                    childObserver.hit = default;
                }
            }
        }
    }
}
