using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Prexonite;
using Prexonite.Types;

namespace Prx.Benchmarking;

public sealed class Benchmark : IObject
{
    #region Construction

    public Engine Machine { get; set; }

    public int Iterations { get; }

    public Benchmark(StackContext sctx, int iterations)
        : this(sctx.ParentEngine, iterations) { }

    public Benchmark(Engine eng, int iterations)
    {
        if (iterations < 0)
            throw new ArgumentOutOfRangeException(
                nameof(iterations),
                iterations,
                "iterations must be a positive integer."
            );
        Iterations = iterations;
        Machine = eng ?? throw new ArgumentNullException(nameof(eng));
    }

    public Benchmark(Engine eng)
        : this(eng, DefaultIterations) { }

    public Benchmark(StackContext sctx)
        : this(sctx.ParentEngine) { }

    #endregion

    #region Entries

    public const string BenchmarkKey = nameof(Benchmark);
    public const string TitleKey = "Title";
    public const string DescriptionKey = "Description";
    public const string UsesIterationKey = "Iteration";
    public const string OverheadKey = "Overhead";
    public const int DefaultIterations = 1000;
    public const int DefaultWarmUpIterations = 2;

    public BenchmarkEntryCollection Entries { get; } = new();

    [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Internal")]
    internal Stopwatch _Stopwatch { get; } = new();

    public void IncludeAll(Application application)
    {
        if (application == null)
            throw new ArgumentNullException(nameof(application));
        foreach (var function in application.Functions)
            if (function.Meta[BenchmarkKey])
                Include(function);
    }

    public void IncludeRange(params PFunction[] functions)
    {
        if (functions == null)
            throw new ArgumentNullException(nameof(functions));
        foreach (var function in functions)
            Include(function);
    }

    public void Include(PFunction function)
    {
        if (function == null)
            throw new ArgumentNullException(nameof(function));
        Entries.Add(new(this, function));
    }

    public List<Measurement> MeasureAll(bool verbose)
    {
        var lst = new List<Measurement>(Entries.Count);
        foreach (var entry in Entries)
            lst.Add(entry.Measure(verbose));
        return lst;
    }

    public void WarmUp()
    {
        foreach (var entry in Entries)
            entry.WarmUp();
    }

    #endregion

    #region IObject Members

    public bool TryDynamicCall(
        StackContext sctx,
        ReadOnlySpan<PValue> args,
        PCall call,
        string id,
        [NotNullWhen(true)] out PValue? result
    )
    {
        if (sctx == null)
            throw new ArgumentNullException(nameof(sctx));
        id ??= "";

        result = null;
        switch (id.ToLower(CultureInfo.InvariantCulture))
        {
            case "include":
                foreach (var arg in args)
                    Include(arg.ConvertTo<PFunction>(sctx));
                result = PType.Null.CreatePValue();
                break;
            case "includerange":
                foreach (var arg in args)
                {
                    if (arg.Type.ToBuiltIn() != PType.BuiltIn.List)
                        continue;
                    foreach (var func in (List<PValue>)arg.Value!)
                        Include(func.ConvertTo<PFunction>(sctx));
                }
                result = PType.Null.CreatePValue();
                break;
            case "includeall":
                if (!(args.Length > 0 && args[0].TryConvertTo(sctx, out Application? tapp)))
                    tapp = sctx.ParentApplication;
                IncludeAll(tapp);
                result = PType.Null.CreatePValue();
                break;
            case "measureall":
                if (!(args.Length > 0 && args[0].TryConvertTo(sctx, out bool verbose)))
                    verbose = true;
                var plst = MeasureAll(verbose).Select(sctx.CreateNativePValue).ToList();
                result = (PValue)plst;
                break;
            case "warmup":
                WarmUp();
                result = PType.Null.CreatePValue();
                break;
        }
        return result != null;
    }

    #endregion
}
