context: using System;
  using System.Collections.Generic;
  using System.Data;
  using System.Diagnostics;
  using System.IO;
  using System.Linq;
  using System.Threading;
  using System.Threading.Tasks;
  using SokoSolve.Core.Analytics;
  using SokoSolve.Core.Solver.Lookup;
  using SokoSolve.Core.Solver.Queue;
  using TextRenderZ;
  
  namespace SokoSolve.Core.Solver.Solvers
  {
      
      public class MultiThreadedForwardReverseSolver : ISolver
      {
          private readonly ISolverNodePoolingFactory nodePoolingFactory;
          //private SolverStateMultiThreaded? masterState = null;
  
          public MultiThreadedForwardReverseSolver(ISolverNodePoolingFactory nodePoolingFactory)
          {
              this.nodePoolingFactory = nodePoolingFactory;
              var total = Environment.ProcessorCount;
              ThreadCountForward = ThreadCountReverse = (total / 2);
          }
  
          public int ThreadCountForward { get; set; }
          public int ThreadCountReverse { get; set; }
          public int VersionMajor       => 2;
          public int VersionMinor       => 2;
          public int VersionUniversal   => SolverHelper.VersionUniversal;
          public string VersionDescription =>
              $"Multi-threaded logic for solving a set of Forward (T:{ThreadCountForward}) and a set of Reverse (T:{ThreadCountForward}) streams on a SINGLE pool";
          
          public SolverState Init(SolverCommand command)
          {
              if (nodePoolingFactory is ISolveNodePoolingFactoryPuzzleDependant dep)
              {
                  dep.SetupForPuzzle(command.Puzzle);
              }
  
              if (command.ServiceProvider == null)
              {
                  throw new Exception("Must have a non-null ServiceProvider");
              }
  
              var poolForward = command.ServiceProvider.GetInstanceElseDefault<INodeLookup>(
                  () => {
                      var def = new NodeLookupSlimRwLock(new NodeLookupBinarySearchTree(new NodeLookupLongTerm()));
                      command.Report?.WriteLine($"ServiceProvider does not contain ISolverPool; using default => {def.TypeDescriptor}");
                      return def;
                  });
              if (!poolForward.IsThreadSafe) throw new NotSupportedException(poolForward.GetType().Name);
              
              var poolReverse = command.ServiceProvider.GetInstanceElseDefault<INodeLookup>(
                  () => {
                      var def = new NodeLookupSlimRwLock(new NodeLookupBinarySearchTree(new NodeLookupLongTerm()));
                      command.Report?.WriteLine($"ServiceProvider does not contain ISolverPool; using default => {def.TypeDescriptor}");
                      return def;
                  });
              if (!poolReverse.IsThreadSafe) throw new NotSupportedException(poolReverse.GetType().Name);
              poolForward.Statistics.Name  = "Pool (Forward)";
              poolReverse.Statistics.Name  = "Pool (Reverse)";
              
              var queueForward = command.ServiceProvider.GetInstanceElseDefault<ISolverQueue>(
                  () => {
                      var def = new SolverQueueSortedWithDeDup();
                      command.Report?.WriteLine($"ServiceProvider does not contain ISolverQueue; using default => {def.TypeDescriptor}");
                      return def;
                  });
              if (!queueForward.IsThreadSafe) throw new NotSupportedException(queueForward.GetType().Name);
              var queueReverse = command.ServiceProvider.GetInstanceElseDefault<ISolverQueue>(
                  () => {
                      var def = new SolverQueueSortedWithDeDup();
                      command.Report?.WriteLine($"ServiceProvider does not contain ISolverQueue; using default => {def.TypeDescriptor}");
                      return def;
              });
              if (!queueReverse.IsThreadSafe) throw new NotSupportedException(queueReverse.GetType().Name);
              queueForward.Statistics.Name = "Queue (Forward)";
              queueReverse.Statistics.Name = "Queue (Reverse)";
              
              if (command.Topology.PoolEval_QueueUnEval)
              {
                  queueForward.Init(SolverQueueMode.QueueAndUnEvalLookup);
                  queueReverse.Init(SolverQueueMode.QueueAndUnEvalLookup);
              }
              else
              {
                  queueForward.Init(SolverQueueMode.QueueOnly);
                  queueReverse.Init(SolverQueueMode.QueueOnly);    
              }
  
              var fwdEvalMaster = new ForwardEvaluator(command, nodePoolingFactory);
              var fwdRoot       = (SolverNodeRoot)fwdEvalMaster.Init(command, queueForward, poolForward);
              var fwdTree       = new TreeStateCore(fwdRoot, poolForward, queueForward);
  
              var revEvalMaster = new ReverseEvaluator(command, nodePoolingFactory);
              var revRoot       = (ReverseEvaluator.SolverNodeRootReverse)revEvalMaster.Init(command, queueReverse, poolReverse);
              var revTree       = new TreeStateCore(revRoot, poolReverse, queueReverse);
  
              fwdTree.Alt = revTree;
              revTree.Alt = fwdTree;
              
              var masterState = new SolverStateMultiThreaded(command, this, fwdTree, revTree)
              {
                  StaticMaps = new StaticAnalysisMaps(command.Puzzle)
              };
              
              // Common Init
              SolverBase<SolverStateMultiThreaded>.InitState(command, masterState);
              
              masterState.GlobalStats.Name    = GetType().Name;
              masterState.GlobalStats.Started = DateTime.Now;
              
              for (int i = 0; i < ThreadCountForward; i++)
              {
                  var primary = new TreeState(
                      fwdTree.Root, 
                      fwdTree.Pool, 
                      new SolverQueueLocalSlave(fwdTree.Queue),               // Keep a 1:1 between eval and queue (each eval has its own queue)
                      new ForwardEvaluator(command, nodePoolingFactory))
                  {
                      Alt = revTree
                  };
                  var forwardWorker = new SingleThreadWorker(
                      $"F{i,00}",
                      true,
                      nodePoolingFactory,
                      this,
                      masterState,
                      new SolverStateMultiThreaded.WorkerState(masterState, primary));
  
                  forwardWorker.Solver = new ForwardSolver(command, nodePoolingFactory, forwardWorker);
                  
                  // Use Threads instead of Tasks to get explicit control
                  // Also stops .net trying to auto-level CPU demand across other threads
                  forwardWorker.Thread = new Thread(x => Execute((SingleThreadWorker)x))
                  {
                      Name         = forwardWorker.Name,
                      Priority     = ThreadPriority.Normal,
                      IsBackground = false
                  };
  
                  masterState.Workers.Add(forwardWorker);
              }
              for (int i = 0; i < ThreadCountReverse; i++)
              {
                  var primary = new TreeState(
                      revTree.Root, 
                      revTree.Pool, 
                      new SolverQueueLocalSlave(revTree.Queue),               // Keep a 1:1 between eval and queue (each eval has its own queue)
                      new ReverseEvaluator(command, nodePoolingFactory))
                  {
                      Alt = fwdTree
                  };
  
                  var reverseWorker = new SingleThreadWorker(
                      $"R{i,00}",
                      false,
                      nodePoolingFactory,
                      this, masterState,
                      new SolverStateMultiThreaded.WorkerState(masterState, primary));
                  
                  reverseWorker.Solver = new ReverseSolver(command, nodePoolingFactory, reverseWorker);
                  
                  // Use Threads instead of Tasks to get explicit control
                  // Also stops .net trying to auto-level CPU demand across other threads
                  reverseWorker.Thread = new Thread(x => Execute((SingleThreadWorker)x))
                  {
                      Name         = reverseWorker.Name,
                      Priority     = ThreadPriority.Normal,
                      IsBackground = false
                  };
                 
                  masterState.Workers.Add(reverseWorker);
              }
  
              masterState.Statistics.Add(masterState.GlobalStats);
              masterState.Statistics.Add(poolForward.Statistics);
              masterState.Statistics.Add(poolReverse.Statistics);
              masterState.Statistics.Add(queueForward.Statistics);
              masterState.Statistics.Add(queueReverse.Statistics);
              
              return masterState;
          }
          
          
          private ProcessThread? GetThreadProc(Process proc, Thread thread)
          {
              foreach (ProcessThread processThread in proc.Threads)
              {
                  if (processThread.Id == thread.ManagedThreadId)
                  {
                      return processThread;
                  }
              }
              return null;
          }
  
  
          public ExitResult Solve(SolverState state)
          {
              var masterState = (SolverStateMultiThreaded) state;    
              if (masterState.Workers is null || masterState.Workers.Count == 0)
              {
                  throw new Exception("Init did not create workers");
              }
              
              var allTasksArray = masterState.Workers.Select(x=>x.Thread).ToArray();
              var cancel = state.Command.CancellationSource;
              if (cancel.IsCancellationRequested) throw new InvalidDataException();
              
              // Start and wait
              masterState.IsRunning = true;
              
              // TODO: Confirm the queue is NOT empty
              var x = masterState.Forward.Queue;
              foreach (var worker in masterState.Workers)
              {
                  if (worker.Thread == null) throw new ArgumentNullException(nameof(worker.Thread));
                  worker.Thread.Start(worker);
              }
              
              // Setup global/aggregate statistics and updates; Always do this so that the agg stats are up to date for valid exit conditions
              var statisticsTick = Task.Run(() =>
              {
                  try
                  {
                      while (masterState.IsRunning)
                      {
                          Thread.Sleep(1000);
                          state.GlobalStats.TotalNodes = masterState.Forward.Pool.Statistics.TotalNodes
                                                         + masterState.Reverse.Pool.Statistics.TotalNodes;
  
                          state.GlobalStats.Warnings = masterState.Workers.Sum(x => x.WorkerState.GlobalStats.Warnings);
                          state.GlobalStats.Errors   = masterState.Workers.Sum(x => x.WorkerState.GlobalStats.Errors);
  
                          int df = 0;
                          int dr = 0;
                          foreach (var worker in masterState.Workers)
                          {
                              if (worker.IsForward && worker.WorkerState.GlobalStats.DepthMax > df) df = worker.WorkerState.GlobalStats.DepthMax;
                              if (!worker.IsForward && worker.WorkerState.GlobalStats.DepthMax > dr) dr = worker.WorkerState.GlobalStats.DepthMax;
                          }
                          state.GlobalStats.DepthMax = Math.Max(df, dr);
                          if (state.Command.AggProgress != null)
                          {
                              var qf = masterState.Forward.Queue.Statistics.CurrentNodes * 100f / masterState.Forward.Pool.Statistics.TotalNodes;
                              var qr = masterState.Reverse.Queue.Statistics.CurrentNodes * 100f / masterState.Reverse.Pool.Statistics.TotalNodes;
  
                              var txt = new FluentString()
                                        .Append($"==> {state.GlobalStats.ToString(false, true)}")
                                        .Append($" Fwd({masterState.Forward.Pool.Statistics.TotalNodes:#,##0} q{qf:#,##0.0}%)")
                                        .Append($" Rev({masterState.Reverse.Pool.Statistics.TotalNodes:#,##0} q{qr:#,##0.0}%)")
                                        .Append($" Depth({df}:{dr})")
                                        .IfNotNull(masterState.KnownSolutionTracker, tracker => "; " + tracker!.Status)
                                  ;
                              state.Command.AggProgress.Update(this, state, state.GlobalStats, txt);    
                          }
                      }
  
                  }
                  catch (Exception e)
                  {
                      Console.WriteLine(e);
                      Console.WriteLine("STOPPING AGG Progress...");
                  }
                  finally
                  {
                      if (state.Command.AggProgress is IDisposable dp) dp.Dispose();
                  }
              });    
  
              if (WaitForAll(allTasksArray, state.Command.ExitConditions.Duration.TotalMilliseconds, cancel.Token))
              {
                  // Close down the workers as gracefully as possible
                  state.Command.ExitConditions.ExitRequested = true;
                  state.Command.CancellationSource.Cancel(true);
                  
                  Console.WriteLine("Timeout... Cancelling");
                  
                  // Allow them to process the ExitRequested
                  Thread.Sleep((int)TimeSpan.FromSeconds(1).TotalMilliseconds);
                  
                  // Close down any outliers
                  var wait = masterState.Workers.Any(x => x.IsRunning);
                  if (wait)
                  {
                      Console.WriteLine("Waiting.... For Task to complete (after cancellation)");
                      Thread.Sleep((int)TimeSpan.FromSeconds(5).TotalMilliseconds);
  
                      var notFinished = masterState.Workers.Where(x => x.IsRunning);
                      if (notFinished.Any())
                      {
                          throw new AggregateException("Workers did not exit");
                      }
                  }
                  
                  state.Exit = ExitResult.TimeOut;
              }
              
              #if WINDOWS
              // Cleanup
              foreach (var worker in masterState.Workers)
              {
  
gt:                 if (worker.IsRunning) worker.Thread.Abort();