#region Namespace Imports

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Prexonite;
using Prexonite.Commands;
using Prexonite.Compiler;
using Prexonite.Compiler.Build;
using Prexonite.Properties;
using Prexonite.Types;
using Prx.Benchmarking;

#endregion

namespace Prx;

static class Program
{
    public const string PrxScriptFileName = "prx_main.pxs";

    public static string GetPrxPath()
    {
        return AppContext.BaseDirectory;
    }

    public static void Main(string[] args)
    {
        Console.CancelKeyPress += delegate
        {
            Environment.Exit(1);
        };
        var prexoniteConsole = new PrexoniteConsole(true);

        //Let the exceptions surface so they can more easily be debugged
        try
        {
            var engine = new Engine();
            engine.RegisterAssembly(Assembly.GetExecutingAssembly());

            //Load application
            var app = _loadApplication(engine, prexoniteConsole);

            //Run the applications main function.
            if (app != null) //errors have already been reported
                _runApplication(engine, app, args);
        }
        // ReSharper disable once RedundantCatchClause
        catch (Exception ex)
        {
#if DEBUG
            _dummyUsageOf(ex);
            throw;
#else
            Console.WriteLine(ex);
#endif
        }
        finally
        {
            if (Debugger.IsAttached)
            {
                Console.WriteLine(Resources.Program_DebugExit);
                Console.ReadLine();
            }
        }
    }

    // ReSharper disable once UnusedParameter.Local
    static void _dummyUsageOf(object any) { }

    static void _runApplication(Engine engine, Application app, IEnumerable<string> args)
    {
        app.Run(engine, args.Select(engine.CreateNativePValue).ToArray());
    }

