using System.Globalization;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using MoreLinq;
using System.Linq;

public class WorldCursor : MonoBehaviour
{
    public RaycastHit[] Hits;
    public Fact[] HitFacts;

    public float IntersectionDistance = 0.1f;

    public string deactivateSnapKey;
    private Camera Cam;
    private int layerMask;

    public float MaxRange = 10f;
    public bool useCamCurser = false;

    private void Awake()
    {
        //Ignore player and TalkingZone
        this.layerMask = ~LayerMask.GetMask("Player", "TalkingZone");
    }

    void Start()
    {
        Cam = Camera.main;
        CultureInfo.CurrentCulture = new CultureInfo("en-US");
    }

    public void setLayerMask(int layerMask)
    {
        this.layerMask = layerMask;
    }

    // working currently to include multiple hits 
    // TODO 

    void Update()
    {
        UpdateMaxRange();

        Cam = Camera.main; //WARN: Should not called every Update; TODO: Cache in Start/Awake?
        Vector3 mousePos = UIconfig.InputManagerVersion switch
        {
            1 or 2 or 3 => Input.mousePosition,
            _ => Vector3.zero
        };

        Ray ray = useCamCurser
            ? new Ray(Cam.transform.position, Cam.transform.forward)
            : Cam.ScreenPointToRay(mousePos);

        transform.up = Cam.transform.forward;
        transform.position = ray.GetPoint(GlobalBehaviour.GadgetPhysicalDistance);

        //************************************************
        bool deactSnapKey =
               Input.GetButton(this.deactivateSnapKey)
            && UIconfig.InputManagerVersion >= 1
            && UIconfig.InputManagerVersion <= 3;
        //************************************************

        Hits = Physics.RaycastAll(ray, MaxRange, layerMask);
        if (Hits.Length == 0)
        {
            Hits = Physics.RaycastAll(transform.position, Vector3.down, GlobalBehaviour.GadgetPhysicalDistance, layerMask);
            if (Hits.Length == 0)
                return; // in case we dont hit anything, just return
        }
        Hits = Hits
            .OrderBy(h => h.distance)
            .ToArray();

        int i = 0;
        for (; i < Hits.Length; i++)
        { // find first acceptable hit

            Fact fact = Hits[i].transform.TryGetComponent(out FactObject factObj)
                     && FactOrganizer.AllFacts.ContainsKey(factObj.URI)
                ? FactOrganizer.AllFacts[factObj.URI]
                : null;

            if (fact is AbstractLineFact lineFact)
            {
                Hits[i].point =
                    Math3d.ProjectPointOnLine(lineFact.Point1.Point, lineFact.Dir, Hits[i].point);
            }
            else if (fact is CircleFact circleFact)
            {
                var projPlane =
                    Math3d.ProjectPointOnPlane(circleFact.normal, circleFact.Point1.Point, Hits[i].point);

                var circleDistance = projPlane - circleFact.Point1.Point;
                if (circleDistance.magnitude >= circleFact.radius)
                {
                    projPlane = circleFact.Point1.Point + circleDistance.normalized * circleFact.radius;
                    Hits[i].normal = circleDistance.normalized;
                }

                Hits[i].point = projPlane;
            }
            else if (!deactSnapKey &&
                  (Hits[i].collider.transform.CompareTag("SnapZone")
                || Hits[i].collider.transform.CompareTag("Selectable")))
            {
                Hits[i].point = Hits[i].collider.transform.position;
                Hits[i].normal = Vector3.up;
            }
            else
                continue;

            break; // we had at least 1 succesfull case
        }

        if (i == Hits.Length)
        {
            Hits = new[] { Hits[0] };
            HitFacts = new Fact[0];
        }
        else
        {
            Hits = Hits.Skip(i)
                       .TakeWhile(h => h.distance - Hits[i].distance < IntersectionDistance)
                       .ToArray();

            HitFacts = Hits
                .Select(h =>
                    h.transform.TryGetComponent(out FactObject factObj)
                    ? FactOrganizer.AllFacts[factObj.URI]
                    : null)
                .ToArray();
        }

        if (Hits.Length > 1)
        { // we have two or more objects intersecting
            if (HitFacts[0] is RayFact rayFact1
             && HitFacts[1] is RayFact rayFact2)
            {
                if (Math3d.LineLineIntersection(out Vector3 intersectionPoint
                    , rayFact2.Point1.Point, rayFact2.Dir
                    , rayFact1.Point1.Point, rayFact1.Dir))
                {
                    Hits[0].point = intersectionPoint;
                    Hits[1].point = intersectionPoint;
                }
            }

            //TODO: check for other types of intersection. Future Work
        }

        transform.up = Hits[0].normal;
        transform.position = Hits[0].point + .01f * Hits[0].normal;

        //for (i = 0; i < HitFacts.Length; i++) //TODO: not constantly => see ShinyThings.Update
        //    if (HitFacts[i]?.Id != null)
        //        CommunicationEvents.AnimateExistingFactEvent.Invoke(HitFacts[i].Id, FactWrapper.FactMaterials.Selected);

        if (Input.GetMouseButtonDown(0))
            ClickHandler();
    }

    private void UpdateMaxRange()
    {
        switch (UIconfig.GameplayMode)
        {
            case 2:
                UIconfig.interactingRangeMode = UIconfig.InteractingRangeMode.fromObserverView;
                break;

            case 5:
            case 6:
                UIconfig.interactingRangeMode = UIconfig.InteractingRangeMode.fromCharacterView;
                break;

            default:
                break;
        }
    }

    private void ClickHandler()
    {
        if (Hits[0].collider.transform.CompareTag("NPC1_text")
         && UIconfig.nextDialogPlease < 2)
            UIconfig.nextDialogPlease++;

        if (IsPointerOverUIObject() //Needed for Android. NOT JUST: EventSystem.current.IsPointerOverGameObject()
         || Hits[0].transform.gameObject.layer == LayerMask.NameToLayer("Water") // not allowed to meassure on water
         || Hits[0].collider.transform.CompareTag("NPC1_text"))                  // not allowed to meassure on textfields
            return;

        CommunicationEvents.TriggerEvent.Invoke(Hits);
        return;

        static bool IsPointerOverUIObject()
        {
            PointerEventData eventDataCurrentPosition = new(EventSystem.current)
            {
                position = new Vector2(Input.mousePosition.x, Input.mousePosition.y)
            };

            List<RaycastResult> results = new();
            EventSystem.current.RaycastAll(eventDataCurrentPosition, results);

            return results.Count > 0;
        }
    }
}