using Prexonite.Commands.Core;
using Prexonite.Commands.Core.PartialApplication;
using Prexonite.Modular;
using Prexonite.Properties;
using Debug = System.Diagnostics.Debug;

namespace Prexonite.Compiler.Ast;

/// <summary>
///     An AST node that represents a language construct that can be used in a partial application.
/// </summary>
public interface IAstPartiallyApplicable
{
    /// <summary>
    ///     Checks the nodes immediate child nodes for instances of <see cref = "AstPlaceholder" />. Implement this member publicly for interoperation with Prexonite compile-time macros.
    /// </summary>
    /// <para>For implementers: The <see cref="AstNode"/> class provides a default implementation that delegates
    /// to <see cref="CheckNodeApplicationState"/> (if this interface is implemented).
    /// This method mainly exists for backwards compatibility with script code.</para>
    /// <returns>True if this node has placeholders; false otherwise</returns>
    [Obsolete("Use CheckNodeApplicationState instead")]
    bool CheckForPlaceholders()
    {
        return CheckNodeApplicationState().HasPlaceholders;
    }

    /// <summary>
    ///     Checks the node's immediate child nodes for <see cref="AstPlaceholder"/>s and
    ///     <see cref="AstArgumentSplice"/>s.
    /// </summary>
    NodeApplicationState CheckNodeApplicationState();

    /// <summary>
    ///     <para>Emits code that performs the partial application.</para>
    ///     <para>Important: The code generator is free to call this method independently of the result of <see
    ///      cref = "CheckNodeApplicationState" />.</para>
    ///     <para>For internal use only. Implement this member explicitly.</para>
    /// </summary>
    /// <param name = "target">The compiler target to emit code to.</param>
    void DoEmitPartialApplicationCode(CompilerTarget target);
}

public struct NodeApplicationState : IEquatable<NodeApplicationState>
{
    public bool HasPlaceholders { get; }
    public bool HasArgumentSplices { get; }
    public static NodeApplicationState Closed = new(false, false);

    public NodeApplicationState WithPlaceholders(bool newHasPlaceholders) =>
        new(newHasPlaceholders, HasArgumentSplices);

    public NodeApplicationState WithArgumentSpliced(bool newHasArgumentSplices) =>
        new(HasPlaceholders, newHasArgumentSplices);

    public NodeApplicationState(bool hasPlaceholders, bool hasArgumentSplices)
    {
        HasPlaceholders = hasPlaceholders;
        HasArgumentSplices = hasArgumentSplices;
    }

    public bool Equals(NodeApplicationState other)
    {
        return HasPlaceholders == other.HasPlaceholders
            && HasArgumentSplices == other.HasArgumentSplices;
    }

    public override bool Equals(object? obj)
    {
        if (ReferenceEquals(null, obj))
            return false;
        return obj is NodeApplicationState && Equals((NodeApplicationState)obj);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            return (HasPlaceholders.GetHashCode() * 397) ^ HasArgumentSplices.GetHashCode();
        }
    }

    public static bool operator ==(NodeApplicationState left, NodeApplicationState right)
    {
        return left.Equals(right);
    }

    public static bool operator !=(NodeApplicationState left, NodeApplicationState right)
    {
        return !left.Equals(right);
    }
}

/// <summary>
///     Provides helper methods for the implementation of <see cref = "IAstPartiallyApplicable.DoEmitPartialApplicationCode" />.
/// </summary>
public static class AstPartiallyApplicable
{
    /// <summary>
    ///     <para>Takes the sequence of argument nodes, including any placeholders and emits byte code for the constructor call for the partial application.</para>
    ///     <para>The arguments must be passed in the order they would be supplied to the call target normally and include any non-arguments (like the object member call as the first "argument")</para>
    ///     <para>This method determines placeholder indices, i.e., it assigns absolute indices to placeholders that don't have an index assigend. This is a destructive update.</para>
    /// </summary>
    /// <typeparam name = "T">The kind of <see cref = "AstNode" /> this method operates on.</typeparam>
    /// <param name = "node">The <see cref = "AstNode" /> this method operates on.</param>
    /// <param name = "target">The compiler target to compile to.</param>
    /// <param name = "argv">Result of <see cref = "PreprocessPartialApplicationArguments" />.</param>
    public static int EmitConstructorArguments<T>(T node, CompilerTarget target, List<AstExpr> argv)
        where T : AstNode, IAstPartiallyApplicable
    {
        var mappings8 = new int[argv.Count];
        var closedArguments = new List<AstExpr>(argv.Count);
        GetMapping(argv, mappings8, closedArguments);

        var mappings32 = PartialApplicationCommandBase.PackMappings32(mappings8);

        //Emit arguments and mapping
        foreach (var arg in closedArguments)
            arg.EmitValueCode(target);

        foreach (var mapping in mappings32)
            target.EmitConstant(node.Position, mapping);

        return closedArguments.Count + mappings32.Length;
    }

