using Newtonsoft.Json;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using static SOMDocManager;

/// <summary>
/// Base-class for 1D-Facts
/// </summary>
public abstract class AbstractLineFact : FactWrappedCRTP<AbstractLineFact>
{
    /// @{ <summary>
    /// One <see cref="Fact.Id">Id</see> of two <see cref="PointFact"/> defining <see cref="Dir"/>.
    /// </summary>
    public string Pid1, Pid2;
    /// @}

    [JsonIgnore]
    public PointFact Point1 { get => (PointFact)_Facts[Pid1]; }
    [JsonIgnore]
    public PointFact Point2 { get => (PointFact)_Facts[Pid2]; }

    /// <summary>
    /// Normalized Direction from <see cref="Pid1"/> to <see cref="Pid2"/>.
    /// </summary>
    public Vector3 Dir;

    /// <summary>
    /// \copydoc Fact.Fact()
    /// </summary>
    protected AbstractLineFact() : base()
    {
        Pid1 = null;
        Pid2 = null;
        Dir = Vector3.zero;
    }

    /// <summary>
    /// Copies <paramref name="fact"/> by initiating new MMT %Fact.
    /// </summary>
    /// <param name="fact">Fact to be copied</param>
    /// <param name="old_to_new"><c>Dictionary</c> mapping <paramref name="fact"/>.<see cref="getDependentFactIds"/> in <paramref name="fact"/>.<see cref="Fact._Facts"/> to corresponding <see cref="Fact.Id"/> in <paramref name="organizer"/> </param>
    /// <param name="organizer">sets <see cref="_Facts"/></param>
    protected AbstractLineFact(AbstractLineFact fact, Dictionary<string, string> old_to_new, FactOrganizer organizer) : base(fact, organizer)
    {
        set_public_members(old_to_new[fact.Pid1], old_to_new[fact.Pid2]);
    }

    /// <summary>
    /// Standard Constructor
    /// </summary>
    /// <param name="pid1">sets <see cref="AbstractLineFact.Pid1"/></param>
    /// <param name="pid2">sets <see cref="AbstractLineFact.Pid2"/></param>
    /// <param name="organizer">sets <see cref="Fact._Facts"/></param>
    protected AbstractLineFact(string pid1, string pid2, FactOrganizer organizer) : base(organizer)
    {
        set_public_members(pid1, pid2);
    }

    /// <summary>
    /// Bypasses initialization of new MMT %Fact by using existend URI, _which is not checked for existence_.
    /// </summary>
    /// <param name="pid1">sets <see cref="Pid1"/></param>
    /// <param name="pid2">sets <see cref="Pid2"/></param>
    /// <param name="backendURI">MMT URI</param>
    /// <param name="organizer">sets <see cref="Fact._Facts"/></param>
    protected AbstractLineFact(string pid1, string pid2, string backendURI, FactOrganizer organizer) : base(organizer)
    {
        set_public_members(pid1, pid2);
        this._URI = backendURI;
    }

    /// <summary>
    /// Initiates <see cref="Pid1"/>, <see cref="Pid2"/>, <see cref="Dir"/>
    /// </summary>
    /// <param name="pid1">sets <see cref="Pid1"/></param>
    /// <param name="pid2">sets <see cref="Pid2"/></param>
    private void set_public_members(string pid1, string pid2)
    {
        this.Pid1 = pid1;
        this.Pid2 = pid2;
        PointFact pf1 = _Facts[pid1] as PointFact;
        PointFact pf2 = _Facts[pid2] as PointFact;
        this.Dir = (pf2.Point - pf1.Point).normalized;
    }

    /// \copydoc Fact.hasDependentFacts
    public override bool hasDependentFacts()
    {
        return true;
    }

    /// \copydoc Fact.getDependentFactIds
    public override string[] getDependentFactIds()
    {
        return new string[] { Pid1, Pid2 };
    }

    /// \copydoc Fact.GetHashCode
    public override int GetHashCode()
    {
        return this.Pid1.GetHashCode() ^ this.Pid2.GetHashCode();
    }
}

