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

import java.util.List;
import java.util.Optional;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.geotools.geometry.jts.ReferencedEnvelope;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;

import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.data.SelfDescribingScalarData;
import nz.org.riskscape.engine.pipeline.Realized;
import nz.org.riskscape.engine.pipeline.TupleInput;
import nz.org.riskscape.engine.projection.Projection;
import nz.org.riskscape.engine.restriction.Restriction;
import nz.org.riskscape.engine.types.Geom;
import nz.org.riskscape.engine.types.Referenced;
import nz.org.riskscape.engine.types.RelationType;
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.ProblemSink;
import nz.org.riskscape.problem.ResultOrProblems;


public interface Relation extends Realized, SelfDescribingScalarData {

  /**
   * @deprecated use {@link #getProducedType()} instead
   */
  @Deprecated
  default Struct getType() {
    return getProducedType();
  }

  @Override
  Struct getProducedType();

  TupleIterator iterator();

  ResultOrProblems<Relation> project(Projection projection);
  ResultOrProblems<Relation> restrict(Restriction restriction);

  default Stream<Tuple> stream() {
    TupleIterator iterator = iterator();
    Spliterator<Tuple> spliterator = Spliterators.spliteratorUnknownSize(
        iterator, Spliterator.IMMUTABLE | Spliterator.NONNULL);

    return StreamSupport.stream(spliterator, false);
  }
  /**
   * @return {@link SpatialMetadata} for this relation.  Should return {@link Optional#empty()} if not a spatial layer.
   */
  default Optional<SpatialMetadata> getSpatialMetadata() {
    return Optional.empty();
  }

  /**
   * @return a new relation that skips rows that are invalid for some reason
   * TODO consider whether we need some sort of 'cloneWithOptions' method, that returns a new relation if the options
   * are supported
   */
  Relation skipInvalid(ProblemSink sendProblemsTo);

  /**
   * @return true if invalid rows will be skipped for this relation
   */
  boolean hasSkipOnInvalid();

  String getSourceInformation();

  /**
   * @return a freshly computed ReferencedEnvelope for this relation which covers all of the geometry in the relation
   * identified by its {@link SpatialMetadata}, or empty if no {@link SpatialMetadata} or for an empty {@link Relation}.
   */
  default Optional<ReferencedEnvelope> calculateBounds() {
    return getSpatialMetadata().flatMap(sm -> {
      try (TupleIterator iterator = skipInvalid(ProblemSink.DEVNULL).iterator()) {

        if (!iterator.hasNext()) {
          return Optional.empty();
        }
        StructMember geomMember = sm.getGeometryStructMember();

        Envelope envelope = new Envelope();
        while (iterator.hasNext()) {
          Tuple tuple = iterator.next();
          Geometry geometry = tuple.fetch(geomMember);
          if (geometry != null) {
            envelope.expandToInclude(geometry.getEnvelopeInternal());
          }
        }

        return Optional.of(new ReferencedEnvelope(
            envelope.getMinX(),
            envelope.getMaxX(),
            envelope.getMinY(),
            envelope.getMaxY(),
            sm.getCrs()));
      }
    });
  }

  /**
   * @return The maximum number of {@link Tuple}s this Relation will yield via an iterator it produces.
   * If the underlying data set is not ordered, this could be a completely arbitrary set of results.
   */
  long getLimit();

  /**
   * @return The number of {@link Tuple}s that will be skipped before an iterator starts yielding results.  If
   * the underlying data set is not ordered, this could be a completely arbitrary set of results.
   */
  long getOffset();

  /**
   * Convenience version of {@link #limitAndOffset(long, long)} with offset set to the Relation's current offset
   */
  default Relation limit(long newLimit) {
    return limitAndOffset(newLimit, getOffset());
  }

  /**
   * Convenience version of {@link #limitAndOffset(long, long)} with limit set to the Relation's current limit
   */
  default Relation offset(long newOffset) {
    return limitAndOffset(getLimit(), newOffset);
  }

  /**
   * Returns a new relation that yields a subset of tuples this relation would normally return.  This should apply
   * after any filtering that's applied to the relation, even if those filters are applied after a limit and offset.
   *
   * @return a new Relation with the limit and offset applied
   */
  Relation limitAndOffset(long newLimit, long newOffset);

  @Override
  default Type getScalarDataType() {
    RelationType type = new RelationType(getType());

    // here we see if we have a single referenced geometry in our type, and if we do, we wrap the relation type in that
    // same referenced type, which allows a certain amount of introspection about the layer, a bit like spatial
    // metadata, but not.
    //
    // This is mostly here because there's a wizard feature that's relying on it (getting bounds from the referenced
    // info to optimize a filter) which could have been done using the existing SpatialMetadata API on Relation, but we
    // have discussed dropping spatial metadata from relation so this is possibly a move in the right direction,
    // although I'm not sure it's 100% how we want it like this - see
    // https://gitlab.catalyst.net.nz/riskscape/riskscape/-/issues/141 for more information
    List<StructMember> geomMembers = getType().getMembers().stream()
    .filter(m -> m.getType().findAllowNull(Geom.class).isPresent())
    .collect(Collectors.toList());

    if (geomMembers.size() == 1) {
      Type geomMember = geomMembers.get(0).getType();
      return geomMember.findAllowNull(Referenced.class)
          .map(r -> r.wrapNullable(type))
          .orElse(type);
    } else {
      return type;
    }
  }

  /**
   * Get a *rough* count of the *untransformed* tuples in the relation. This may include tuples that are skipped
   * because they are invalid or removed by a `filter` bookmark parameter. A rule of thumb is this should match
   * the number of tuples produced by {@link BaseRelation#rawIterator()}.
   *
   * @return the number of tuples present in the underlying data (if easily known).
   */
  default Optional<Long> size() {
    return Optional.empty();
  }

  default TupleInput toTupleInput() {
    return TupleInput.fromIterator(getType(), iterator(), size());
  };
}