    static Application? _loadApplication(Engine engine, PrexoniteConsole prexoniteConsole)
    {
        var plan = Plan.CreateSelfAssembling();
        var opts = new LoaderOptions(engine, null);
        if (plan.Options != null)
        {
            opts.InheritFrom(plan.Options);
        }
        plan.Options = opts;

        #region Stopwatch commands

        //prx.exe provides these three additional commands for high speed access to a stopwatch from your script code
        var timer = new Stopwatch();
        engine.Commands.AddHostCommand(
            @"timer\start",
            new DelegatePCommand(
                delegate
                {
                    timer.Start();
                    return PType.Null;
                }
            )
        );

        engine.Commands.AddHostCommand(
            @"timer\stop",
            new DelegatePCommand(
                delegate
                {
                    timer.Stop();
                    return (double)timer.ElapsedMilliseconds;
                }
            )
        );

        engine.Commands.AddHostCommand(
            @"timer\reset",
            new DelegatePCommand(
                delegate
                {
                    timer.Reset();
                    return PType.Null;
                }
            )
        );

        engine.Commands.AddHostCommand(
            @"timer\elapsed",
            new DelegatePCommand(
                delegate
                {
                    return (double)timer.ElapsedMilliseconds;
                }
            )
        );

        #endregion

        #region Stack Manipulation commands

        engine.Commands.AddHostCommand(
            @"__replace_call",
            delegate(StackContext sctx, PValue[] cargs)
            {
                cargs = (PValue[]?)cargs ?? [];
                if ((StackContext?)sctx == null)
                    throw new ArgumentNullException(nameof(sctx));

                var e = sctx.ParentEngine;

                if (cargs.Length < 1)
                    throw new PrexoniteException(
                        "__replace_call requires the context or function to be replaced."
                    );

                var carg = cargs[0];
                var rargs = new PValue[cargs.Length - 1];
                Array.Copy(cargs, 1, rargs, 0, rargs.Length);

                FunctionContext? rctx = null;
                PFunction? f;
                switch (carg.Type.ToBuiltIn())
                {
                    case PType.BuiltIn.String:
                        if (
                            !sctx.ParentApplication.Functions.TryGetValue(
                                (string)carg.Value!,
                                out f
                            )
                        )
                            throw new PrexoniteException(
                                "Cannot replace call to "
                                    + carg
                                    + " because no such function exists."
                            );

                        rctx = f.CreateFunctionContext(e, rargs);
                        break;
                    case PType.BuiltIn.Object:
                        var clrType = ((ObjectPType)carg.Type).ClrType;
                        if (clrType == typeof(PFunction))
                        {
                            f = (PFunction)carg.Value!;
                            rctx = f.CreateFunctionContext(e, rargs);
                        }
                        else if (clrType == typeof(Closure) && clrType != typeof(Continuation))
                        {
                            var c = (Closure)carg.Value!;
                            rctx = c.CreateFunctionContext(sctx, rargs);
                        }
                        else if (clrType == typeof(FunctionContext))
                        {
                            rctx = (FunctionContext)carg.Value!;
                        }
                        break;
                }
                if (rctx == null)
                    throw new PrexoniteException("Cannot replace a context based on " + carg);

                var node = e.Stack.Last;
                do
                {
                    if (node?.Value is FunctionContext ectx)
                    {
                        if (ReferenceEquals(ectx.Implementation, rctx.Implementation))
                        {
                            node.Value = rctx;
                            break;
                        }
                    }
                } while ((node = node?.Previous) != null);

                return PType.Null.CreatePValue();
            }
        );

        #endregion

        #region Prexonite Console constant

        engine.Commands.AddHostCommand("__console", prexoniteConsole);

        #endregion

        #region Create benchmark command

        engine.Commands.AddHostCommand(
            "createBenchmark",
            delegate(StackContext sctx, PValue[] cargs)
            {
                if ((StackContext?)sctx == null)
                    throw new ArgumentNullException(nameof(sctx));
                if ((PValue[]?)cargs == null)
                    cargs = [];

                Engine teng;
                int tit;

                if (cargs.Length >= 2)
                {
                    teng = cargs[0].ConvertTo<Engine>(sctx);
                    tit = cargs[1].ConvertTo<int>(sctx);
                }
                else if (cargs.Length >= 1)
                {
                    teng = sctx.ParentEngine;
                    tit = cargs[0].ConvertTo<int>(sctx);
                }
                else
                {
                    return sctx.CreateNativePValue(new Benchmark(sctx.ParentEngine));
                }

                return sctx.CreateNativePValue(new Benchmark(teng, tit));
            }
        );

        #endregion

        var prxPath = GetPrxPath();

        #region Self-assembling build plan reference

        engine.Commands.AddHostCommand(
            @"host\self_assembling_build_plan",
            (sctx, _) => sctx.CreateNativePValue(plan)
        );
        engine.Commands.AddHostCommand(
            @"host\prx_path",
            (sctx, _) => sctx.CreateNativePValue(prxPath)
        );

        #endregion

        (Application Application, ITarget Target)? result;
        try
        {
            var entryDesc = plan.AssembleAsync(
                Source.FromEmbeddedResource(
                    Assembly.GetAssembly(typeof(Program))!,
                    PrxScriptFileName
                )
            ).Result;
            result = plan.Load(entryDesc.Name);
        }
        catch (BuildFailureException e)
        {
            _reportErrors(e.Messages);
            Console.WriteLine(e);
#if DEBUG
            throw;
#else
            result = null;
#endif
        }
        catch (BuildException e)
        {
            if (e.RelatedTarget != null)
                _reportErrors(e.RelatedTarget.BuildMessages);

            Console.WriteLine(e);
#if DEBUG
            throw;
#else
            result = null;
#endif
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
#if DEBUG
            throw;
#else
            result = null;
#endif
        }

        // ReSharper disable once ConditionIsAlwaysTrueOrFalse
        if (((Application, ITarget)?)result == null)
            return null;
        else if (_reportErrors(result.Value.Target.Messages))
            return null;

        var app = result.Value.Application;
        app.Meta[nameof(Version)] = Assembly.GetExecutingAssembly().GetName().Version!.ToString();
        return app;
    }

    static bool _reportErrors(IEnumerable<Message> messages)
    {
        var originalColor = Console.ForegroundColor;
        var msgBySev = messages.ToGroupedDictionary<Message, MessageSeverity, List<Message>>(m =>
            m.Severity
        );
#if DEBUG
        _reportWarnings(msgBySev, originalColor);
#endif
        if (msgBySev.TryGetValue(MessageSeverity.Error, out var errors) && errors.Count > 0)
        {
            try
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.Error.WriteLine("Errors during compilation detected. Aborting.");
                foreach (var err in errors)
                    Console.WriteLine(err);
            }
            finally
            {
                Console.ForegroundColor = originalColor;
            }
            return true;
        }
        else
        {
            return false;
        }
    }

    static void _reportWarnings(
        IDictionary<MessageSeverity, List<Message>> messages,
        ConsoleColor originalColor
    )
    {
        try
        {
            Console.ForegroundColor = originalColor;
            if (messages.TryGetValue(MessageSeverity.Info, out var messageCategory))
                foreach (var message in messageCategory)
                    Console.WriteLine(message);

            Console.ForegroundColor = ConsoleColor.Yellow;
            if (messages.TryGetValue(MessageSeverity.Warning, out messageCategory))
                foreach (var warning in messages[MessageSeverity.Warning])
                    Console.WriteLine(warning);
        }
        finally
        {
            Console.ForegroundColor = originalColor;
        }
    }
}