/// <summary>
/// Implements CRTP for <see cref="AbstractLineFact"/>; Escalates constructors;
/// </summary>
/// <typeparam name="T">class, which inherits from AbstractLineFactWrappedCRTP</typeparam>
public abstract class AbstractLineFactWrappedCRTP<T> : AbstractLineFact where T : AbstractLineFactWrappedCRTP<T>
{
    /// <summary>\copydoc Fact.Fact</summary>
    protected AbstractLineFactWrappedCRTP() : base() { }

    /// <summary>\copydoc AbstractLineFact.AbstractLineFact(AbstractLineFact, Dictionary{string, string}, FactOrganizer)</summary>
    protected AbstractLineFactWrappedCRTP(AbstractLineFactWrappedCRTP<T> fact, Dictionary<string, string> old_to_new, FactOrganizer organizer) : base(fact, old_to_new, organizer) { }

    /// <summary>\copydoc AbstractLineFact.AbstractLineFact(string, string, FactOrganizer)</summary>
    protected AbstractLineFactWrappedCRTP(string pid1, string pid2, FactOrganizer organizer) : base(pid1, pid2, organizer) { }

    /// <summary>\copydoc AbstractLineFact.AbstractLineFact(string, string, string, FactOrganizer)</summary>
    protected AbstractLineFactWrappedCRTP(string pid1, string pid2, string backendURI, FactOrganizer organizer) : base(pid1, pid2, backendURI, organizer) { }

    /// \copydoc Fact.Equivalent(Fact, Fact)
    protected override bool EquivalentWrapped(AbstractLineFact f1, AbstractLineFact f2)
        => EquivalentWrapped((T)f1, (T)f2);

    /// <summary>CRTP step of <see cref="EquivalentWrapped(AbstractLineFact, AbstractLineFact)"/></summary>
    protected abstract bool EquivalentWrapped(T f1, T f2);
}

/// <summary>
/// Line within 3D Space of finite length
/// </summary>
public class LineFact : AbstractLineFactWrappedCRTP<LineFact>
{
    /// \copydoc Fact.s_type
    [JsonProperty]
    protected static new string s_type = "LineFact";

    /// <summary> Distance between <see cref="AbstractLineFact.Pid1"/> and <see cref="AbstractLineFact.Pid2"/></summary>
    public float Distance;

    /// <summary> \copydoc Fact.Fact </summary>
    public LineFact() : base()
    {
        Distance = 0;
    }

    /// <summary> \copydoc AbstractLineFact.AbstractLineFact(AbstractLineFact, Dictionary<string, string>, FactOrganizer) </summary>
    public LineFact(LineFact fact, Dictionary<string, string> old_to_new, FactOrganizer organizer) : base(fact, old_to_new, organizer)
        => init(old_to_new[fact.Pid1], old_to_new[fact.Pid2]);

    /// <summary> \copydoc AbstractLineFact.AbstractLineFact(string, string, string, FactOrganizer) </summary>
    public LineFact(string pid1, string pid2, string backendURI, FactOrganizer organizer) : base(pid1, pid2, backendURI, organizer)
    {
        SetDistance();
        _ = this.Label;
    }

    /// <summary> \copydoc AbstractLineFact.AbstractLineFact(string, string, FactOrganizer) </summary>
    public LineFact(string pid1, string pid2, FactOrganizer organizer) : base(pid1, pid2, organizer)
        => init(pid1, pid2);

