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

import java.io.IOException;
import java.util.Iterator;

import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.ReaderUtil;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.spatial.ShapeValues;
import org.apache.lucene.spatial.ShapeValuesSource;
import org.apache.lucene.spatial.SpatialStrategy;
import org.apache.lucene.spatial.composite.CompositeSpatialStrategy;
import org.apache.lucene.spatial.serialized.SerializedDVStrategy;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.JSONResponseWriter;
import org.apache.solr.response.QueryResponseWriter;
import org.apache.solr.schema.AbstractSpatialFieldType;
import org.apache.solr.schema.SchemaField;
import org.locationtech.spatial4j.io.GeoJSONWriter;
import org.locationtech.spatial4j.io.ShapeWriter;
import org.locationtech.spatial4j.io.SupportedFormats;
import org.locationtech.spatial4j.shape.Shape;


/**
 * This DocumentTransformer will write a {@link Shape} to the SolrDocument using
 * the requested format.  Supported formats include:
 * <ul>
 *  <li>GeoJSON</li>
 *  <li>WKT</li>
 *  <li>Polyshape</li>
 * </ul>
 * For more information see: <a href="https://github.com/locationtech/spatial4j/blob/master/FORMATS.md">spatial4j/FORMATS.md</a>
 * 
 * The shape is either read from a stored field, or a ValueSource.
 * 
 * This transformer is useful when:
 * <ul>
 *  <li>You want to return a format different than the stored encoding (WKT vs GeoJSON)</li>
 *  <li>The {@link Shape} is stored in a {@link ValueSource}, not a stored field</li>
 *  <li>the value is not stored in a format the output understands (ie, raw GeoJSON)</li>
 * </ul>
 * 
 */
public class GeoTransformerFactory extends TransformerFactory
{ 
  @Override
  public DocTransformer create(String display, SolrParams params, SolrQueryRequest req) {

    String fname = params.get("f", display);
    if(fname.startsWith("[") && fname.endsWith("]")) {
      fname = display.substring(1,display.length()-1);
    }
    SchemaField sf = req.getSchema().getFieldOrNull(fname);
    if(sf==null) {
      throw new SolrException(ErrorCode.BAD_REQUEST, 
          this.getClass().getSimpleName() +" using unknown field: "+fname);
    }
    if(!(sf.getType() instanceof AbstractSpatialFieldType)) {
      throw new SolrException(ErrorCode.BAD_REQUEST, 
          "GeoTransformer requested non-spatial field: "+fname + " ("+sf.getType().getClass().getSimpleName()+")");
    }

    final GeoFieldUpdater updater = new GeoFieldUpdater();
    updater.field = fname;
    updater.display = display;
    updater.display_error = display+"_error"; 
        
    final ShapeValuesSource shapes;
    AbstractSpatialFieldType<?> sdv = (AbstractSpatialFieldType<?>)sf.getType();
    SpatialStrategy strategy = sdv.getStrategy(fname);
    if(strategy instanceof CompositeSpatialStrategy) {
      shapes = ((CompositeSpatialStrategy)strategy)
          .getGeometryStrategy().makeShapeValueSource();
    }
    else if(strategy instanceof SerializedDVStrategy) {
      shapes = ((SerializedDVStrategy)strategy)
          .makeShapeValueSource();
    }
    else
      shapes = null;
    
    
    String writerName = params.get("w", "GeoJSON");
    updater.formats = strategy.getSpatialContext().getFormats();
    updater.writer = updater.formats.getWriter(writerName);
    if(updater.writer==null) {
      StringBuilder str = new StringBuilder();
      str.append( "Unknown Spatial Writer: " ).append(writerName);
      str.append(" [");
      for(ShapeWriter w : updater.formats.getWriters()) {
        str.append(w.getFormatName()).append(' ');
      }
      str.append("]");
      throw new SolrException(ErrorCode.BAD_REQUEST, str.toString());
    }
    
    QueryResponseWriter qw = req.getCore().getQueryResponseWriter(req);
    updater.isJSON =
        (qw.getClass() == JSONResponseWriter.class) &&
        (updater.writer instanceof GeoJSONWriter);


    // Using ValueSource
    if(shapes!=null) {
      return new DocTransformer() {
        @Override
        public String getName() {
          return display;
        }

        @Override
        public void transform(SolrDocument doc, int docid) throws IOException {
          int leafOrd = ReaderUtil.subIndex(docid, context.getSearcher().getTopReaderContext().leaves());
          LeafReaderContext ctx = context.getSearcher().getTopReaderContext().leaves().get(leafOrd);
          ShapeValues values = shapes.getValues(ctx);
          int segmentDoc = docid - ctx.docBase;
          if (values.advanceExact(segmentDoc)) {
            updater.setValue(doc, values.value());
          }
        }
      };

    }
    
    // Using the raw stored values
    return new DocTransformer() {
      
      @Override
      public void transform(SolrDocument doc, int docid) throws IOException {
        Object val = doc.remove(updater.field);
        if(val!=null) {
          updater.setValue(doc, val);
        }
      }
      
      @Override
      public String getName() {
        return updater.display;
      }

      @Override
      public String[] getExtraRequestFields() {
        return new String[] {updater.field};
      }
    };
  }

}

class GeoFieldUpdater {
  String field;
  String display;
  String display_error;
  
  boolean isJSON;
  ShapeWriter writer;
  SupportedFormats formats;
  
  void addShape(SolrDocument doc, Shape shape) {
    if(isJSON) {
      doc.addField(display, new WriteableGeoJSON(shape, writer));
    }
    else {
      doc.addField(display, writer.toString(shape));
    }
  }
  
  void setValue(SolrDocument doc, Object val) {
    doc.remove(display);
    if(val != null) {
      if(val instanceof Iterable) {
        Iterator iter = ((Iterable)val).iterator();
        while(iter.hasNext()) {
          addValue(doc, iter.next());
        }
      }
      else {
        addValue(doc, val);
      }
    }
  }
    
  void addValue(SolrDocument doc, Object val) {
    if(val == null) {
      return;
    }
    
    if(val instanceof Shape) {
      addShape(doc, (Shape)val);
    }
    // Don't explode on 'InvalidShpae'
    else if( val instanceof Exception) {
      doc.setField( display_error, ((Exception)val).toString() );
    }
    else {
      // Use the stored value
      if(val instanceof IndexableField) {
        val = ((IndexableField)val).stringValue();
      }
      try {
        addShape(doc, formats.read(val.toString()));
      }
      catch(Exception ex) {
        doc.setField( display_error, ex.toString() );
      }
    }
  }
}

