/*
 * 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.PrintStream;
import java.util.Collections;
import java.util.Formatter;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;

import com.google.common.base.Strings;
import com.google.common.collect.Lists;

import nz.org.riskscape.engine.i18n.Messages;
import nz.org.riskscape.engine.pipeline.RealizedPipeline;
import nz.org.riskscape.engine.pipeline.RealizedStep;
import nz.org.riskscape.engine.types.ContainingType;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Struct.StructMember;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.rl.ast.Expression;

/**
 * Mixin for rendering {@link RealizedPipeline} in a human understandable form.
 */
public interface PipelineRenderer {

  Messages getMessages();

  Locale getLocale();

  /**
   * Prints the structure and parameters of the {@link RealizedPipeline}.
   *
   * @param realized pipeline to print structure of
   * @param out where to print structure to
   */
  default void printPipeline(RealizedPipeline realized, Formatter out) {
    for (RealizedStep step : realized.getRealizedSteps()) {
      printStep(step, out);
    }
  }

  default void printStep(RealizedStep step, Formatter out) {
    out.format("Step: %s:[%s]%n", step.getName(), step.getImplementation());
    if (!step.getDependencies().isEmpty()) {
      out.format("  Inputs: %s%n", step.getDependencies().stream().map(RealizedStep::getName)
          .collect(Collectors.toList()));
    }
    out.format("  Parameters: %n");
    List<String> parameterNames = Lists.newArrayList(step.getBoundParameters().keySet());
    Collections.sort(parameterNames);
    for (String parameterName : parameterNames) {
      List<?> boundValues = step.getBoundParameters().get(parameterName);

      if (boundValues.size() == 1) {
        out.format("    %s : %s%n", parameterName, boundPipelineParameterToString(boundValues.get(0)));
      } else if (boundValues.size() > 1) {
        out.format("    %s : [%n", parameterName);
        int idx = 0;
        for (Object object : boundValues) {
          out.format("        %d : %s%n", idx + 1, boundPipelineParameterToString(object));
        }
        out.format("    ]%n");
      } else {
        out.format("    %s : <none-given>%n", parameterName);
      }
    }
    out.format("  Produces:%n");
    printStruct(out, 6, step.getProduces());

    ResultOrProblems<?> resultOr = step.getResult();
    if (resultOr.hasErrors()) {
      out.format("  Result: failed%n");
      for (Problem problem : resultOr.getProblems()) {
        out.format("%s%n", getMessages().renderProblem(problem, getLocale())
            .toString(opts -> opts.prefixAll = "    "));
      }
    } else {
      Object result = resultOr.get();
      out.format("  Result: %s%n", result.getClass().getSimpleName());
    }
  }

  default String boundPipelineParameterToString(Object object) {
    if (object instanceof Expression) {
      return ((Expression) object).toSource();
    } else {
      return object.toString();
    }
  }

  /**
   * Pretty-prints the type to the given {@link Formatter}, terminated with a line feed.
   */
  default void printStruct(Formatter stdout, int indent, Struct type) {
    printStructNoLF(stdout, indent, type);
    if (! Struct.EMPTY_STRUCT.equals(type)) {
      // don't print a line feed if the struct is empty, nothing at all should be printed in that case.
      stdout.format("%n");
    }
  }

  /**
   * interface-private method for printing the given struct type so that the printed type will never end with a
   * trailing line feed.
   */
  private static void printStructNoLF(Formatter stdout, int i, Struct type) {
    String spaces = Strings.repeat(" ", i);
    for (int idx = 0; idx < type.getMembers().size(); idx++) {
      StructMember member = type.getMembers().get(idx);
      stdout.format("%s%s => ", spaces, member.getKey());
      printTypeNoLF(stdout, i, member.getType());
      if (idx + 1 < type.getMembers().size()) {
        // we don't put a trailling line feed on the last member
        stdout.format("%n");
      }
    }
  }

  /**
   * interface-private method for printing the given type without a trailing line feed.
   */
  private static void printTypeNoLF(Formatter stdout, int i, Type type) {
    if (type.getUnwrappedType() instanceof Struct struct) {
      stdout.format("%n");
      printStructNoLF(stdout, i + 2, struct);
    } else if (type.getUnwrappedType() instanceof ContainingType ct) {
      stdout.format("%s[", ct.getClass().getSimpleName());
      printTypeNoLF(stdout, i, ct.getContainedType());
      if (ct.getContainedType().isA(Struct.class)) {
        // a struct has line feeds (but not on the last member. So we need:
        // - add the line feed
        // - pad out the indent spaces
        // - then close the containing type
        stdout.format("%n%s]", ct.getContainedType().isA(Struct.class) ? Strings.repeat(" ", i) : "");
      } else {
        stdout.format("]");
      }
    } else {
      stdout.format("%s", type);
    }
  }

  /**
   * Render the pipeline as a graph suitable to input to dot(graphviz).
   * @param pipeline to render
   * @param dotOut print stream to render pipeline to
   * @param graphType
   * @param name to assign pipeline in rendered graph
   * @param other
   */
  default void graph(
      RealizedPipeline pipeline,
      PrintStream dotOut,
      String graphType,
      String name,
      List<String> other
  ) {
    dotOut.format("%s \"%s\" {%n", graphType, name);

    for (String otherString : other) {
      dotOut.println(otherString + ";");
    }

    for (RealizedStep step : pipeline.getRealizedSteps()) {
      dotOut.format("\"%s\"[label=\"%s\"];%n", step.getName(), step.getName());
    }

    for (RealizedStep step : pipeline.getRealizedSteps()) {
      for (RealizedStep dependency : step.getDependencies()) {
        String from = dependency.getName();
        String to = step.getName();
        String extra = ""; // todo find input name from AST
        dotOut.format("\"%s\" -> \"%s\"[%s];%n", from, to, extra);
      }
    }

    dotOut.println("}");
  }

}
