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

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.spatial.ShapeValues;
import org.apache.lucene.spatial.ShapeValuesSource;
import org.apache.lucene.spatial.composite.CompositeSpatialStrategy;
import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy;
import org.apache.lucene.spatial.query.SpatialArgsParser;
import org.apache.lucene.spatial.serialized.SerializedDVStrategy;
import org.apache.solr.common.SolrException;
import org.apache.solr.core.SolrCore;
import org.apache.solr.request.SolrRequestInfo;
import org.apache.solr.search.SolrCache;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.jts.JtsGeometry;

/** A Solr Spatial FieldType based on {@link CompositeSpatialStrategy}.
 * @lucene.experimental */
public class RptWithGeometrySpatialField extends AbstractSpatialFieldType<CompositeSpatialStrategy> {

  public static final String DEFAULT_DIST_ERR_PCT = "0.15";

  private SpatialRecursivePrefixTreeFieldType rptFieldType;
  private SolrCore core;

  @Override
  protected void init(IndexSchema schema, Map<String, String> args) {
    Map<String, String> origArgs = new HashMap<>(args); // clone so we can feed it to an aggregated field type
    super.init(schema, origArgs);

    //TODO Move this check to a call from AbstractSpatialFieldType.createFields() so the type can declare
    // if it supports multi-valued or not. It's insufficient here; we can't see if you set multiValued on the field.
    if (isMultiValued()) {
      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Not capable of multiValued: " + getTypeName());
    }

    // Choose a better default distErrPct if not configured
    if (args.containsKey(SpatialArgsParser.DIST_ERR_PCT) == false) {
      args.put(SpatialArgsParser.DIST_ERR_PCT, DEFAULT_DIST_ERR_PCT);
    }

    rptFieldType = new SpatialRecursivePrefixTreeFieldType();
    rptFieldType.setTypeName(getTypeName());
    rptFieldType.properties = properties;
    rptFieldType.init(schema, args);

    rptFieldType.argsParser = argsParser = newSpatialArgsParser();
    this.ctx = rptFieldType.ctx;
    this.distanceUnits = rptFieldType.distanceUnits;
  }

  @Override
  protected CompositeSpatialStrategy newSpatialStrategy(String fieldName) {
    // We use the same field name for both sub-strategies knowing there will be no conflict for these two

    RecursivePrefixTreeStrategy rptStrategy = rptFieldType.newSpatialStrategy(fieldName);

    SerializedDVStrategy geomStrategy = new CachingSerializedDVStrategy(ctx, fieldName);

    return new CompositeSpatialStrategy(fieldName, rptStrategy, geomStrategy);
  }

  @Override
  public Analyzer getQueryAnalyzer() {
    return rptFieldType.getQueryAnalyzer();
  }

  @Override
  public Analyzer getIndexAnalyzer() {
    return rptFieldType.getIndexAnalyzer();
  }

  // Most of the complexity of this field type is below, which is all about caching the shapes in a SolrCache

  private static class CachingSerializedDVStrategy extends SerializedDVStrategy {
    public CachingSerializedDVStrategy(SpatialContext ctx, String fieldName) {
      super(ctx, fieldName);
    }

    @Override
    public ShapeValuesSource makeShapeValueSource() {
      return new CachingShapeValuesource(super.makeShapeValueSource(), getFieldName());
    }
  }

  private static class CachingShapeValuesource extends ShapeValuesSource {

    private final ShapeValuesSource targetValueSource;
    private final String fieldName;

    private CachingShapeValuesource(ShapeValuesSource targetValueSource, String fieldName) {
      this.targetValueSource = targetValueSource;
      this.fieldName = fieldName;
    }

    @Override
    public String toString() {
      return "cache(" + targetValueSource.toString() + ")";
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      CachingShapeValuesource that = (CachingShapeValuesource) o;

      if (!targetValueSource.equals(that.targetValueSource)) return false;
      return fieldName.equals(that.fieldName);

    }

    @Override
    public int hashCode() {
      int result = targetValueSource.hashCode();
      result = 31 * result + fieldName.hashCode();
      return result;
    }

    @Override
    public ShapeValues getValues(LeafReaderContext readerContext) throws IOException {
      final ShapeValues targetFuncValues = targetValueSource.getValues(readerContext);
      // The key is a pair of leaf reader with a docId relative to that reader. The value is a Map from field to Shape.
      @SuppressWarnings({"unchecked"})
      final SolrCache<PerSegCacheKey,Shape> cache =
          SolrRequestInfo.getRequestInfo().getReq().getSearcher().getCache(CACHE_KEY_PREFIX + fieldName);
      if (cache == null) {
        return targetFuncValues; // no caching; no configured cache
      }

      return new ShapeValues() {
        int docId = -1;

        @Override
        public Shape value() throws IOException {
          //lookup in cache
          IndexReader.CacheHelper cacheHelper = readerContext.reader().getCoreCacheHelper();
          if (cacheHelper == null) {
            throw new IllegalStateException("Leaf " + readerContext.reader() + " is not suited for caching");
          }
          PerSegCacheKey key = new PerSegCacheKey(cacheHelper.getKey(), docId);
          Shape shape = cache.computeIfAbsent(key, k -> {
            try {
              return targetFuncValues.value();
            } catch (IOException e) {
              return null;
            }
          });
          if (shape != null) {
            //optimize shape on a cache hit if possible. This must be thread-safe and it is.
            if (shape instanceof JtsGeometry) {
              ((JtsGeometry) shape).index(); // TODO would be nice if some day we didn't have to cast
            }
          }
          return shape;
        }

        @Override
        public boolean advanceExact(int doc) throws IOException {
          this.docId = doc;
          return targetFuncValues.advanceExact(doc);
        }

      };

    }

    @Override
    public boolean isCacheable(LeafReaderContext ctx) {
      return targetValueSource.isCacheable(ctx);
    }

  }

  public static final String CACHE_KEY_PREFIX = "perSegSpatialFieldCache_";//then field name

  // Used in a SolrCache for the key
  private static class PerSegCacheKey {
    final WeakReference<Object> segCoreKeyRef;
    final int docId;
    final int hashCode;//cached because we can't necessarily compute after construction

    private PerSegCacheKey(Object segCoreKey, int docId) {
      this.segCoreKeyRef = new WeakReference<>(segCoreKey);
      this.docId = docId;
      this.hashCode = segCoreKey.hashCode() * 31 + docId;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      PerSegCacheKey that = (PerSegCacheKey) o;

      if (docId != that.docId) return false;

      //compare by referent not reference
      Object segCoreKey = segCoreKeyRef.get();
      if (segCoreKey == null) {
        return false;
      }
      return segCoreKey.equals(that.segCoreKeyRef.get());
    }

    @Override
    public int hashCode() {
      return hashCode;
    }

    @Override
    public String toString() {
      return "Key{seg=" + segCoreKeyRef.get() + ", docId=" + docId + '}';
    }
  }
}