using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Text;

namespace Kit.Helpers.Log
{
    public interface ICallStackLog
    {
        string LogMessage { get; }
        ICallStackLog NextLevel();
        void FinishLevel();
        void BeginCall(string message);
        void EndCall();
    }

    public class EmptyCallStackLog : ICallStackLog
    {
        public string LogMessage
        {
            get { return ""; }
        }

        public ICallStackLog NextLevel()
        {
            return CallStackLog.Empty;
        }

        public void FinishLevel()
        {
            return;
        }

        public void BeginCall(string message)
        {
            return;
        }

        public void EndCall()
        {
            return;
        }
    }

    public class CallStackLog : ICallStackLog
    {
        protected ConcurrentDictionary<string, long> Logs { get; }
        protected ConcurrentBag<CallStackLog> ChildCallStackLogs { get; set; }
        protected readonly int _level;
        protected readonly string _path;
        protected string _currentCall;
        protected int _number;
        protected readonly Stopwatch _stopwatch = new Stopwatch();
        public static ICallStackLog Empty { get { return new EmptyCallStackLog(); } }

        protected string Number
        {
            get
            {
                return _number.ToString("D5");
            }
        }
        public CallStackLog()
        {
            Logs = new ConcurrentDictionary<string, long>();
            ChildCallStackLogs = new ConcurrentBag<CallStackLog>();
            _level = 0;
            _path = "";
            _number = 1;
        }

        public CallStackLog(int level, string path)
        {
            Logs = new ConcurrentDictionary<string, long>();
            ChildCallStackLogs = new ConcurrentBag<CallStackLog>();
            _level = level;
            _path = path;
            _number = 1;
        }

        public string LogMessage
        {
            get
            {
                StringBuilder stringBuilder = new StringBuilder();
                foreach (var logLine in Logs.OrderBy(x => x.Key))
                {
                    string time = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:0.000}", logLine.Value / 1000.0f);

                    stringBuilder.AppendLine(logLine.Key + $". Time - {time} ms");
                }
                return stringBuilder.ToString();
            }
        }
        public ConcurrentDictionary<string, long> GetLogs
        {
            get {
                return this.Logs;
            }
        }

        public ICallStackLog NextLevel()
        {
            var childCallStackLog = new CallStackLog(_level + 1, _path + this.Number + ".");
            ChildCallStackLogs.Add(childCallStackLog);
            return childCallStackLog;
        }

        public void FinishLevel()
        {
            foreach (CallStackLog childCallStackLog in ChildCallStackLogs)
            {
                lock (Logs)
                {
                    foreach (var logLine in childCallStackLog.Logs)
                    {
                        if (Logs.ContainsKey(logLine.Key))
                        {
                            Logs[logLine.Key] += logLine.Value;
                        }
                        else
                        {
                            Logs[logLine.Key] = logLine.Value;
                        }
                    }
                }
            }

            ChildCallStackLogs = new ConcurrentBag<CallStackLog>();
        }

        public void BeginCall(string message)
        {
            _currentCall = message;
            _stopwatch.Restart();
        }

        public void EndCall()
        {
            _stopwatch.Stop();
            lock (Logs)
            {
                long ticksByMicrosecond = Stopwatch.Frequency / (1000L * 1000L);
                long microSeconds = _stopwatch.ElapsedTicks / ticksByMicrosecond;
                Logs.TryAdd($"{_path}{this.Number} {_currentCall.ToStringOrEmpty()}", microSeconds);
                _number++;
            }
        }
    }

    public class CallStackLogFile : ICallStackLog
    {
        protected object _lock = new object();
        protected ConcurrentBag<CallStackLogFile> ChildCallStackLogs { get; set; }
        protected readonly int _level;
        protected readonly string _path;
        protected string _currentCall;
        protected int _number;
        protected readonly Stopwatch _stopwatch = new Stopwatch();
        protected readonly StreamWriter _writer;
        public static ICallStackLog Empty { get { return new EmptyCallStackLog(); } }

        protected string Number
        {
            get
            {
                return _number.ToString("D5");
            }
        }
        public CallStackLogFile(StreamWriter writer)
        {
            _writer = writer;
            ChildCallStackLogs = new ConcurrentBag<CallStackLogFile>();
            _level = 0;
            _path = "";
            _number = 1;
        }

        public CallStackLogFile(StreamWriter writer, int level, string path)
        {
            _writer = writer;
            ChildCallStackLogs = new ConcurrentBag<CallStackLogFile>();
            _level = level;
            _path = path;
            _number = 1;
        }

        public string LogMessage => string.Empty;
        
        public ICallStackLog NextLevel()
        {
            var childCallStackLog = new CallStackLogFile(_writer, _level + 1, _path + this.Number + ".");
            ChildCallStackLogs.Add(childCallStackLog);
            return childCallStackLog;
        }

        public void FinishLevel()
        {
            ChildCallStackLogs = new ConcurrentBag<CallStackLogFile>();
        }

        public void BeginCall(string message)
        {
            _currentCall = message;
            _stopwatch.Restart();
        }

        public void EndCall()
        {
            _stopwatch.Stop();
            lock (_lock)
            {
                long microSeconds = _stopwatch.ElapsedTicks / (Stopwatch.Frequency / (1000L * 1000L));

                string time = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:0.000}", microSeconds / 1000.0f);

                _writer?.WriteLine($"{_path}{this.Number} {_currentCall.ToStringOrEmpty()}. Time - {time} ms");
                _number++;
            }
        }
    }
}
