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

import static nz.org.riskscape.engine.SRIDSet.*;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.NoSuchElementException;

import javax.xml.stream.XMLStreamException;

import org.geotools.feature.FeatureIterator;
import org.geotools.kml.v22.KML;
import org.geotools.kml.v22.KMLConfiguration;
import org.geotools.xsd.PullParser;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.geotools.api.feature.simple.SimpleFeature;
import org.geotools.api.feature.simple.SimpleFeatureType;
import org.xml.sax.SAXException;

import com.google.common.collect.ImmutableMap;

import lombok.Getter;
import nz.org.riskscape.engine.Engine;
import nz.org.riskscape.engine.RiskscapeException;
import nz.org.riskscape.engine.RiskscapeIOException;
import nz.org.riskscape.engine.data.Bookmark;
import nz.org.riskscape.engine.data.BookmarkProblems;
import nz.org.riskscape.engine.data.relation.RelationBookmarkParams;
import nz.org.riskscape.engine.data.relation.RelationBookmarkResolver;
import nz.org.riskscape.engine.relation.BaseRelation;
import nz.org.riskscape.engine.relation.FeatureSourceRelation;
import nz.org.riskscape.engine.relation.FeatureSourceTupleIterator;
import nz.org.riskscape.engine.relation.Relation;
import nz.org.riskscape.engine.relation.SpatialMetadata;
import nz.org.riskscape.engine.relation.TupleIterator;
import nz.org.riskscape.engine.types.Referenced;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Struct.StructBuilder;
import nz.org.riskscape.engine.types.Struct.StructMember;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.problem.ResultOrProblems;

public class KmlResolver extends RelationBookmarkResolver<RelationBookmarkParams> {

  public static final String GEOM_KEY = "Geometry";

  public static class KmlRelation extends BaseRelation {

    private final Bookmark bookmark;
    private final Path kmlFile;
    private final GeometryFactory crs84Factory;

    public KmlRelation(Struct type, Bookmark bookmark, Path kmlFile, GeometryFactory crs84Factory) {
      super(type, null, new SpatialMetadata(EPSG4326_LONLAT, type.getEntry(GEOM_KEY)));
      this.bookmark = bookmark;
      this.kmlFile = kmlFile;
      this.crs84Factory = crs84Factory;
    }

    public KmlRelation(Fields fields, Bookmark bookmark, Path kmlFile, GeometryFactory crs84Factory) {
      super(fields);
      this.bookmark = bookmark;
      this.kmlFile = kmlFile;
      this.crs84Factory = crs84Factory;
    }

    @Override
    protected TupleIterator rawIterator() {
      KmlFeatureReader reader;
      try {
        reader = new KmlFeatureReader(bookmark, Files.newInputStream(kmlFile), crs84Factory);
      } catch (Exception e) {
        throw new RiskscapeException(Problems.caught(e));
      }
      return new FeatureSourceTupleIterator(reader, getRawType());
    }

    @Override
    protected BaseRelation clone(Fields fields) {
      return new KmlRelation(fields, bookmark, kmlFile, crs84Factory);
    }

    @Override
    public String getSourceInformation() {
      return kmlFile.toString();
    }
  }

  /**
   * Reads features from KML
   */
  private static class KmlFeatureReader implements FeatureIterator<SimpleFeature> {

    private final Bookmark bookmark;
    private final SimpleFeatureType type;
    private final PullParser parser;
    private final InputStream is;

    private SimpleFeature next = null;

    KmlFeatureReader(Bookmark bookmark, InputStream is, GeometryFactory geometryFactory) {
      this.bookmark = bookmark;
      this.is = is;
      // we make the config and then inject the CRS84 geometry factory into it. This way we don't
      // need to double handle the geometries, they will start life with the right SRID set.
      KMLConfiguration config = new KMLConfiguration();
      config.getContext().registerComponentInstance(geometryFactory);
      parser = new PullParser(config, is, KML.Placemark);
      getNext();
      if (next != null) {
        type = next.getType();
      } else {
        type = null;
      }
    }

    public SimpleFeatureType getFeatureType() {
      return type;
    }

