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

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

import lombok.Getter;
import nz.org.riskscape.engine.IdentifiedCollection;
import nz.org.riskscape.engine.types.ancestor.AncestorRule;
import nz.org.riskscape.engine.types.ancestor.RSListAncestorRule;
import nz.org.riskscape.engine.types.ancestor.NothingAncestorRule;
import nz.org.riskscape.engine.types.ancestor.NullableAncestorRule;
import nz.org.riskscape.engine.types.ancestor.NumberTypeAncestorRule;
import nz.org.riskscape.engine.types.ancestor.ReferencedAncestorRule;
import nz.org.riskscape.engine.types.ancestor.StructMemberAncestorRule;
import nz.org.riskscape.engine.types.ancestor.WrappedTypeAncestorRule;
import nz.org.riskscape.engine.types.eqrule.EquivalenceRule;
import nz.org.riskscape.engine.types.eqrule.NumberToFloatingRule;
import nz.org.riskscape.engine.types.eqrule.NullableRule;
import nz.org.riskscape.engine.types.eqrule.PartialStructRule;
import nz.org.riskscape.engine.types.eqrule.SingleGeomToMultiGeomRule;
import nz.org.riskscape.engine.types.eqrule.SingleValueStructRule;
import nz.org.riskscape.engine.types.eqrule.StructGeometryRule;
import nz.org.riskscape.engine.types.varule.AnythingRule;
import nz.org.riskscape.engine.types.varule.ContainingTypeRule;
import nz.org.riskscape.engine.types.varule.EmptyListRule;
import nz.org.riskscape.engine.types.varule.EqualityRule;
import nz.org.riskscape.engine.types.varule.GeometryRule;
import nz.org.riskscape.engine.types.varule.LinkedTypeRule;
import nz.org.riskscape.engine.types.varule.LookupTableTypeRule;
import nz.org.riskscape.engine.types.varule.NothingRule;
import nz.org.riskscape.engine.types.varule.NullableTargetRule;
import nz.org.riskscape.engine.types.varule.SameRule;
import nz.org.riskscape.engine.types.varule.VarianceRule;
import nz.org.riskscape.engine.types.varule.WrappingTypesRule;

public class TypeRegistry extends IdentifiedCollection.Base<TypeInformation> {

  public static TypeRegistry withDefaults() {
    return addDefaults(new TypeRegistry());
  }

  public static TypeRegistry addDefaults(TypeRegistry registry) {
    registry.add(SimpleType.getTypeInformation(Integer.class));
    registry.add(SimpleType.getTypeInformation(Bool.class));
    registry.add(SimpleType.getTypeInformation(Date.class));
    registry.add(SimpleType.getTypeInformation(Text.class));
    registry.add(SimpleType.getTypeInformation(Floating.class));
    registry.add(SimpleType.getTypeInformation(Smallfloat.class));
    registry.add(SimpleType.getTypeInformation(Decimal.class));
    registry.add(SimpleType.getTypeInformation(Geom.class));
    registry.add(SimpleType.getTypeInformation(Anything.class));

    registry.add(SimpleType.getTypeInformation(GeomType.Line.class));
    registry.add(SimpleType.getTypeInformation(GeomType.Point.class));
    registry.add(SimpleType.getTypeInformation(GeomType.Polygon.class));

    registry.add(MultiGeom.MULTI_LINE_TYPE_INFORMATION);
    registry.add(MultiGeom.MULTI_POINT_TYPE_INFORMATION);
    registry.add(MultiGeom.MULTI_POLYGON_TYPE_INFORMATION);

    registry.add(Struct.TYPE_INFORMATION);
    registry.add(WithinSet.TYPE_INFORMATION);
    registry.add(WithinRange.TYPE_INFORMATION);
    registry.add(Enumeration.TYPE_INFORMATION);
    registry.add(Nullable.TYPE_INFORMATION);
    registry.add(RSList.TYPE_INFORMATION);
    registry.add(CoverageType.TYPE_INFORMATION);
    registry.add(RelationType.TYPE_INFORMATION);
    registry.add(LambdaType.TYPE_INFORMATION);

    addDefaultVarianceRules(registry);
    addDefaultAncestorRules(registry);

    registry.addEquivalenceRule(new NullableRule());
    registry.addEquivalenceRule(new NumberToFloatingRule());
    registry.addEquivalenceRule(new SingleValueStructRule());
    registry.addEquivalenceRule(new PartialStructRule());
    registry.addEquivalenceRule(new SingleGeomToMultiGeomRule());
    registry.addEquivalenceRule(new StructGeometryRule());

    return registry;
  }

  public static TypeRegistry addDefaultVarianceRules(TypeRegistry registry) {
    // Order is important here, in particular rules that detect invariance.
    // - NothingRule should be checked before NullableTargetRule
    // - NullableTargetRule should be checked before AnythingRule
    registry.addVarianceRule(new SameRule());
    registry.addVarianceRule(new NothingRule());
    // java rule comes after more special case nothing rule - nothing (Void) can be assigned to any nullable target
    registry.addVarianceRule(new JavaRule());
    registry.addVarianceRule(new NullableTargetRule());
    registry.addVarianceRule(new EmptyListRule());
    registry.addVarianceRule(new EqualityRule());
    registry.addVarianceRule(new AnythingRule());
    registry.addVarianceRule(new LinkedTypeRule());
    registry.addVarianceRule(new WrappingTypesRule());
    registry.addVarianceRule(new ContainingTypeRule());
    registry.addVarianceRule(new LookupTableTypeRule());
    registry.addVarianceRule(new GeometryRule());

    return registry;
  }

  public static TypeRegistry addDefaultAncestorRules(TypeRegistry registry) {
    // Order is important here, in particular rules that detect invariance.

    registry.addAncestorRule(new NothingAncestorRule());
    registry.addAncestorRule(new NullableAncestorRule());
    registry.addAncestorRule(new StructMemberAncestorRule());
    registry.addAncestorRule(new RSListAncestorRule());
    // needs to go above wrapped types
    registry.addAncestorRule(new ReferencedAncestorRule());
    registry.addAncestorRule(new WrappedTypeAncestorRule());
    registry.addAncestorRule(new NumberTypeAncestorRule());

    return registry;
  }

  // for now these are just lists stored here, but they should probably be IdentifiedCollections
  @Getter
  private final List<EquivalenceRule> equivalenceRules = new ArrayList<>();

  @Getter
  private final List<VarianceRule> varianceRules = new ArrayList<>();

  @Getter
  private final List<AncestorRule> ancestorRules = new ArrayList<>();

  public void addEquivalenceRule(EquivalenceRule rule) {
    this.equivalenceRules.add(rule);
  }

  public void addVarianceRule(VarianceRule rule) {
    this.varianceRules.add(rule);
  }

  private void addAncestorRule(AncestorRule rule) {
    this.ancestorRules.add(rule);
  }

  /**
   * Search for a {@link TypeInformation} object that reports the given type as its type class \
   * (via {@link TypeInformation#getTypeClass()}
   */
  public Optional<TypeInformation> findTypeInformation(Type someType) {
    return findTypeInformation(someType.getClass());
  }

  /**
   * Search for a {@link TypeInformation} object that reports the given class as its type class \
   * (via {@link TypeInformation#getTypeClass()}
   */
  public Optional<TypeInformation> findTypeInformation(Class<? extends Type> someTypeClass) {
    for (TypeInformation info : getAll()) {
      if (info.getTypeClass().equals(someTypeClass)) {
        return Optional.of(info);
      }
    }

    return Optional.empty();
  }

}
