using AnotherReplayReader.ReplayFile;
using OxyPlot;
using OxyPlot.Axes;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

namespace AnotherReplayReader.Apm
{
    public class ApmPlotterFilterOptions
    {
        public bool CountUnknowns { get; }
        public bool CountAutos { get; }
        public bool CountClicks { get; }

        public ApmPlotterFilterOptions(bool countUnknowns, bool countAutos, bool countClicks)
        {
            CountUnknowns = countUnknowns;
            CountAutos = countAutos;
            CountClicks = countClicks;
        }

        public bool ShouldSkip(byte command)
        {
            if (!CountUnknowns && ApmPlotter.IsUnknown(command))
            {
                return true;
            }
            if (!CountAutos && ApmPlotter.IsAuto(command))
            {
                return true;
            }
            if (!CountClicks && ApmPlotter.IsClick(command))
            {
                return true;
            }
            return false;
        }
    }

    internal class ApmPlotter
    {
        private static readonly ConcurrentDictionary<byte, byte> UnknownCommands;
        private static readonly ConcurrentDictionary<byte, byte> AutoCommands;
        private readonly ImmutableArray<(TimeSpan, ImmutableArray<CommandChunk>)> _commands;

        public Replay Replay { get; }
        public ImmutableArray<Player> Players { get; }
        public ImmutableArray<TimeSpan> PlayerLifes { get; }
        public ImmutableArray<TimeSpan> StricterPlayerLifes { get; }
        public TimeSpan ReplayLength { get; }

        static ApmPlotter()
        {
            static KeyValuePair<byte, byte> Create(byte x) => new(x, default);
            UnknownCommands = new(RA3Commands.UnknownCommands.Select(Create));
            AutoCommands = new(RA3Commands.AutoCommands.Select(Create));
        }

        public ApmPlotter(Replay replay)
        {
            if (replay.Body is not { } body)
            {
                throw new InvalidOperationException("Replay must be fully parsed!");
            }

            Replay = replay;

            // player list without post Commentator
            Players = (replay.Players.Last().PlayerName == Replay.PostCommentator
                ? replay.Players.Take(replay.Players.Length - 1)
                : replay.Players).ToImmutableArray();

            // get all commands
            IEnumerable<CommandChunk> Filter(ReplayChunk chunk)
            {
                foreach (var command in CommandChunk.Parse(chunk))
                {
                    if (command.PlayerIndex >= Players.Length)
                    {
                        Debug.Instance.DebugMessage += $"Unknown command with invalid player index: {command}\r\n";
                        continue;
                    }
                    yield return command;
                }
            }
            var query = from chunk in body
                        where chunk.Type is 1
                        select (chunk.Time, Filter(chunk).ToImmutableArray());
            _commands = query.ToImmutableArray();

            var playerLifes = new TimeSpan[Players.Length];
            var threeSeconds = TimeSpan.FromSeconds(3);
            var stricterLifes = new TimeSpan[Players.Length];
            foreach (var (time, commands) in _commands)
            {
                var estimatedTime = time + threeSeconds;
                foreach (var command in commands)
                {
                    var commandId = command.CommandId;
                    var playerIndex = command.PlayerIndex;
                    if (commandId == 0x21)
                    {
                        if (playerLifes[playerIndex] < estimatedTime)
                        {
                            playerLifes[playerIndex] = estimatedTime;
                        }
                    }
                    else if (!IsUnknown(commandId) && !IsAuto(commandId) && playerIndex >= 0)
                    {
                        if (stricterLifes[playerIndex] < estimatedTime)
                        {
                            stricterLifes[playerIndex] = estimatedTime;
                        }
                    }
                }
            }
            PlayerLifes = playerLifes.ToImmutableArray();
            StricterPlayerLifes = stricterLifes.ToImmutableArray();
            ReplayLength = replay.Footer?.ReplayLength ?? PlayerLifes.Max();
        }

        public DataPoint[][] GetPoints(TimeSpan resolution, ApmPlotterFilterOptions options)
        {
            var lists = Players
                .Select(x => new List<int> { 0 })
                .ToArray();
            var currentTime = TimeSpan.Zero;
            var currentIndex = 0;
            foreach (var (time, commands) in _commands)
            {
                while (time >= currentTime + resolution)
                {
                    currentTime += resolution;
                    ++currentIndex;
                }
                foreach (var command in commands)
                {
                    if (options.ShouldSkip(command.CommandId) || command.PlayerIndex < 0)
                    {
                        continue;
                    }
                    var list = lists[command.PlayerIndex];
                    var gap = currentIndex - list.Count;
                    if (gap >= 0)
                    {
                        list.AddRange(Enumerable.Repeat(0, gap + 1));
                    }
                    ++lists[command.PlayerIndex][currentIndex];
                }
            }
            var apmMultiplier = 1 / resolution.TotalMinutes;
            DataPoint Create(int v, int i) => new(TimeSpanAxis.ToDouble(resolution) * i,
                                                  v * apmMultiplier);
            return lists.Select(l => l.Select(Create).ToArray()).ToArray();
        }

        public double[] CalculateAverageApm(ApmPlotterFilterOptions options)
        {
            var apm = new double[Players.Length];
            foreach (var (_, commands) in _commands)
            {
                foreach (var command in commands)
                {
                    if (options.ShouldSkip(command.CommandId) || command.PlayerIndex < 0)
                    {
                        continue;
                    }
                    ++apm[command.PlayerIndex];
                }
            }
            for (var i = 0; i < apm.Length; ++i)
            {
                apm[i] /= PlayerLifes[i].TotalMinutes;
            }
            return apm;
        }

        public static double[] CalculateInstantApm(DataPoint[][] data, TimeSpan begin, TimeSpan end)
        {
            return data.Select(playerData =>
            {
                return playerData
                    .SkipWhile(p => TimeSpanAxis.ToTimeSpan(p.X) < begin)
                    .TakeWhile(p => TimeSpanAxis.ToTimeSpan(p.X) < end)
                    .Average(p => new double?(p.Y)) ?? double.NaN;
            }).ToArray();
        }

        public Dictionary<byte, int[]> GetCommandCounts(TimeSpan begin, TimeSpan end)
        {
            var playerCommands = new Dictionary<byte, int[]>();

            foreach (var (time, commands) in _commands)
            {
                if (time < begin)
                {
                    continue;
                }
                if (time >= end)
                {
                    break;
                }
                foreach (var command in commands)
                {
                    if (!playerCommands.TryGetValue(command.CommandId, out var commandCount))
                    {
                        commandCount = playerCommands[command.CommandId] = new int[Players.Length];
                    }
                    if (command.PlayerIndex >= 0 && command.PlayerIndex < Players.Length)
                    {
                        commandCount[command.PlayerIndex] = commandCount[command.PlayerIndex] + 1;
                    }
                }
            }
            return playerCommands;
        }

        public static bool IsUnknown(byte commandId)
        {
            if (UnknownCommands.ContainsKey(commandId))
            {
                return true;
            }
            if (RA3Commands.IsUnknownCommand(commandId))
            {
                UnknownCommands.TryAdd(commandId, default);
                return true;
            }
            return false;
        }

        public static bool IsAuto(byte commandId) => AutoCommands.ContainsKey(commandId);

        public static bool IsClick(byte commandId) => commandId is 0xF8 or 0xF5;
    }
}