    /// <summary>
    /// Initiates <see cref="AbstractLineFact.Pid1"/>, <see cref="AbstractLineFact.Pid2"/>, <see cref="Fact._URI"/> and creates MMT %Fact Server-Side
    /// </summary>
    /// <param name="pid1">sets <see cref="AbstractLineFact.Pid1"/></param>
    /// <param name="pid2">sets <see cref="AbstractLineFact.Pid2"/></param>
    private void init(string pid1, string pid2)
    {
        SetDistance();

        PointFact pf1 = _Facts[pid1] as PointFact;
        PointFact pf2 = _Facts[pid2] as PointFact;
        float v = (pf1.Point - pf2.Point).magnitude;

        SOMDoc lhs =
            new OMA(
                new OMS(MMT_OMS_URI.Metric),
                new List<SOMDoc> {
                    new OMS(pid1),
                    new OMS(pid2)
                }
            );

        SOMDoc valueTp = new OMS(MMT_OMS_URI.RealLit);
        SOMDoc value = new OMF(v);

        MMTValueDeclaration mmtDecl = new(this.Label, lhs, valueTp, value);
        AddFactResponse.sendAdd(mmtDecl, out this._URI);
    }

    /// \copydoc Fact.parseFact(Scroll.ScrollFact)
    public new static LineFact parseFact(Scroll.ScrollFact fact)
    {
        string uri = fact.@ref.uri;
        string pointAUri = ((OMS)((OMA)((Scroll.ScrollValueFact)fact).lhs).arguments[0]).uri;
        string pointBUri = ((OMS)((OMA)((Scroll.ScrollValueFact)fact).lhs).arguments[1]).uri;

        if (StageStatic.stage.factState.ContainsKey(pointAUri)
         && StageStatic.stage.factState.ContainsKey(pointBUri))
            return new LineFact(pointAUri, pointBUri, uri, StageStatic.stage.factState);

        //If dependent facts do not exist return null
        else return null;
    }

    /// \copydoc Fact.generateLabel
    protected override string generateLabel()
        => "[" + _Facts[Pid1].Label + _Facts[Pid2].Label + "]";

    /// \copydoc Fact.instantiateDisplay(GameObject, Transform)
    public override GameObject instantiateDisplay(GameObject prefab, Transform transform)
    {
        var obj = GameObject.Instantiate(prefab, Vector3.zero, Quaternion.identity, transform);
        obj.transform.GetChild(0).gameObject.GetComponent<TextMeshProUGUI>().text = _Facts[this.Pid1].Label;
        obj.transform.GetChild(1).gameObject.GetComponent<TextMeshProUGUI>().text = _Facts[this.Pid2].Label;
        obj.GetComponent<FactWrapper>().fact = this;
        return obj;
    }

    /// \copydoc Fact.Equivalent(Fact, Fact)
    protected override bool EquivalentWrapped(LineFact f1, LineFact f2)
    {
        if ((f1.Pid1 == f2.Pid1 && f1.Pid2 == f2.Pid2))// || 
            //(f1.Pid1 == f2.Pid2 && f1.Pid2 == f2.Pid1))
            return true;

        PointFact p1f1 = (PointFact)_Facts[f1.Pid1];
        PointFact p2f1 = (PointFact)_Facts[f1.Pid2];
        PointFact p1f2 = (PointFact)_Facts[f2.Pid1];
        PointFact p2f2 = (PointFact)_Facts[f2.Pid2];

        return (p1f1.Equivalent(p1f2) && p2f1.Equivalent(p2f2))
            ;//|| (p1f1.Equivalent(p2f2) && p2f1.Equivalent(p1f2));
    }

    /// <summary> Calculates and sets <see cref="Distance"/>; <remarks> <see cref="AbstractLineFact.Pid1"/> and <see cref="AbstractLineFact.Pid2"/> needs to be set first.</remarks></summary>
    private void SetDistance()
        => this.Distance = Vector3.Distance(((PointFact)_Facts[Pid1]).Point, ((PointFact)_Facts[Pid2]).Point);
}

/// <summary>
/// Ray within 3D Space of infinite length
/// </summary>
public class RayFact : AbstractLineFactWrappedCRTP<RayFact>
{
    /// \copydoc Fact.s_type
    [JsonProperty]
    protected static new string s_type = "RayFact";

    /// <summary> \copydoc Fact.Fact </summary>
    public RayFact() : base() { }

    /// <summary> \copydoc AbstractLineFact.AbstractLineFact(AbstractLineFact, Dictionary<string, string>, FactOrganizer) </summary>
    public RayFact(RayFact fact, Dictionary<string, string> old_to_new, FactOrganizer organizer) : base(fact, old_to_new, organizer)
        => init(old_to_new[fact.Pid1], old_to_new[fact.Pid2]);

