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)FactOrganizer.AllFacts[Pid1]; }
    [JsonIgnore]
    public PointFact Point2 { get => (PointFact)FactOrganizer.AllFacts[Pid2]; }

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

    /// <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>
    /// 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)
    {
        this._URI = backendURI;
        set_public_members(pid1, pid2);
    }

    /// <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;
        Vector3 diff = (Point1.Point - Point2.Point);
        this.Dir = diff.normalized;
        this.Distance = diff.magnitude;
    }

    protected override void RecalculateTransform()
    {
        Position = Vector3.Lerp(Point1.Point, Point2.Point, 0.5f);
        Rotation = Quaternion.LookRotation(Dir, Vector3.up);
        LocalScale = new Vector3(1, 1, Distance);
    }

    /// \copydoc Fact.hasDependentFacts
    public override bool HasDependentFacts => true;

    /// \copydoc Fact.getDependentFactIds
    protected override string[] GetGetDependentFactIds() 
        => new string[] { Pid1, Pid2 };
}

/// <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(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> \copydoc Fact.Fact </summary>
    public LineFact() : base() { }

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

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

    public override MMTDeclaration MakeMMTDeclaration()
    {
        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(Distance);

        return new MMTValueDeclaration(this.Label, lhs, valueTp, value);
    }

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

        if (!FactOrganizer.AllFacts.ContainsKey(pointAUri)
         || !FactOrganizer.AllFacts.ContainsKey(pointBUri))
            return null;

        return new LineFact(pointAUri, pointBUri, fact.@ref.uri, StageStatic.stage.factState);
    }

    /// \copydoc Fact.generateLabel
    protected override string generateLabel()
        => "[" + Point1.Label + Point2.Label + "]";

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

    protected override Fact _ReInitializeMe(Dictionary<string, string> old_to_new, FactOrganizer organizer)
        => new LineFact(old_to_new[this.Pid1], old_to_new[this.Pid2], organizer);
}

/// <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(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)
    {
        SendToMMT();
    }

    public override MMTDeclaration MakeMMTDeclaration()
    {
        SOMDoc type = new OMS(MMT_OMS_URI.LineType);
        SOMDoc defines = 
            new OMA(
                new OMS(MMT_OMS_URI.LineOf),
                new List<SOMDoc> {
                    new OMS(Pid1),
                    new OMS(Pid2)
            });

        return new MMTSymbolDeclaration(this.Label, type, defines);
    }

    /// \copydoc Fact.parseFact(ScrollFact)
    public new static RayFact parseFact(MMTDeclaration fact)
    {
        if (((MMTSymbolDeclaration)fact).defines is not OMA defines)
            return null;

        string pointAUri = ((OMS)defines.arguments[0]).uri;
        string pointBUri = ((OMS)defines.arguments[1]).uri;

        if (!FactOrganizer.AllFacts.ContainsKey(pointAUri)
         || !FactOrganizer.AllFacts.ContainsKey(pointBUri))
            return null;

        return new RayFact(pointAUri, pointBUri, fact.@ref.uri, StageStatic.stage.factState);
    }

    protected override void RecalculateTransform()
    {
        base.RecalculateTransform();
        LocalScale = new Vector3(1, 1, 2048);
    }

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

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

        return Math3d.IsPointApproximatelyOnLine(f1.Point1.Point, f1.Dir, f2.Point1.Point)
            && Math3d.IsPointApproximatelyOnLine(f1.Point1.Point, f1.Dir, f2.Point2.Point);
    }

    protected override Fact _ReInitializeMe(Dictionary<string, string> old_to_new, FactOrganizer organizer)
        => new RayFact(old_to_new[this.Pid1], old_to_new[this.Pid2], organizer);
}