/*
 * 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.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import org.apache.lucene.index.DocValuesType;
import org.apache.lucene.search.DoubleValuesSource;
import org.apache.lucene.spatial.bbox.BBoxOverlapRatioValueSource;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.util.ShapeAreaValueSource;
import org.apache.solr.common.SolrException;
import org.apache.solr.legacy.BBoxStrategy;
import org.apache.solr.legacy.LegacyFieldType;
import org.apache.solr.search.QParser;
import org.locationtech.spatial4j.shape.Rectangle;

public class BBoxField extends AbstractSpatialFieldType<BBoxStrategy> implements SchemaAware {
  private static final String PARAM_QUERY_TARGET_PROPORTION = "queryTargetProportion";
  private static final String PARAM_MIN_SIDE_LENGTH = "minSideLength";

  //score modes:
  private static final String OVERLAP_RATIO = "overlapRatio";
  private static final String AREA = "area";
  private static final String AREA2D = "area2D";

  private String numberTypeName;//required
  private String booleanTypeName = "boolean";
  private boolean storeSubFields = false;

  private IndexSchema schema;

  public BBoxField() {
    super(new HashSet<>(Arrays.asList(OVERLAP_RATIO, AREA, AREA2D)));
  }

  @Override
  protected void init(IndexSchema schema, Map<String, String> args) {
    super.init(schema, args);

    String v = args.remove("numberType");
    if (v == null) {
      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "The field type: " + typeName
          + " must specify the numberType attribute.");
    }
    numberTypeName = v;

    v = args.remove("booleanType");
    if (v != null) {
      booleanTypeName = v;
    }
    
    v = args.remove("storeSubFields");
    if (v != null) {
      storeSubFields = Boolean.valueOf(v);
    }
  }

  @Override
  public void inform(IndexSchema schema) {
    this.schema = schema;
    FieldType numberType = schema.getFieldTypeByName(numberTypeName);
    FieldType booleanType = schema.getFieldTypeByName(booleanTypeName);

    if (numberType == null) {
      throw new RuntimeException("Cannot find number fieldType: " + numberTypeName);
    }
    if (booleanType == null) {
      throw new RuntimeException("Cannot find boolean fieldType: " + booleanTypeName);
    }
    if (!(booleanType instanceof BoolField)) {
      throw new RuntimeException("Must be a BoolField: " + booleanType);
    }
    if (numberType.getNumberType() != NumberType.DOUBLE) {
      throw new RuntimeException("Must be Double number type: " + numberType);
    }

    //note: this only works for explicit fields, not dynamic fields
    List<SchemaField> fields = new ArrayList<>(schema.getFields().values());//copy, because we modify during iteration
    for (SchemaField sf : fields) {
      if (sf.getType() == this) {
        String name = sf.getName();
        registerSubFields(schema, name, numberType, booleanType);
      }
    }
  }

  private void registerSubFields(IndexSchema schema, String name, FieldType numberType, FieldType booleanType) {
    register(schema, name + BBoxStrategy.SUFFIX_MINX, numberType);
    register(schema, name + BBoxStrategy.SUFFIX_MAXX, numberType);
    register(schema, name + BBoxStrategy.SUFFIX_MINY, numberType);
    register(schema, name + BBoxStrategy.SUFFIX_MAXY, numberType);
    register(schema, name + BBoxStrategy.SUFFIX_XDL, booleanType);
  }

  // note: Registering the field is probably optional; it makes it show up in the schema browser and may have other
  //  benefits.
  private void register(IndexSchema schema, String name, FieldType fieldType) {
    int props = fieldType.properties;
    if(storeSubFields) {
      props |= STORED;
    }
    else {
      props &= ~STORED;
    }
    SchemaField sf = new SchemaField(name, fieldType, props, null);
    schema.getFields().put(sf.getName(), sf);
  }

  @Override
  protected BBoxStrategy newSpatialStrategy(String fieldName) {
    //if it's a dynamic field, we register the sub-fields now.
    FieldType numberType = schema.getFieldTypeByName(numberTypeName);
    FieldType booleanType = schema.getFieldTypeByName(booleanTypeName);
    if (schema.isDynamicField(fieldName)) {
      registerSubFields(schema, fieldName, numberType, booleanType);
    }

    //Solr's FieldType ought to expose Lucene FieldType. Instead as a hack we create a Field with a dummy value.
    final SchemaField solrNumField = new SchemaField("_", numberType);//dummy temp
    org.apache.lucene.document.FieldType luceneType =
        (org.apache.lucene.document.FieldType) solrNumField.createField(0.0).fieldType();
    if ( ! (luceneType instanceof LegacyFieldType)) {
      luceneType = new org.apache.lucene.document.FieldType(luceneType);
    }
    luceneType.setStored(storeSubFields);
    
    //and annoyingly this Field isn't going to have a docValues format because Solr uses a separate Field for that
    if (solrNumField.hasDocValues()) {
      if (luceneType instanceof LegacyFieldType) {
        luceneType = new LegacyFieldType((LegacyFieldType)luceneType);
      } else {
        luceneType = new org.apache.lucene.document.FieldType(luceneType);
      }
      luceneType.setDocValuesType(DocValuesType.NUMERIC);
    }
    return new BBoxStrategy(ctx, fieldName, luceneType);
  }

  @Override
  protected DoubleValuesSource getValueSourceFromSpatialArgs(QParser parser, SchemaField field, SpatialArgs spatialArgs, String scoreParam, BBoxStrategy strategy) {
    if (scoreParam == null) {
      return null;
    }

    switch (scoreParam) {
      //TODO move these to superclass after LUCENE-5804 ?
      case OVERLAP_RATIO:
        double queryTargetProportion = 0.25;//Suggested default; weights towards target area

        String v = parser.getParam(PARAM_QUERY_TARGET_PROPORTION);
        if (v != null)
          queryTargetProportion = Double.parseDouble(v);

        double minSideLength = 0.0;
        v = parser.getParam(PARAM_MIN_SIDE_LENGTH);
        if (v != null)
          minSideLength = Double.parseDouble(v);

        return new BBoxOverlapRatioValueSource(
            strategy.makeShapeValueSource(), ctx.isGeo(),
            (Rectangle) spatialArgs.getShape(),
            queryTargetProportion, minSideLength);

      case AREA:
        return new ShapeAreaValueSource(strategy.makeShapeValueSource(), ctx, ctx.isGeo(),
            distanceUnits.multiplierFromDegreesToThisUnit() * distanceUnits.multiplierFromDegreesToThisUnit());

      case AREA2D:
        return new ShapeAreaValueSource(strategy.makeShapeValueSource(), ctx, false,
            distanceUnits.multiplierFromDegreesToThisUnit() * distanceUnits.multiplierFromDegreesToThisUnit());

      default:
        return super.getValueSourceFromSpatialArgs(parser, field, spatialArgs, scoreParam, strategy);
    }
  }
}