    /// <summary> \copydoc AbstractLineFact.AbstractLineFact(string, string, string, FactOrganizer) </summary>
    public RayFact(string pid1, string pid2, string backendURI, FactOrganizer organizer) : base(pid1, pid2, backendURI, organizer)
        => _ = this.Label;

    /// <summary> \copydoc AbstractLineFact.AbstractLineFact(string, string, FactOrganizer) </summary>
    public RayFact(string pid1, string pid2, FactOrganizer organizer) : base(pid1, pid2, organizer)
        => init(pid1, pid2);

    /// <summary>
    /// Initiates <see cref="AbstractLineFact.Pid1"/>, <see cref="AbstractLineFact.Pid2"/>, <see cref="Fact._URI"/> and creates MMT %Fact Server-Side
    /// </summary>
    /// <param name="pid1">sets <see cref="AbstractLineFact.Pid1"/></param>
    /// <param name="pid2">sets <see cref="AbstractLineFact.Pid2"/></param>
    private void init(string pid1, string pid2)
    {
        SOMDoc tp = new OMS(MMT_OMS_URI.LineType);
        SOMDoc df = new OMA(
            new OMS(MMT_OMS_URI.LineOf),
            new List<SOMDoc> {
                new OMS(pid1),
                new OMS(pid2)
            });

        AddFactResponse.sendAdd(new MMTSymbolDeclaration(this.Label, tp, df), out this._URI);

        ParsingDictionary.parseTermsToId[df.ToString()] = this._URI;
    }

    /// \copydoc Fact.parseFact(Scroll.ScrollFact)
    public new static RayFact parseFact(Scroll.ScrollFact fact)
    {
        string uri = fact.@ref.uri;

        if ((OMA)((Scroll.ScrollSymbolFact)fact).df == null)
            return null;

        string pointAUri = ((OMS)((OMA)((Scroll.ScrollSymbolFact)fact).df).arguments[0]).uri;
        string pointBUri = ((OMS)((OMA)((Scroll.ScrollSymbolFact)fact).df).arguments[1]).uri;

        if (StageStatic.stage.factState.ContainsKey(pointAUri)
             && StageStatic.stage.factState.ContainsKey(pointBUri))
            return new RayFact(pointAUri, pointBUri, uri, StageStatic.stage.factState);

        //If dependent facts do not exist return null
        else return null;
    }

    /// \copydoc Fact.generateLabel
    protected override string generateLabel()
    {
        // TODO this string is too large to properly depict on scrolls. 
        // return "]" + _Facts[Pid1].Label + _Facts[Pid2].Label + "[";
        return _Facts[Pid1].Label + _Facts[Pid2].Label;
    }

    /// \copydoc Fact.instantiateDisplay(GameObject, Transform)
    public override GameObject instantiateDisplay(GameObject prefab, Transform transform)
    {
        var obj = GameObject.Instantiate(prefab, Vector3.zero, Quaternion.identity, transform);
        obj.transform.GetChild(0).gameObject.GetComponent<TextMeshProUGUI>().text = this.Label;
        obj.GetComponent<FactWrapper>().fact = this;
        return obj;
    }

    /// \copydoc Fact.Equivalent(Fact, Fact)
    protected override bool EquivalentWrapped(RayFact f1, RayFact f2)
    {
        if (!Math3d.IsApproximatelyParallel(f1.Dir, f2.Dir))
            return false;

        PointFact p1f1 = (PointFact)_Facts[f1.Pid1];
        PointFact p1f2 = (PointFact)_Facts[f2.Pid1];
        PointFact p2f2 = (PointFact)_Facts[f2.Pid2];

        return Math3d.IsPointApproximatelyOnLine(p1f1.Point, f1.Dir, p1f2.Point)
            && Math3d.IsPointApproximatelyOnLine(p1f1.Point, f1.Dir, p2f2.Point);
    }
}