    /// <summary>
    ///     Goes over <paramref name = "arguments" /> and determines the partial application argument mapping. Closed arguments are collected in <paramref
    ///      name = "closedArguments" />.
    /// </summary>
    /// <param name = "arguments">Result of <see cref = "PreprocessPartialApplicationArguments" />.</param>
    /// <param name = "mappings8">The list (or array) to store the mappings in. Must have space for at least <paramref
    ///      name = "arguments" />.Count elements.</param>
    /// <param name = "closedArguments">The list to store the closed arguments in. Must have space for at least <paramref
    ///      name = "arguments" />.Count elements.</param>
    /// <remarks>
    ///     <para>Does not alter <paramref name = "arguments" />.</para>
    ///     <para><paramref name = "mappings8" /> are not yet packed into 32bit integers.
    ///         Use <see cref = "PartialApplicationCommandBase.PackMappings32" /> to pack the
    ///         mappings into a more compact format.</para>
    /// </remarks>
    public static void GetMapping(
        IList<AstExpr> arguments,
        IList<int> mappings8,
        List<AstExpr> closedArguments
    )
    {
        for (var i = 0; i < arguments.Count; i++)
        {
            var arg = arguments[i];
            AstPlaceholder? placeholder;
            if ((placeholder = arg as AstPlaceholder) != null)
            {
                //Insert mapping to open argument
                Debug.Assert(placeholder.Index != null);
                mappings8[i] = -(placeholder.Index.Value + 1); //mappings are 1-based
            }
            else
            {
                //Insert mapping to closed argument
                closedArguments.Add(arg);
                mappings8[i] = closedArguments.Count; //mappings are 1-based; no need for -1
            }
        }
    }

    /// <summary>
    ///     <para>Performs preprocessing on the sequence of arguments for a partial application (including any non-arguments, like call targets).</para>
    ///     <para>You must use this method to prepare an argument list for <see cref = "EmitConstructorArguments{T}" />.</para>
    /// </summary>
    /// <param name = "arguments">Arguments for the partial call (including things like the call subject)</param>
    /// <returns>A preprocessed copy of the supplied arguments, ready for <see cref = "EmitConstructorArguments{T}" />.</returns>
    public static List<AstExpr> PreprocessPartialApplicationArguments(
        IEnumerable<AstExpr> arguments
    )
    {
        var processedArgv = arguments.ToList();
        var placeholders = processedArgv.MapMaybe(n => n as AstPlaceholder).ToList();
        AstPlaceholder.DeterminePlaceholderIndices(placeholders);
        _removeRedundantPlaceholders(processedArgv, placeholders);
        return processedArgv;
    }

    /// <summary>
    ///     <para>Removes placeholders that are redundant in the presence of excess arguments mapping from the supplied list.</para>
    ///     <para>It is not usually necessary to call this method separately. It is called as part of <see
    ///      cref = "PreprocessPartialApplicationArguments" />. Use that method instead.</para>
    /// </summary>
    /// <param name = "argv"></param>
    public static void RemoveRedundantPlaceholders(List<AstExpr> argv)
    {
        _removeRedundantPlaceholders(argv, argv.MapMaybe(n => n as AstPlaceholder));
    }

