﻿using BNEBotCore;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace BNEBotLauncher
{
  public static class BotProcessManager
  {
    static BotProcessManager()
    {
      m_UpdateThread = new Thread(UpdateProc);
      m_UpdateThread.Start();
    }

    // Launch the given No. of bots
    public static void SpawnBots(int count, string strategy = "", int targetLevel = 0)
    {
      for (int i = 0; i < count; ++i)
      {
        try
        {
          string botExe = LaunchSettings.ExeFile;
          if (LaunchSettings.DebugExe)
          {
            botExe += "_d";
          }
          BotProcess bot = BotProcess.StartNew(botExe, strategy, targetLevel);

          lock (m_DataLock)
          {
            m_RunningBots.Add(bot);
          }

          Thread.Sleep(LaunchSettings.SpawnInterval);
        }
        catch (Exception)
        {
          // TODO: error output
          i = i;
        }
      }
    }

    // Enqueue spawn command
    public static void QueueSpawnCommand(SpawnBotsCommand.SpawnInfo spawnInfo)
    {
      EnqueuedSpawns.Enqueue(spawnInfo);
    }

    private static ConcurrentQueue<SpawnBotsCommand.SpawnInfo> EnqueuedSpawns = new ConcurrentQueue<SpawnBotsCommand.SpawnInfo>();

    // Terminate all bot processes
    public static void KillAll()
    {
      lock (m_DataLock)
      {
        foreach (BotProcess bot in m_RunningBots)
        {
          bot.Kill();
        }
      }
    }

    // Similar to KillAll but indicates normal exit path
    // Returns true if all the bots have exited normally (not crashed)
    public static bool Terminate()
    {
      bool result = true;

      lock (m_DataLock)
      {
        // Collect some stats
        int running = 0, stopped = 0, killed = 0, crashed = 0;
        foreach (BotProcess bot in m_RunningBots)
        {
          if (bot.Bot.State == BotState.Running)
          {
            running++;

            bot.Bot.State = BotState.Stopped;
            bot.Kill();
          }
          else if (bot.Bot.State == BotState.Crashed)
          {
            crashed++;

            result = false;
          }
          else
          {
            switch (bot.Bot.State)
            {
              case BotState.Crashed:
                crashed++;
                break;
              case BotState.Killed:
                killed++;
                break;
              case BotState.Running:
                running++;
                break;
              case BotState.Stopped:
                stopped++;
                break;
            }
          }
        }

        Console.WriteLine("Bot launcher finished, report:");
        Console.WriteLine("\tBots finished: " + stopped);
        Console.WriteLine("\tBots crashed: " + crashed);
        Console.WriteLine("\tBots killed: " + killed);
        Console.WriteLine("\tBots still running: " + running);
      }

      m_Exit = true;
      if (m_UpdateThread.IsAlive)
      {
        m_UpdateThread.Join();
      }

      return result;
    }

    // Terminate a specific bot process
    public static void KillBot(string botName)
    {
      lock (m_DataLock)
      {
        foreach (BotProcess bot in m_RunningBots)
        {
          if (bot.Bot.Name == botName)
          {
            bot.Kill();
            break;
          }
        }
      }
    }

    // Collect info about all bots
    public static List<BotInfo> GetBotInfo()
    {
      lock (m_DataLock)
      {
        // FIXME: we also use this to update the process info
        List<BotInfo> bots = new List<BotInfo>();
        foreach (BotProcess bot in m_RunningBots)
        {
          if (!bot.Proc.HasExited)
          {
            bot.Bot.Uptime = DateTime.Now - bot.Bot.LaunchTimestamp;
          }

          bots.Add(bot.Bot);
        }

        //return (from b in m_RunningBots select b.Bot).ToList();
        return bots;
      }
    }

    // Check if all the bots have finished
    public static bool AnyBotRunning()
    {
      lock (m_DataLock)
      {
        foreach (BotProcess bot in m_RunningBots)
        {
          if (!bot.Proc.HasExited)
          {
            return true;
          }
        }

        return false;
      }
    }

    // Update bot data
    private static void UpdateProc()
    {
      if (LaunchSettings.Randomize)
      {
        PrepareNextRandomEvent();
      }

      while (!m_Exit)
      {
        // FIXME: for now we just use this thread to spawn queued bots. This will hang it until
        // the spawn finishes to no logic further down will run for a while. This is no problem
        // now as we have disabled the perf stats tracking anyway...
        SpawnBotsCommand.SpawnInfo spawn;
        if (EnqueuedSpawns.TryDequeue(out spawn))
        {
          Stopwatch sw = Stopwatch.StartNew();
          
          SpawnBots(spawn.NumBots, spawn.Strategy, spawn.TargetLevel);

          if (LaunchSettings.Randomize)
          {
            // Make sure the time spent spawning does not interfere with our randomization timers
            m_NextRandomEvent += sw.Elapsed;
          }
        }

        if (LaunchSettings.Randomize)
        {
          if (m_NextRandomEvent <= DateTime.Now)
          {
            // Count how many bots are still running
            List<BotProcess> stopped = new List<BotProcess>();
            foreach (BotProcess bot in m_RunningBots)
            {
              if (bot.Bot.State != BotState.Running)
              {
                stopped.Add(bot);
              }
            }

            // How many will be affected
            int cnt = LaunchSettings.RandomCountMin + m_Random.Next(LaunchSettings.RandomCountMax - LaunchSettings.RandomCountMin);
            cnt = Math.Min(cnt, m_RunningBots.Count);

            if (m_RandomKill)
            {
              // Need to kill some more
              int tokill = cnt - stopped.Count;
              while (tokill > 0)
              {
                int bot = m_Random.Next(m_RunningBots.Count);
                BotProcess proc = m_RunningBots[bot];
                if (proc.Bot.State == BotState.Running)
                {
                  tokill--;
                  proc.Kill();
                }
              }
            }
            else
            {
              // Restart stopped bots
              // TEST: restart them all
              foreach (BotProcess bot in stopped)
              {
                bot.Restart();

                Thread.Sleep(LaunchSettings.SpawnInterval);
              }
            }

            // Prepare a new one
            m_RandomKill = !m_RandomKill;
            PrepareNextRandomEvent();
          }
        }

        //int i = 0;
        //int count = 0;
        //lock (m_DataLock)
        //{
        //  count = m_RunningBots.Count;
        //}

        //for (; i < count; ++i)
        //{
        //  lock (m_DataLock)
        //  {
        //    if (i < m_RunningBots.Count)
        //    {
        //      m_RunningBots[i].UpdatePerfStats();
        //    }
        //  }
        //}

        Thread.Sleep(10);
      }
    }

    private static void PrepareNextRandomEvent()
    {
      m_NextRandomEvent = DateTime.Now;
      double min = LaunchSettings.RandomIntervalMin.TotalMinutes;
      double interval = LaunchSettings.RandomIntervalMax.TotalMinutes - min;
      m_NextRandomEvent = m_NextRandomEvent.AddMinutes(min + (m_Random.NextDouble() * interval));
    }

    // Running bots
    private static List<BotProcess> m_RunningBots = new List<BotProcess>();

    private static Thread m_UpdateThread;
    private static bool m_Exit;
    private static object m_DataLock = new object();

    // Spawn randomization
    private static Random m_Random = new Random();
    private static DateTime m_NextRandomEvent;
    private static bool m_RandomKill = true;
  }
}
