/*
 * RiskScape™ Copyright New Zealand Institute for Earth Science Limited
 * (Earth Sciences New Zealand) is distributed for research purposes only
 * under the terms of AGPLv3.
 *
 * RiskScape™ Copyright 2026 New Zealand Institute for Earth Science
 * Limited (Earth Sciences New Zealand). All rights reserved. Source code
 * available under the AGPLv3.
 * 
 * This program is free software: you can redistribute it and/or modify it under
 *  the terms of the GNU Affero General Public License as published by the Free
 *  Software Foundation, either version 3 of the License, or (at your option) any
 *  later version.
 * 
 * This program is distributed for RESEARCH PURPOSES ONLY, in the hope that it will
 * be useful for research and education initiatives.
 * 
 * If you are not a researcher, or you are a researcher who wishes to use this
 * program on terms other than AGPLv3 (including those who wish to restrict the
 * distribution of any source code created using this program), please contact:
 * https://riskscape.org.nz
 * 
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.  You should have received a copy
 * of the GNU Affero General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * By way of summary only, under the AGPLv3:
 *     • Permissions of this strongest copyleft license are conditioned
 *       on making available complete source code of licensed works and
 *       modifications, which include larger works using a licensed work,
 *       under the same license.
 *     • Copyright and license notices must be preserved.
 *     • Contributors provide an express grant of patent rights.
 *     • When a modified version is used to provide a service over a
 *       network, the complete source code of the modified version must be made
 *       available.
 */

package nz.org.riskscape.engine.cli;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.UserInterruptException;
import org.jline.utils.AttributedString;
import org.jline.utils.Status;

import lombok.Getter;
import nz.org.riskscape.cli.AnsiPrintStream;
import nz.org.riskscape.engine.i18n.Messages;

/**
 * A {@link Terminal} implementation that is backed by a Jline terminal. A Jline terminal has good cross
 * platform support for getting terminal widths and detection of ctrl-c whilst reading input lines.
 */
public class JlineTerminal extends BaseTerminal {

  private final org.jline.terminal.Terminal jlineTerminal;
  private final LineReader lineReader;
  @Getter
  private final PrintStream err;
  @Getter
  private final AnsiPrintStream ansiErr;
  @Getter
  private final PrintStream out;
  @Getter
  private final AnsiPrintStream ansiOut;
  @Getter
  private final InputStream in;

  // indicates if the terminal is has been stopped. status updates could not be processed if the terminal
  // is no longer running.
  private final AtomicBoolean stopped = new AtomicBoolean(false);

  public JlineTerminal(org.jline.terminal.Terminal jlineTerminal, Messages messages) {
    super(messages);
    this.jlineTerminal = jlineTerminal;
    this.lineReader = LineReaderBuilder.builder()
        .terminal(jlineTerminal)
        .build();
    this.err = wrapAsUncloseable(System.err);
    this.ansiErr = new AnsiPrintStream(getAnsi(), System.err);
    this.out = wrapAsUncloseable(new PrintStream(jlineTerminal.output()));
    this.ansiOut = new AnsiPrintStream(this);
    this.in = jlineTerminal.input();

    // Set jline to call the RiskScape fallback handler when the user enters ctrl-c. This will allow
    // RiskScape to clean up on user cancel. Note that jline will swap this handler out when it is doing
    // a read line. In that case it'l throw UserInterruptException as is handled in readLine().
    jlineTerminal.handle(org.jline.terminal.Terminal.Signal.INT, sig -> getFallbackHandler().handleInterrupt());
  }

  @Override
  public String readline() throws IOException {
    try {
      return lineReader.readLine();
    } catch (UserInterruptException e) {
      throw new ReadlineInterruptedException();
    }
  }

  @Override
  public int getDisplayWidth() {
    if (! isTTY()) {
      // If we are not running with a tty when the actual width won't be known, so we return the default.
      return DEFAULT_TTY_WIDTH;
    }
    return jlineTerminal.getWidth();
  }

  @Override
  public int getDisplayHeight() {
    if (! isTTY()) {
      // If we are not running with a tty when the actual height won't be known/limited
      return TERMINAL_HEIGHT_UNLIMITED;
    }
    return jlineTerminal.getHeight();
  }

  @Override
  public boolean isTTY() {
    // we infer that that terminal has a tty if it has a width greater than zero.
    return jlineTerminal.getWidth() > 0;
  }

  @Override
  public void updateStatus(List<String> lines) {
    if (stopped.get()) {
      // Don't make any status updates if the terminal has been stopped.
      // Making status updates with a closed terminal can result in junk characters (on Windows)
      // and no one wants that.
      return;
    }

    List<AttributedString> attributedStrings = null;
    if (lines != null) {
      attributedStrings = lines.stream()
          .map(line -> AttributedString.fromAnsi(line))
          .collect(Collectors.toList());
    }


    Status status = Status.getStatus(jlineTerminal);
    synchronized (this) {
      // we do the actual status update inside this synchronized block to unsure updates do not occur at the same
      // time as problem logs are written (see Terminal.formatToErrorStream()).
      // This is because junk characters get written to the Windows console when they occur concurrently.
      if (lines == null || lines.isEmpty()) {
        status.reset();
      } else {
        if (status.size() > attributedStrings.size()) {
          // There are fewer status lines. We need to clear the status first.
          // Update only updates the last x lines of status which means that renmants can remain if you
          // don't clear them out.
          status.clear();
        }

        status.update(attributedStrings);
      }
    }
  }

  @Override
  void doExit() {
    // set stopped to prevent status update processing
    stopped.set(true);
    try {
      // the jline terminal needs a chance to clean itself up. This is more important if it is in
      // the middle of progress updates. So they do not get left hanging around on the terminal.
      jlineTerminal.close();
    } catch (Exception e) {}
    super.doExit();
  }

}
