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

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.problem.GeneralProblems;
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.types.TypeProblems;
import nz.org.riskscape.engine.typeset.TypeRules;
import nz.org.riskscape.engine.typeset.TypeSet;
import nz.org.riskscape.problem.Problem;

public class PartialStructRule implements EquivalenceRule {

  @Override
  public Optional<Coercer> getCoercer(TypeRules typeRules, Type sourceType, Type targetType) {
    Struct targetStruct = targetType.find(Struct.class).orElse(null);
    Struct sourceStruct = sourceType.find(Struct.class).orElse(null);

    if (targetStruct != null && sourceStruct != null) {
      @SuppressWarnings("unchecked")
      Optional<Coercer>[] coercers = new Optional[targetStruct.size()];
      int[] sourceIndices = new int[coercers.length];

      List<StructMember> targetMembers = targetStruct.getMembers();

      typeRules.debug("Checking members {} in source...", targetStruct.getMemberKeys());

      for (int idx = 0; idx < sourceIndices.length; idx++) {
        StructMember targetMember = targetMembers.get(idx);
        StructMember sourceMember = sourceStruct.getEntry(targetMember.getKey());
        sourceIndices[idx] = sourceMember == null ? -1 : sourceMember.getIndex();

        if (sourceMember == null) {
          if (targetMember.getType().isNullable()) {
            typeRules.debug("  member {} is missing from source, but target is nullable, can be ignored", targetMember);
            coercers[idx] = Optional.empty();
            continue;
          } else {
            typeRules.debug("  member {} is missing from source, no safe coercion possible", targetMember);
            return Optional.empty();
          }
        }

        if (typeRules.isAssignable(sourceMember.getType(), targetMember.getType())) {
          typeRules.debug("  {} is directly assignable to {}", sourceMember.getType(), targetMember);
          coercers[idx] = Optional.empty();
        } else {
          Optional<Coercer> memberCoercer =
              typeRules.findEquivalenceCoercer(sourceMember.getType(), targetMember.getType());

          if (memberCoercer.isPresent()) {
            typeRules.debug("  {} is assignable to {} with coercion", sourceMember.getType(), targetMember);
            coercers[idx] = memberCoercer;
          } else {
            typeRules.debug("  {} is not assignable to {} with coercion, no safe coercion possible",
                sourceMember.getType(), targetMember);
            // no equivalence coercer - give up
            return Optional.empty();
          }
        }
      }

      // ok, all target members are present, assignable, or safely coerce-able - we can coerce this safely
      return Optional.of(Coercer.build(sourceType, targetType, srcValue -> {
        Tuple targetTuple = new Tuple(targetStruct);
        Tuple sourceTuple = (Tuple) srcValue;

        for (int idx = 0; idx < sourceIndices.length; idx++) {
          // -1 means it's optional and not present in the source - leave it null
          if (sourceIndices[idx] != -1) {
            Object rawValue = sourceTuple.fetch(sourceIndices[idx]);
            targetTuple.set(idx, coercers[idx].map(c -> c.apply(rawValue)).orElse(rawValue));
          }
        }

        return targetTuple;
      }));

    } else {
      return Optional.empty();
    }
  }

  /**
   * Drills down into why a struct mismatch occurred. As the user data can often
   * contain *a lot* of struct members, it helps to pinpoint which struct members
   * in particular we didn't like.
   *
   * @param given    the struct data provided by the user
   * @param expected what was expected (i.e. by a function)
   * @return a list of Problems that describe the mismatch
   */
  public static List<Problem> describeMismatch(TypeSet typeset, Struct given, Struct expected) {
    List<Problem> problems = new ArrayList<>();
    for (StructMember member : expected.getMembers()) {
      if (given.hasMember(member.getKey())) {
        // attribute is present, check if it's a compatible type
        Type givenType = given.getEntry(member.getKey()).getType();
        if (!typeset.isAssignable(givenType, member.getType())
            && !typeset.findEquivalenceCoercer(givenType, member.getType()).isPresent()) {
          problems.add(TypeProblems.get().mismatch(member, member.getType(), givenType));
        }
      } else if (!member.getType().isNullable()) {
        problems.add(GeneralProblems.get().required(member));
      }
    }
    return problems;
  }
}
