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

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.types.Anything;
import nz.org.riskscape.engine.types.Nullable;
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.engine.typeset.TypeSet;
import nz.org.riskscape.problem.ResultOrProblems;

/**
 * Takes tuples that potentially differ in type from two or more sources, and
 * reprojects the tuples to a common type
 *
 * TODO we can come back and rework this to use the ancestor type conversion
 */
@RequiredArgsConstructor
public class UnionProjector implements Projector {

  /**
   * Takes the union of two structs. For shared attributes, the common ancestor type is used.
   * Attributes that are only in one struct become nullable. Shared nested structs are recursed, so
   * that they end up with a shared set of attributes (rather than becoming the {@link Anything} type.
   *
   * @return A Struct that is the union of the two structs' members
   */
  public static Struct unionOf(TypeSet typeSet, Struct a, Struct b) {
    Struct combined = Struct.EMPTY_STRUCT;

    // go through a's members and use the common type (if any) between the 2 structs
    for (StructMember member : a.getMembers()) {
      combined = combined.add(member.getKey(), combinedType(typeSet, member, b));
    }
    // add in any remaining other members in b (as nullable)
    for (StructMember member : b.getMembers()) {
      if (!combined.hasMember(member.getKey())) {
        combined = combined.add(member.getKey(), combinedType(typeSet, member, a));
      }
    }
    return combined;
  }

  /**
   * @return a new UnionProjector built from the given list of input types
   */
  public static ResultOrProblems<? extends Projector> realize(TypeSet typeSet, List<Struct> inputTypes) {
    Struct projectedStruct = inputTypes.get(0);
    for (int i = 1; i < inputTypes.size(); i++) {
      Struct struct = inputTypes.get(i);
      projectedStruct = unionOf(typeSet, projectedStruct, struct);
    }
    return ResultOrProblems.of(new UnionProjector(inputTypes, projectedStruct));
  }

  /**
   * Convenience API to build a UnionProjector from the given input types.
   */
  public static ResultOrProblems<? extends Projector> realize(TypeSet typeSet, Struct... inputTypes) {
    return realize(typeSet, Arrays.asList(inputTypes));
  }

  private static Type combinedType(TypeSet typeSet, StructMember member, Struct mergeWith) {
    if (!mergeWith.hasMember(member.getKey())) {
      // doesn't exist on one side, so it needs to be nullable
      return Nullable.of(member.getType());
    } else {
      // if both structs have the same attribute, compute the common ancestor
      Type thisType = member.getType();
      Optional<Struct> thisStruct = thisType.findAllowNull(Struct.class);
      Type otherType = mergeWith.getEntry(member.getKey()).getType();
      Optional<Struct> otherStruct = otherType.findAllowNull(Struct.class);

      // recurse so we compute a union of struct members too. E.g. if the 'exposure'
      // struct is present for both inputs, we create a union of the 2 structs
      if (thisStruct.isPresent() && otherStruct.isPresent()) {
        return Nullable.ifTrue(Nullable.any(thisType, otherType),
            unionOf(typeSet, thisStruct.get(), otherStruct.get()));
      } else {
        return typeSet.computeAncestorNoConversion(thisType, otherType);
      }
    }
  }

  @Getter
  private final List<Struct> sourceTypes;

  @Getter
  private final Struct producedType;

  private Object fetchOrNull(Tuple t, String attribute) {
    if (t.getStruct().hasMember(attribute)) {
      return t.fetch(attribute);
    } else {
      return null;
    }
  }

  private Tuple adaptToType(Tuple input, Struct expectedType) {
    Tuple adapted = new Tuple(expectedType);
    for (StructMember member : expectedType.getMembers()) {
      Object value = fetchOrNull(input, member.getKey());

      // we have to recurse nested structs to null any extra members it gained from the union.
      // The common case is the exposure-layers have slightly different attributes, so
      // the union makes some attributes nullable. We need to be null these here
      if (value != null && member.getType().isA(Struct.class)) {
        value = adaptToType((Tuple) value, (Struct) member.getType());
      }
      adapted.set(member, value);
    }
    return adapted;
  }

  @Override
  public Tuple apply(Tuple t) {
    return adaptToType(t, producedType);
  }

  @Override
  public Struct getSourceType() {
    // TODO not really correct, but this method isn't really used
    return sourceTypes.get(0);
  }
}
