/*
 * 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.rl;

import java.util.LinkedList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

import com.google.common.collect.Lists;

import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.rl.DefaultExpressionRealizer.Realized;
import nz.org.riskscape.engine.types.Nullable;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Struct.StructBuilder;
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.rl.ast.Expression;
import nz.org.riskscape.rl.ast.ExpressionProblems;
import nz.org.riskscape.rl.ast.StructDeclaration;

/**
 * Takes care of realizing a StructDeclaration, as the code is complicated enough without being buried along
 * with all the other realization code.
 */
@RequiredArgsConstructor
class StructDeclarationRealizer {

  /**
   * Created during realization - codifies/memoizes logic for inserting each struct declaration member's
   * realized result in to the result tuple
   */
  @RequiredArgsConstructor @ToString
  private static final class RealizedStructMember {
    // the realized expression to evaluate
    final RealizedExpression realized;
    // whether the result is a struct that should be splatted in to the result tuple
    final boolean splat;
    // the location in the result array to put our result (or start copying results in the case of a splat)
    final int index;
  }

  /**
   * Created during realization - each element maps to a struct member of the result, which is potentially a different
   * size to the list of RealizedStructMembers we also construct
   *
   */
  // equality based on name to simplify member collisions with splats etc
  @RequiredArgsConstructor @EqualsAndHashCode(of = "name") @ToString
  private static final class StructMemberElement {
    // the name/key of the struct member
    final String name;
    // the resulting type
    final Type type;
    // the expression that yielded this member - used to support validation
    final StructDeclaration.Member expression;

    // true if this member was inserted as the result of a splat operation
    public boolean isSplat() {
      return expression.isSelectAll() || expression.isSelectAllOnReceiver();
    }
  }

  /**
   * The list of problems to add to when realization of something fails
   */
  private final List<Problem> problems;

  /**
   * The type we are realizing against, aka the scope
   */
  private final Type inputType;

  private final RealizationContext context;

  /**
   * A function pointer back to the default realizer's internal realizing method.
   */
  private final BiFunction<Expression, Expression, RealizedExpression> realizeCallback;

  // extracted in to a static method to avoid closing over fields by accident
  static RealizedExpression create(
      Type inputType,
      StructDeclaration sd,
      Struct resultType,
      List<RealizedStructMember> members
  ) {

    // throw away the linked list and replace it with an array
    RealizedStructMember[] memberArray = members.toArray(new RealizedStructMember[0]);

    BiFunction<Realized, Object, Object> function = (re, input) -> {
      Tuple result = new Tuple(resultType);

      // evaluate each child, copy results in to a new array,
      // following the 'instructions' baked in to each RealizedStructMember
      for (RealizedStructMember member : memberArray) {
        Object value = member.realized.evaluate(input);

        if (member.splat) {
          if (value != null) {
            // copy in all of the splat, regardless of any replacements - it's simpler to just over-write them later
            // (over-rides always come after)
            result.setAll(member.index, (Tuple) value);
          }
          // if it's null, there's no values to copy (the result type should have already commuted the nullability to
          // the splatted members)
        } else {
          result.set(member.index, value);
        }
      }

      return result;
    };

    List<RealizedExpression> dependencies =
        members.stream().map(rsm -> rsm.realized).collect(Collectors.toList());

    return new Realized(inputType, sd, resultType, function, dependencies, null);
  }

  /**
   * @return a realized expression for the given {@link StructDeclaration}.
   */
  RealizedExpression realize(StructDeclaration sd, Expression parent) {
    List<RealizedStructMember> members = new LinkedList<>();
    List<StructMemberElement> elements = new LinkedList<>();

    realizeStructMembers(sd, parent, members, elements);

    // create the return type
    StructBuilder builder = Struct.builder();
    for (StructMemberElement element : elements) {
      builder.add(element.name, element.type);
    }

    // we call build, not buildOr, as we expect there to be no collisions - we've already checked
    Struct resultType = context.normalizeStruct(builder.build());
    return create(inputType, sd, resultType, members);
  }