    [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
    static void _removeRedundantPlaceholders(
        List<AstExpr> argv,
        IEnumerable<AstPlaceholder> placeholders
    )
    {
        //Placeholders are redundant iff they map an open argument that would be supplied
        //  by the excess arguments mechanism anyway.
        //
        //      println(x,?0)
        //  Here the mapping of the first argument is redundant because the first excess
        //  argument would be mapped to that same position anyway.
        //
        //      println(?1,x,?3,?0,?2)
        //  Here only the last ?2 is redundant.
        //
        //      println(?3,?0,?1,?2,?3)
        //  No redundant placeholders here, because ?3 is mapped before, therefore
        //  it wouldn't be supplied as an excess argument.
        //
        //      println(?3,?0,?1,?2)
        //  When you remove the ?3 at the end, you can reduce the mapping to
        //  println(?3), since all other open arguments are in their "natural" position

        var maxIndex = placeholders.Max(p => p.Index ?? 0);
        var numUsages = new int[maxIndex + 1];
        foreach (var placeholder in placeholders)
        {
            Debug.Assert(placeholder.Index != null);
            numUsages[placeholder.Index.Value]++;
        }

        //If there are open argument indices that weren't mapped, we are dealing with
        //  a deliberate use of excess arguments already and are therefor
        //  not dealing with any redundancy at all
        if (numUsages.Contains(0))
            return;

        //Redundant placeholders only occur in a placeholder-only postfix with strictly
        //  ascending indices, that have not been mapped before. Find that postfix
        var lastIndex = int.MaxValue;
        int postfixIndex;
        for (postfixIndex = argv.Count - 1; postfixIndex >= 0; postfixIndex--)
        {
            var arg = argv[postfixIndex] as AstPlaceholder;
            if (arg == null || lastIndex <= arg.Index)
                break;
            Debug.Assert(arg.Index != null);
            if (numUsages[arg.Index.Value] > 1)
                break;
            lastIndex = arg.Index.Value;
        }
        postfixIndex++; //points to the first argument of the postfix
        if (argv.Count <= postfixIndex)
            return; //there is no qualifying postfix

        //We need to know which arguments are mapped by the rest of argv
        //Simulate open arguments mapping for the rest of the mapping
        for (var i = 0; i < postfixIndex; i++)
        {
            var placeholder = argv[i] as AstPlaceholder;
            if (placeholder == null)
                continue;

            Debug.Assert(placeholder.Index.HasValue);
            numUsages[placeholder.Index.Value]--;
        }

        //Correlate potential excess arguments with placeholders from the end
        int redundantIndex;
        var excessIndex = numUsages.Length - 1;
        for (redundantIndex = argv.Count - 1; postfixIndex <= redundantIndex; redundantIndex--)
        {
            var placeholder = (AstPlaceholder)argv[redundantIndex];
            Debug.Assert(placeholder.Index != null);

            //find next potential excess argument
            for (; 0 <= excessIndex; excessIndex--)
                if (numUsages[excessIndex] == 1)
                    break;

            if (placeholder.Index != excessIndex--)
                break;
        }
        redundantIndex++;

        //Remove redundant placeholders
        argv.RemoveRange(redundantIndex, argv.Count - redundantIndex);
    }

    public static bool IsPlaceholder(AstExpr expression) =>
        expression is AstPlaceholder
        || expression is AstArgumentSplice { IsPlaceholderSplice: true };

    public static bool IsArgumentSplice(AstExpr expression) => expression is AstArgumentSplice;
}

public static class AstPartiallyApplicableExtensions
{
    extension(AstExpr expr)
    {
        public AstExpr ConstFunc()
        {
            var constCmd = new AstIndirectCall(
                expr.Position,
                PCall.Get,
                new AstReference(expr.Position, EntityRef.Command.Create(Const.Alias))
            );
            constCmd.Arguments.Add(expr);
            return constCmd;
        }

        public AstExpr ConstFunc(object? constantValue)
        {
            return expr.Position.ConstFunc(constantValue);
        }

        public bool IsPlaceholder() =>
            expr is AstPlaceholder || expr is AstArgumentSplice { IsPlaceholderSplice: true };

        public bool IsArgumentSplice() => expr is AstArgumentSplice;
    }

    extension(ISourcePosition position)
    {
        public AstExpr ConstFunc(object? constantValue)
        {
            if (constantValue != null)
                return new AstConstant(
                    position.File,
                    position.Line,
                    position.Column,
                    constantValue
                ).ConstFunc();
            else
                return new AstNull(position.File, position.Line, position.Column).ConstFunc();
        }

        public AstExpr IdFunc()
        {
            return new AstPlaceholder(position.File, position.Line, position.Column, 0).IdFunc();
        }
    }

    extension(AstPlaceholder placeholder)
    {
        public AstExpr IdFunc()
        {
            if (!placeholder.Index.HasValue)
                throw new ArgumentException(
                    Resources.AstPartiallyApplicable_IdFunc_Placeholder_must_have_its_index_assigned_,
                    nameof(placeholder)
                );

            var call = new AstIndirectCall(
                placeholder.Position,
                PCall.Get,
                new AstReference(placeholder.Position, EntityRef.Command.Create(Id.Alias))
            );
            call.Arguments.Add(placeholder.GetCopy());
            return call;
        }
    }
}
