/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.lucene.spatial.composite;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.lucene.document.Field;
import org.apache.lucene.search.DoubleValuesSource;
import org.apache.lucene.search.Query;
import org.apache.lucene.spatial.SpatialStrategy;
import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy;
import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.query.SpatialOperation;
import org.apache.lucene.spatial.query.UnsupportedSpatialOperation;
import org.apache.lucene.spatial.serialized.SerializedDVStrategy;
import org.apache.lucene.spatial.util.ShapeValuesPredicate;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Shape;

/**
 * A composite {@link SpatialStrategy} based on {@link RecursivePrefixTreeStrategy} (RPT) and {@link
 * SerializedDVStrategy} (SDV). RPT acts as an index to the precision available in SDV, and in some
 * circumstances can avoid geometry lookups based on where a cell is in relation to the query shape.
 * Currently the only predicate optimized like this is Intersects. All predicates are supported
 * except for the BBox* ones, and Disjoint.
 *
 * @lucene.experimental
 */
public class CompositeSpatialStrategy extends SpatialStrategy {

  // TODO support others? (BBox)
  private final RecursivePrefixTreeStrategy indexStrategy;

  /** Has the geometry. */
  // TODO support others?
  private final SerializedDVStrategy geometryStrategy;

  private boolean optimizePredicates = true;

  public CompositeSpatialStrategy(
      String fieldName,
      RecursivePrefixTreeStrategy indexStrategy,
      SerializedDVStrategy geometryStrategy) {
    super(indexStrategy.getSpatialContext(), fieldName); // field name; unused
    this.indexStrategy = indexStrategy;
    this.geometryStrategy = geometryStrategy;
  }

  public RecursivePrefixTreeStrategy getIndexStrategy() {
    return indexStrategy;
  }

  public SerializedDVStrategy getGeometryStrategy() {
    return geometryStrategy;
  }

  public boolean isOptimizePredicates() {
    return optimizePredicates;
  }

  /**
   * Set to false to NOT use optimized search predicates that avoid checking the geometry sometimes.
   * Only useful for benchmarking.
   */
  public void setOptimizePredicates(boolean optimizePredicates) {
    this.optimizePredicates = optimizePredicates;
  }

  @Override
  public Field[] createIndexableFields(Shape shape) {
    List<Field> fields = new ArrayList<>();
    Collections.addAll(fields, indexStrategy.createIndexableFields(shape));
    Collections.addAll(fields, geometryStrategy.createIndexableFields(shape));
    return fields.toArray(new Field[fields.size()]);
  }

  @Override
  public DoubleValuesSource makeDistanceValueSource(Point queryPoint, double multiplier) {
    // TODO consider indexing center-point in DV?  Guarantee contained by the shape, which could
    // then be used for
    // other purposes like faster WITHIN predicate?
    throw new UnsupportedOperationException();
  }

  @Override
  public Query makeQuery(SpatialArgs args) {
    final SpatialOperation pred = args.getOperation();

    if (pred == SpatialOperation.BBoxIntersects || pred == SpatialOperation.BBoxWithin) {
      throw new UnsupportedSpatialOperation(pred);
    }

    if (pred == SpatialOperation.IsDisjointTo) {
      //      final Query intersectQuery = makeQuery(new SpatialArgs(SpatialOperation.Intersects,
      // args.getShape()));
      //      DocValues.getDocsWithField(reader, geometryStrategy.getFieldName());
      // TODO resurrect Disjoint spatial query utility accepting a field name known to have
      // DocValues.
      // update class docs when it's added.
      throw new UnsupportedSpatialOperation(pred);
    }

    final ShapeValuesPredicate predicateValueSource =
        new ShapeValuesPredicate(geometryStrategy.makeShapeValueSource(), pred, args.getShape());
    // System.out.println("PredOpt: " + optimizePredicates);
    if (pred == SpatialOperation.Intersects && optimizePredicates) {
      // We have a smart Intersects impl

      final SpatialPrefixTree grid = indexStrategy.getGrid();
      final int detailLevel =
          grid.getLevelForDistance(args.resolveDistErr(ctx, 0.0)); // default to max precision
      return new IntersectsRPTVerifyQuery(
          args.getShape(),
          indexStrategy.getFieldName(),
          grid,
          detailLevel,
          indexStrategy.getPrefixGridScanLevel(),
          predicateValueSource);
    } else {
      // The general path; all index matches get verified

      SpatialArgs indexArgs;
      if (pred == SpatialOperation.Contains) {
        // note: we could map IsWithin as well but it's pretty darned slow since it touches all
        // world grids
        indexArgs = args;
      } else {
        // TODO add args.clone method with new predicate? Or simply make non-final?
        indexArgs = new SpatialArgs(SpatialOperation.Intersects, args.getShape());
        indexArgs.setDistErr(args.getDistErr());
        indexArgs.setDistErrPct(args.getDistErrPct());
      }

      if (indexArgs.getDistErr() == null && indexArgs.getDistErrPct() == null) {
        indexArgs.setDistErrPct(0.10);
      }

      final Query indexQuery = indexStrategy.makeQuery(indexArgs);
      return new CompositeVerifyQuery(indexQuery, predicateValueSource);
    }
  }
}