  /**
   * Populate `members` and `elements` with the results required to build an expression you can evaluate
   * @param members a list to populate with the 'instructions' for evaluating and setting the members of the struct
   * expression
   * @param elements a list to populate with the elements that constitute the resulting type.  If there are no splats
   * in the expression, then this will be one-to-one with elements in the members list.  If there are splats, then it's
   * likely there will be more elements than there will be members.
   */
  private void realizeStructMembers(
      StructDeclaration sd,
      Expression parent,
      List<RealizedStructMember> members,
      List<StructMemberElement> elements
  ) {
    for (StructDeclaration.Member member : sd.getMembers()) {
      // the place in the result tuple that this attribute will be set to (or start being set at for a splat)
      int index;
      // true if the child expression is to be splatted in to our tuple
      boolean splat = member.isSelectAll() || member.isSelectAllOnReceiver();
      RealizedExpression realized = realizeCallback.apply(member.getExpression(), sd);

      if (DefaultExpressionRealizer.anyFailed(realized)) {
        // there is no point continuing to build the expression for a struct member that has failed to realize, plus
        // some of the expectations around the expression might be iffy for a Realized that is failed, such as GL575.
        // Note that #isFailed is *not* actually part of the RealizedExpression's API - more an internal detail we use
        // in the realization routine to simplify the logic),
        continue;
      } else if (member.isSelectAllOnReceiver() && !member.isAnonymous()) {
        // error on {foo: {bar}.*} - it's pointless and ambigious - should it splat, or should it assign?
        problems.add(ExpressionProblems.get().pointlessSelectAllInStruct(member));
        // we assign the struct to the name, rather than doing a splat here, as that seems the sensible choice
        continue;
      }

      if (splat) {
        // splats are always appended
        index = elements.size();
        Struct splattedType = realized.getResultType().findAllowNull(Struct.class).orElse(null);
        if (splattedType == null) {
          problems.add(ExpressionProblems.get().selectAllRequiresAStruct(member, realized.getResultType()));
          continue;
        }

        // we need to commute the nullability of the struct to its members
        boolean splattedWasNullable = Nullable.is(realized.getResultType());

        // go through members of the splat to populate our resulting type and look for dupes
        for (StructMember splattedMember : splattedType.getMembers()) {
          StructMemberElement newElement =
              new StructMemberElement(
                splattedMember.getKey(),
                Nullable.ifTrue(splattedWasNullable, splattedMember.getType()),
                member
              );
          // make sure it's not a duplicate
          int existingIndex = elements.indexOf(newElement);

          if (existingIndex != -1) {
            StructMemberElement existingElement = elements.get(existingIndex);
            problems.add(ExpressionProblems.get()
                .canNotReplaceMember(splattedMember.getKey(), existingElement.expression, member));
          } else {
            elements.add(newElement);
          }
        }
      } else {
        // not a splat, we allow splatted elements to be replaced with explicitly stated ones, but not repeated
        // explicit members.  e.g. {foo} -> {*, foo} is ok, but {foo, bar, foo} is not
        final String name = member.getName().orElse(
            ExpressionRealizer.getImplicitName(
                context,
                realized,
                Lists.transform(elements, el -> el.name)
            )
          );

        StructMemberElement newElement = new StructMemberElement(name, realized.getResultType(), member);

        // lets see if there's already a thing with this name in the struct we are building
        int existingIndex = elements.indexOf(newElement);

        if (existingIndex == -1) {
          // doesn't exist, we can append a new element
          index = elements.size();
          elements.add(newElement);
        } else {
          // oh dear, we already have a member with this name, but it's ok as long as it was put there by a splat
          StructMemberElement existingElement = elements.get(existingIndex);

          if (existingElement.isSplat()) {
            // great, we just replace the splatted member with the explicitly set one
            elements.set(existingIndex, newElement);
            // the element is replaced, rather than appended, so the index is set to replace what we splatted
            // earlier
            index = existingIndex;
          } else {
            // error - can only replace splatted members
            problems.add(ExpressionProblems.get().canNotReplaceMember(name, existingElement.expression, member));
            continue;
          }
        }
      }

      members.add(new RealizedStructMember(realized, splat, index));
    }
  }
}
