content: package featurecat.lizzie.analysis;
  
  import com.zaxxer.nuprocess.NuAbstractProcessHandler;
  import com.zaxxer.nuprocess.NuProcess;
  import com.zaxxer.nuprocess.NuProcessBuilder;
  import com.zaxxer.nuprocess.NuProcessHandler;
  import org.apache.commons.lang3.tuple.ImmutablePair;
  import org.jtrim2.utils.ObjectFinalizer;
  import featurecat.lizzie.util.ArgumentTokenizer;
  import featurecat.lizzie.util.ThreadPoolUtil;
  
  import java.io.Closeable;
  import java.io.IOException;
  import java.nio.ByteBuffer;
  import java.util.List;
  import java.util.Objects;
  import java.util.concurrent.*;
  import java.util.function.Consumer;
  import java.util.stream.Collectors;
  
  public class GeneralGtpClient implements GtpClient {
      protected class GeneralGtpProcessHandler extends NuAbstractProcessHandler implements Closeable {
          private final ObjectFinalizer objectFinalizer;
  
          protected ExecutorService stdoutProcessor;
          protected ExecutorService stderrProcessor;
          protected ExecutorService miscProcessor;
  
          protected StringBuilder stdoutLineBuilder;
          protected StringBuilder stderrLineBuilder;
          protected boolean inCommandResponse;
  
          public GeneralGtpProcessHandler() {
              objectFinalizer = new ObjectFinalizer(this::doCleanup, "GtpClientHandler.cleanup");
  
              stdoutProcessor = Executors.newSingleThreadExecutor();
              stderrProcessor = Executors.newSingleThreadExecutor();
              miscProcessor = Executors.newSingleThreadExecutor();
              stdoutLineBuilder = new StringBuilder(2048);
              stderrLineBuilder = new StringBuilder(2048);
              inCommandResponse = false;
          }
  
          @Override
          public void onStart(final NuProcess nuProcess) {
              miscProcessor.execute(() -> engineStartedObserverList.forEach(observer -> observer.accept(nuProcess.getPID())));
          }
  
          @Override
          public void onExit(final int statusCode) {
              ImmutablePair<GeneralGtpFuture, Consumer<String>> futurePair;
  
              while ((futurePair = runningCommandQueue.poll()) != null) {
                  futurePair.getLeft().markCompleted();
              }
  
              while ((futurePair = stagineCommandQueue.poll()) != null) {
                  futurePair.getLeft().markCompleted();
              }
  
              miscProcessor.execute(() -> engineExitObserverList.forEach(observer -> observer.accept(statusCode)));
  
              engineExit = true;
          }
  
          @Override
          public void onStdout(final ByteBuffer buffer, final boolean closed) {
              if (!closed) {
                  final byte[] bytes = new byte[buffer.remaining()];
                  // You must update buffer.position() before returning (either implicitly,
                  // like this, or explicitly) to indicate how many bytes your handler has consumed.
                  buffer.get(bytes);
  
                  stdoutProcessor.execute(() -> {
                      for (byte b : bytes) {
                          stdoutLineBuilder.append((char) b);
                          if (b == '\n') {
                              onEngineStdoutLine(stdoutLineBuilder.toString());
                              stdoutLineBuilder = new StringBuilder(2048);
                          }
                      }
                  });
              }
          }
  
          @Override
          public void onStderr(final ByteBuffer buffer, final boolean closed) {
              if (!closed) {
                  final byte[] bytes = new byte[buffer.remaining()];
                  // You must update buffer.position() before returning (either implicitly,
                  // like this, or explicitly) to indicate how many bytes your handler has consumed.
                  buffer.get(bytes);
  
                  stderrProcessor.execute(() -> {
                      for (byte b : bytes) {
                          stderrLineBuilder.append((char) b);
                          if (b == '\n') {
                              onEngineStderrLine(stderrLineBuilder.toString());
                              stderrLineBuilder = new StringBuilder(2048);
                          }
                      }
                  });
              }
          }
  
          @Override
          public synchronized boolean onStdinReady(final ByteBuffer buffer) {
              if (!runningCommandQueue.isEmpty() && runningCommandQueue.stream().anyMatch(pair -> !pair.getLeft().isContinuous())) {
                  return false;
              }
  
              ImmutablePair<GeneralGtpFuture, Consumer<String>> futurePair = stagineCommandQueue.poll();
              if (futurePair != null) {
                  GeneralGtpFuture future = futurePair.getLeft();
                  buffer.put(future.getCommand().getBytes());
                  if (!future.getCommand().endsWith("\n")) {
                      buffer.put((byte) '\n');
                  }
                  buffer.flip();
  
                  runningCommandQueue.offer(futurePair);
                  miscProcessor.execute(() -> engineGtpCommandObserverList.forEach(observer -> observer.accept(future.getCommand())));
  
                  // In case of some particular situations
                  if (future.isContinuous() && !stagineCommandQueue.isEmpty()) {
                      return true;
                  }
              }
  
              return false;
          }
  
          protected synchronized void onEngineStdoutLine(final String line) {
              engineStdoutLineConsumerList.forEach(consumer -> consumer.accept(line));
              if (inCommandResponse) {
                  ImmutablePair<GeneralGtpFuture, Consumer<String>> futurePair = Objects.requireNonNull(runningCommandQueue.peek());
                  GeneralGtpFuture future = Objects.requireNonNull(futurePair.getLeft());
                  Consumer<String> commandOutputConsumer = futurePair.getRight();
                  List<String> response = future.getResponse();
  
                  if (commandOutputConsumer == null) {
                      response.add(line);
                  } else {
                      commandOutputConsumer.accept(line);
                  }
  
                  if (line.equals("\n") || line.equals("\r\n") || line.equals("\r")) {
                      future.markCompleted();
  
                      runningCommandQueue.poll();
                      inCommandResponse = false;
  
                      // Notify for next command processing
                      if (runningCommandQueue.stream().allMatch(pair -> pair.getLeft().isContinuous()) && !stagineCommandQueue.isEmpty()) {
                          gtpProcess.wantWrite();
                      }
                  } else {
                      // Prevent stuck
                      if (future.isContinuous() && runningCommandQueue.stream().allMatch(pair -> pair.getLeft().isContinuous()) && !stagineCommandQueue.isEmpty()) {
                          gtpProcess.wantWrite();
                      }
                  }
              } else if (line.startsWith("=") || line.startsWith("?")) {
                  inCommandResponse = true;
  
                  ImmutablePair<GeneralGtpFuture, Consumer<String>> futurePair = Objects.requireNonNull(runningCommandQueue.peek());
                  GeneralGtpFuture future = Objects.requireNonNull(futurePair.getLeft());
                  Consumer<String> commandOutputConsumer = futurePair.getRight();
                  List<String> response = future.getResponse();
  
                  future.markStarted();
  
                  if (commandOutputConsumer == null) {
                      response.add(line);
                  } else {
                      commandOutputConsumer.accept(line);
                      // Prevent stuck
                      if (future.isContinuous() && runningCommandQueue.stream().allMatch(pair -> pair.getLeft().isContinuous()) && !stagineCommandQueue.isEmpty()) {
                          gtpProcess.wantWrite();
                      }
                  }
              } else {
                  onEngineDiagnosticLine(line);
              }
          }
  
          protected void onEngineStderrLine(final String line) {
              engineStderrLineConsumerList.forEach(consumer -> consumer.accept(line));
          }
  
          protected void onEngineDiagnosticLine(final String line) {
              engineDiagnosticLineConsumerList.forEach(consumer -> consumer.accept(line));
          }
  
          private void doCleanup() {
              if (stdoutProcessor != null) {
                  ThreadPoolUtil.shutdownAndAwaitTermination(stdoutProcessor);
                  stdoutProcessor = null;
              }
  
              if (stderrProcessor != null) {
                  ThreadPoolUtil.shutdownAndAwaitTermination(stderrProcessor);
                  stderrProcessor = null;
              }
  
              if (miscProcessor != null) {
                  ThreadPoolUtil.shutdownAndAwaitTermination(miscProcessor);
                  miscProcessor = null;
              }
          }
  
          @Override
          public void close() {
              objectFinalizer.doFinalize();
          }
      }
  
      private final ObjectFinalizer objectFinalizer;
      private List<String> gtpCommandLine;
      private NuProcessHandler gtpProcessHandler;
      private NuProcess gtpProcess;
      private ConcurrentLinkedQueue<ImmutablePair<GeneralGtpFuture, Consumer<String>>> stagineCommandQueue;
      private ConcurrentLinkedQueue<ImmutablePair<GeneralGtpFuture, Consumer<String>>> runningCommandQueue;
      private List<Consumer<String>> engineDiagnosticLineConsumerList;
      private List<Consumer<String>> engineStdoutLineConsumerList;
      private List<Consumer<String>> engineStderrLineConsumerList;
      private List<Consumer<Integer>> engineStartedObserverList;
      private List<Consumer<Integer>> engineExitObserverList;
      private List<Consumer<String>> engineGtpCommandObserverList;
      private boolean engineExit;
  
      public GeneralGtpClient(String commandLine) {
          this(ArgumentTokenizer.tokenize(commandLine));
      }
  
      public GeneralGtpClient(List<String> commandLine) {
          objectFinalizer = new ObjectFinalizer(this::doCleanup, "GtpClient.cleanup");
  
          gtpCommandLine = commandLine;
          stagineCommandQueue = new ConcurrentLinkedQueue<>();
          runningCommandQueue = new ConcurrentLinkedQueue<>();
          engineDiagnosticLineConsumerList = new CopyOnWriteArrayList<>();
          engineStdoutLineConsumerList = new CopyOnWriteArrayList<>();
          engineStderrLineConsumerList = new CopyOnWriteArrayList<>();
          engineStartedObserverList = new CopyOnWriteArrayList<>();
          engineExitObserverList = new CopyOnWriteArrayList<>();
          engineGtpCommandObserverList = new CopyOnWriteArrayList<>();
          engineExit = false;
      }
  
      public void registerDiagnosticLineConsumer(Consumer<String> consumer) {
          engineDiagnosticLineConsumerList.add(consumer);
      }
  
      public void unregisterDiagnosticLineConsumer(Consumer<String> consumer) {
          engineDiagnosticLineConsumerList.remove(consumer);
      }
  
      @Override
      public void registerStdoutLineConsumer(Consumer<String> consumer) {
          engineStdoutLineConsumerList.add(consumer);
      }
  
      @Override
      public void unregisterStdoutLineConsumer(Consumer<String> consumer) {
          engineStdoutLineConsumerList.remove(consumer);
      }
  
      @Override
      public void registerStderrLineConsumer(Consumer<String> consumer) {
          engineStderrLineConsumerList.add(consumer);
      }
  
      @Override
      public void unregisterStderrLineConsumer(Consumer<String> consumer) {
          engineStderrLineConsumerList.remove(consumer);
      }
  
      public boolean removeCommandFromStagineQueue(GeneralGtpFuture future) {
          return stagineCommandQueue.removeIf(futurePair -> futurePair.getLeft().equals(future));
      }
  
      @Override
      public GtpFuture postCommand(String command, boolean continuous, Consumer<String> commandOutputConsumer) {
          GeneralGtpFuture future = new GeneralGtpFuture(command, this, continuous);
          synchronized (gtpProcessHandler) {
              stagineCommandQueue.offer(ImmutablePair.of(future, commandOutputConsumer));
              if (runningCommandQueue.isEmpty() || runningCommandQueue.stream().allMatch(pair -> pair.getLeft().isContinuous())) {
                  gtpProcess.wantWrite();
              }
          }
  
          return future;
      }
  
      @Override
      public void start() {
          NuProcessBuilder processBuilder = new NuProcessBuilder(gtpCommandLine);
  
          gtpProcessHandler = provideProcessHandler();
          processBuilder.setProcessListener(gtpProcessHandler);
          setUpOtherProcessParameters(processBuilder);
  
          gtpProcess = Objects.requireNonNull(processBuilder.start());
      }
  
      protected void setUpOtherProcessParameters(NuProcessBuilder processBuilder) {
      }
  
      protected NuProcessHandler provideProcessHandler() {
          return this.new GeneralGtpProcessHandler();
      }
  
      private int doShutdown(long timeout, TimeUnit timeUnit) {
          int exitCode = Integer.MIN_VALUE;
          if (gtpProcess != null && gtpProcess.isRunning()) {
              try {
                  postCommand("quit");
                  exitCode = gtpProcess.waitFor(timeout, timeUnit);
                  if (exitCode == Integer.MIN_VALUE) {
                      gtpProcess.destroy(false);
                      exitCode = gtpProcess.waitFor(timeout, timeUnit);
                      if (exitCode == Integer.MIN_VALUE) {
                          gtpProcess.destroy(true);
                      }
                  }
              } catch (InterruptedException e) {
                  gtpProcess.destroy(true);
              }
  
              gtpProcess = null;
          }
  
          if (gtpProcessHandler instanceof Closeable) {
              try {
                  ((Closeable) gtpProcessHandler).close();
                  gtpProcessHandler = null;
              } catch (IOException e) {
                  // Do nothing
              }
          }
  
          return exitCode;
      }
  
      @Override
      public int shutdown(long timeout, TimeUnit timeUnit) {
          int exitCode = doShutdown(timeout, timeUnit);
          objectFinalizer.markFinalized();
  
          return exitCode;
      }
  
      @Override
      public boolean isRunning() {
          return gtpProcess != null && gtpProcess.isRunning();
      }
  
      @Override
      public boolean isShutdown() {
          return engineExit;
      }
  
      @Override
      public void registerEngineStartedObserver(Consumer<Integer> observer) {
          engineStartedObserverList.add(observer);
      }
  
      @Override
      public void unregisterEngineStartedObserver(Consumer<Integer> observer) {
          engineStartedObserverList.remove(observer);
      }
  
      @Override
      public void registerEngineExitObserver(Consumer<Integer> observer) {
          engineExitObserverList.add(observer);
      }
  
      @Override
      public void unregisterEngineExitObserver(Consumer<Integer> observer) {
          engineExitObserverList.remove(observer);
      }
  
      @Override
      public void registerGtpCommandObserver(Consumer<String> observer) {
          engineGtpCommandObserverList.add(observer);
      }
  
      @Override
      public void unregisterGtpCommandObserver(Consumer<String> observer) {
          engineGtpCommandObserverList.remove(observer);
      }
  
      private void doCleanup() {
          doShutdown(60, TimeUnit.SECONDS);
      }
  
      @Override
      public void close() {
          objectFinalizer.doFinalize();
      }
  
      public static void main(String[] args) throws Exception {
          final GtpClient gtpClient = new GeneralGtpClient("leelaz.exe -g -t2 -wnetwork");
          gtpClient.start();
  
          Future<List<String>> timeResult = gtpClient.postCommand("888 time_settings 0 10 1");
  
          new Thread(() -> {
              Future<List<String>> nameResult = gtpClient.postCommand("1 name");
              for (int i = 0; i < 100; ++i) {
                  gtpClient.postCommand(100 + i + " name");
                  gtpClient.postCommand(200 + i + " name");
              }
  
              try {
                  System.out.println(nameResult.get().stream().map(String::trim).collect(Collectors.toList()));
              } catch (InterruptedException e) {
                  e.printStackTrace();
              } catch (ExecutionException e) {
                  e.printStackTrace();
              }
          }).start();
  
          new Thread(() -> {
              Future<List<String>> errorResult = gtpClient.postCommand("1234 estimate_score");
              for (int i = 0; i < 145; ++i) {
                  gtpClient.postCommand(300 + i + " name");
                  gtpClient.postCommand(400 + i + " list_commands");
              }
              try {
                  System.out.println(errorResult.get().stream().map(String::trim).collect(Collectors.toList()));
              } catch (InterruptedException e) {
                  e.printStackTrace();
              } catch (ExecutionException e) {
                  e.printStackTrace();
              }
          }).start();
  
          gtpClient.postCommand("5 name");
          gtpClient.postCommand("5 name");
          Future<List<String>> nameResult = gtpClient.postCommand("6 name");
          gtpClient.postCommand("list_commands");
          Future<List<String>> genMoveResult = gtpClient.postCommand("100 genmove b");
          Future<List<String>> listResult = gtpClient.postCommand("7 list_commands");
          gtpClient.postCommand("8 list_commands");
          gtpClient.postCommand("99999 list_commands", s -> System.out.println(s.trim()));
          gtpClient.postCommand("8 list_commands");
          gtpClient.postCommand("111111 list_commands");
  
          System.out.println(genMoveResult.get().stream().map(String::trim).collect(Collectors.toList()));
          System.out.println(listResult.get().stream().map(String::trim).collect(Collectors.toList()));
          System.out.println(nameResult.get().stream().map(String::trim).collect(Collectors.toList()));
          System.out.println(timeResult.get().stream().map(String::trim).collect(Collectors.toList()));
  
          gtpClient.postCommand("hello");
          gtpClient.postCommand("hello");
          gtpClient.postCommand("hello");
          gtpClient.postCommand("hello");
          gtpClient.postCommand("hello");
          gtpClient.postCommand("hello");
          System.out.printf("Staging: %d, Running: %d\n", ((GeneralGtpClient) gtpClient).stagineCommandQueue.size(), ((GeneralGtpClient) gtpClient).runningCommandQueue.size());
  
          gtpClient.postCommand("97654 lz-analyze 50", true, s -> System.out.println(s.trim()));
          System.out.printf("Staging: %d, Running: %d\n", ((GeneralGtpClient) gtpClient).stagineCommandQueue.size(), ((GeneralGtpClient) gtpClient).runningCommandQueue.size());
          gtpClient.postCommand("hello");
          System.out.printf("Staging: %d, Running: %d\n", ((GeneralGtpClient) gtpClient).stagineCommandQueue.size(), ((GeneralGtpClient) gtpClient).runningCommandQueue.size());
  
          Thread.sleep(2000);
  
          gtpClient.postCommand("87654 lz-analyze 50", true, s -> System.out.println(s.trim()));
          System.out.printf("Staging: %d, Running: %d\n", ((GeneralGtpClient) gtpClient).stagineCommandQueue.size(), ((GeneralGtpClient) gtpClient).runningCommandQueue.size());
  
          Thread.sleep(10000);
  
          nameResult = gtpClient.postCommand("89012 name");
          System.out.println(nameResult.get().stream().map(String::trim).collect(Collectors.toList()));
  
          gtpClient.shutdown(60, TimeUnit.SECONDS);
      }
  }
  