    /**
     * Grab the next feature.
     *
     * @return feature
     * @throws NoSuchElementException if there is no next feature to get
     */
    @Override
    public SimpleFeature next() throws NoSuchElementException {
      if (!hasNext()) {
        throw new NoSuchElementException();
      }
      SimpleFeature toReturn = this.next;
      getNext();
      return toReturn;
    }

    private void getNext() {
      try {
        next = (SimpleFeature) parser.parse();
      } catch (XMLStreamException | SAXException e) {
        // make a problem for invalid kml. we include the caught exception in the hope that it has
        // a message of some use to the user. Like what part of the content is invalid.
        Problem invalidProblem = BookmarkProblems.get().invalidContent("KML").withException(e);
        if (next != null) {
          // it next is not null then we started processing the data (pipeline). In this case we want
          // to add the bookmark as context to the problem because saying problems with KML isn't that
          // useful if there are multiple KML inputs.
          throw new RiskscapeException(Problems.foundWith(bookmark, invalidProblem));
        }
        // if next is null then we in the process of resolving the bookmark. We don't want to add the
        // bookmark as context in this case because that will be done again by standard bookmark code.
        throw new RiskscapeException(invalidProblem);
      } catch (IOException e) {
        throw new RiskscapeIOException(Problems.caught(e));
      }
    }

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

    @Override
    public void close() {
      try {
        is.close();
      } catch (IOException e) {
        throw new RiskscapeIOException(Problems.caught(e));
      }
    }
  }

  @Getter
  private Map<String, String> extensionsToFormats = ImmutableMap.of(
      "kml", "kml"
  );

  public KmlResolver(Engine engine) {
    super(engine);
  }

  @Override
  protected ResultOrProblems<Relation> createRawRelationFromBookmark(RelationBookmarkParams params) {
    ResultOrProblems<Path> kmlFile = getBookmarkedPathOr(params);
    if (kmlFile.hasProblems()) {
      return ResultOrProblems.failed(kmlFile.getProblems());
    }
    GeometryFactory crs84Factory = params.getProject().getSridSet().getGeometryFactory(EPSG4326_LONLAT);

    SimpleFeatureType featureType = null;
    // we use the feature reader to get the simple feature type. then we close the reader. the relation
    // will open a new one when it needs to.
    try (KmlFeatureReader reader = new KmlFeatureReader(params.bookmark, Files.newInputStream(kmlFile.get()),
        crs84Factory)) {
      if (reader.type == null) {
        // this happens when the input file is xml (it can be parsed) but isn't kml.
        // it would also occur for kml with no placemarks but what's the point of that.
        return ResultOrProblems.failed(BookmarkProblems.get().invalidContent("KML"));
      }
      featureType = reader.type;
    } catch (RiskscapeException e) {
      return ResultOrProblems.failed(e.getProblem());
    } catch (IOException e) {
      return ResultOrProblems.failed(Problems.caught(e));
    }

    Struct type = FeatureSourceRelation.fromFeatureType(featureType, null, false);
    // The type produced from the kml might not be that useful to RiskScape as is.
    // The the geometry member may not be first and may be pre-ceeded by other
    // geometries that exist in the KML schema (aka Lookat and Region).
    // So we'll re-construct the struct to ensure the first member is the geometry.
    StructBuilder builder = Struct.builder();
    StructMember geomMember = type.getEntry("Geometry");
    builder.add(geomMember.getKey(), Referenced.of(geomMember.getType(), EPSG4326_LONLAT));
    for (StructMember member : type.getMembers()) {
      if (member == geomMember) {
        continue;
      }
      Type memberType = member.getType();
      if (Geometry.class.isAssignableFrom(memberType.internalType())) {
        // This assigns reference info to the LookAt and Region geometries that are included in all
        // KML features.
        // TODO consider whether RiskScape should include these geometries. It may be more helpfult to
        // users if they are discarded.
        memberType = Referenced.of(memberType, EPSG4326_LONLAT);
      }
      builder.add(member.getKey(), memberType);
    }

    return ResultOrProblems.of(new KmlRelation(builder.build(), params.bookmark, kmlFile.get(), crs84Factory));
  }

}
