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

import org.geotools.api.data.FeatureSource;
import org.geotools.api.data.FeatureStore;
import org.geotools.api.data.Query;
import org.geotools.api.data.ServiceInfo;
import org.geotools.api.data.SimpleFeatureSource;
import org.geotools.api.filter.Filter;
import org.geotools.api.referencing.crs.CoordinateReferenceSystem;

import org.locationtech.jts.geom.Geometry;

import lombok.NonNull;
import nz.org.riskscape.engine.SRIDSet;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.types.Struct;

/**
 * Like a {@link FeatureSourceRelation}, useful for working with {@link FeatureStore}s that are backed by WFS.
 *
 * This {@link Relation} is like the {@link FeatureSourceRelation},except it will split up requests for features in to
 * pages to work around potential max-feature limits on the underlying datastore (only seen in WFS so far).
 * While {@link FeatureSource}s are usually implemented to stream efficiently, which means we should
 * not have to page, we might silently hit a feature limit such as the one Geoserver and the LINZ data service impose.
 */
public class PagingFeatureSourceRelation extends BaseRelation {

  private final SimpleFeatureSource featureSource;
  private final int pageSize;
  private final SRIDSet sridSet;
  private final Filter filter;
  private final CoordinateReferenceSystem crs;

  /**
   * Create a {@link PagingFeatureSourceRelation}
   * @param type the type as per {@link Relation#getType()}
   * @param featureSource the {@link FeatureSource} to page from
   * @param sridSet used for constructing {@link Geometry}s with the correct srid
   * @param filter a filter to apply to features in the {@link FeatureSource}
   * @param pageSize the size of each page.  Should be less than the underlying data stores feature limit.
   */
  public PagingFeatureSourceRelation(@NonNull Struct type,
      @NonNull SimpleFeatureSource featureSource, @NonNull SRIDSet sridSet, @NonNull Filter filter,
      @NonNull CoordinateReferenceSystem crs, int pageSize) {
    super(type, null, FeatureSourceRelation.spatialMetadataFromSchema(type, featureSource.getSchema(), crs));
    this.filter = filter;
    this.featureSource = featureSource;
    this.sridSet = sridSet;
    this.pageSize = pageSize;
    this.crs = crs;
  }

  protected PagingFeatureSourceRelation(
      BaseRelation.Fields fields,
      @NonNull SimpleFeatureSource featureSource, @NonNull SRIDSet sridSet, @NonNull Filter filter,
      @NonNull CoordinateReferenceSystem crs, int pageSize) {
    super(fields);
    this.filter = filter;
    this.featureSource = featureSource;
    this.sridSet = sridSet;
    this.crs = crs;
    this.pageSize = pageSize;
  }

  @Override
  public TupleIterator rawIterator() {
    return new TupleIterator() {

      private Tuple tuple;
      private int tuplePeekCount = 0;
      private boolean exhausted = false;
      private TupleIterator currentIterator;

      private Tuple peek() {
        // we keep looking until we find a valid tuple or are exhausted. this is because the the
        // iterator may throw InvalidTupleException particulary if geometries are null.
        while (tuple == null && !exhausted) {

          if (currentIterator == null || !currentIterator.hasNext()) {
            currentIterator = nextIterator();
          }

          if (!currentIterator.hasNext()) {
            exhausted = true;
          } else {
            try {
              tuple = currentIterator.next();
            } catch (InvalidTupleException e) {
              skipOrThrow(e);
            }
            tuplePeekCount++;
          }

        }
        return tuple;
      }

      private TupleIterator nextIterator() {
        int startIndex = tuplePeekCount;
        int maxFeatures = startIndex + pageSize;

        if (tuplePeekCount % pageSize != 0) {
          // there's not point creating a new one, as the current one didn't fill a page, so it must
          // be the last one
          return currentIterator;
        }

        Query query = new Query();
        // This API has "evolved" and so the naming is a bit rubbish. startIndex is akin to offset in SQL
        // and maxFeatures should probably be called endIndex, and in SQL is equivalent to offset + limit
        // However, I wouldn't be surprised if this was only true in WFS - so we'll need to be careful if using other
        // datasources that support paging
        query.setStartIndex(startIndex);
        query.setMaxFeatures(maxFeatures);
        query.setFilter(filter);

        return FeatureSourceTupleIterator.fromQuery(featureSource, query, sridSet, getRawType(), crs);
      }

      @Override
      public Tuple next() {
        Tuple peeked = peek();

        if (peeked == null) {
          throw new NoSuchElementException();
        }

        tuple = null;
        return peeked;
      }

      @Override
      public boolean hasNext() {
        return peek() != null;
      }
    };
  }

  @Override
  public String getSourceInformation() {
    ServiceInfo info = featureSource.getDataStore().getInfo();
    return info.getSource() == null ? info.getTitle() : info.getSource().toString();
  }

  @Override
  protected BaseRelation clone(Fields fields) {
    return new PagingFeatureSourceRelation(fields, featureSource, sridSet, filter, crs, pageSize);
  }

}
