/*
 * Licensed to the Indoqa Software Design und Beratung GmbH (Indoqa) under
 * one or more contributor license agreements. See the NOTICE file distributed
 * with this work for additional information regarding copyright ownership.
 * Indoqa 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 com.indoqa.solr.spatial.clustering;

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

import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexableField;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.PluginInfo;
import org.apache.solr.handler.component.ResponseBuilder;
import org.apache.solr.handler.component.SearchComponent;
import org.apache.solr.util.plugin.PluginInfoInitialized;

import com.tomgibara.cluster.gvm.dbl.DblClusters;

public class SpatialClusteringComponent extends SearchComponent implements PluginInfoInitialized {

    public static final String PARAMETER_SPATIALCLUSTERING = "spatial-clustering";
    public static final String PARAMETER_SIZE = PARAMETER_SPATIALCLUSTERING + ".size";
    public static final String PARAMETER_MIN_RESULT_COUNT = PARAMETER_SPATIALCLUSTERING + ".min-result-count";
    public static final String PARAMETER_COMPACT_FORMAT = PARAMETER_SPATIALCLUSTERING + ".compact-format";

    private static final String PARAMETER_FIELD_NAME_ID = "fieldId";
    private static final String PARAMETER_FIELD_NAME_LON = "fieldLon";
    private static final String PARAMETER_FIELD_NAME_LAT = "fieldLat";
    private static final String PARAMETER_MAX_SIZE = "maxSize";

    private static final int DEFAULT_SIZE = 10;
    private static final int DEFAULT_MAX_SIZE = 1_000_000;
    private static final int MIN_SIZE = 1;

    private String fieldNameId;
    private String fieldNameLon;
    private String fieldNameLat;

    private int maxSize;

    private Set<String> fields;

    private static Number getFieldNumber(Document document, String name) {
        IndexableField field = document.getField(name);
        if (field == null) {
            return null;
        }

        return field.numericValue();
    }

    private static String getFieldString(Document document, String name) {
        IndexableField field = document.getField(name);
        if (field == null) {
            return null;
        }

        return field.stringValue();
    }

    private static int getIntArgument(NamedList<?> values, String name, int minValue, int defaultValue) {
        Object value = values.get(name);
        if (value == null) {
            return defaultValue;
        }

        if (!(value instanceof Number)) {
            throw new IllegalArgumentException("Value for parameter '" + name + "' must be a number.");
        }

        int result = ((Number) value).intValue();
        if (result < minValue) {
            throw new IllegalArgumentException(
                "Value for parameter '" + name + "' must be at least " + minValue + ", but it was " + result + ".");
        }

        return result;

    }

    private static String getStringArgument(NamedList<?> values, String fieldName) {
        Object value = values.get(fieldName);
        if (value == null) {
            throw new IllegalStateException("No value for parameter '" + fieldName + "' specified in solrconfig.xml!");
        }

        String result = String.valueOf(value);
        if (result.trim().length() == 0) {
            throw new IllegalStateException("Value for parameter '" + fieldName + "' specified in solrconfig.xml was empty!");
        }

        return result;
    }

    private static boolean isEnabled(ResponseBuilder responseBuilder) {
        return responseBuilder.req.getParams().getBool(PARAMETER_SPATIALCLUSTERING, false);
    }

    @Override
    public String getDescription() {
        return "indoqa-spatial-clustering";
    }

    @Override
    public void init(PluginInfo info) {
        this.fieldNameId = getStringArgument(info.initArgs, PARAMETER_FIELD_NAME_ID);
        this.fieldNameLon = getStringArgument(info.initArgs, PARAMETER_FIELD_NAME_LON);
        this.fieldNameLat = getStringArgument(info.initArgs, PARAMETER_FIELD_NAME_LAT);

        this.fields = new HashSet<>();
        this.fields.add(this.fieldNameId);
        this.fields.add(this.fieldNameLon);
        this.fields.add(this.fieldNameLat);

        this.maxSize = getIntArgument(info.initArgs, PARAMETER_MAX_SIZE, MIN_SIZE, DEFAULT_MAX_SIZE);
    }

    @Override
    public void prepare(ResponseBuilder responseBuilder) throws IOException {
        if (!isEnabled(responseBuilder)) {
            return;
        }

        responseBuilder.setNeedDocSet(true);
    }

    @Override
    public void process(ResponseBuilder responseBuilder) throws IOException {
        if (!isEnabled(responseBuilder)) {
            return;
        }

        int minResultCount = responseBuilder.req.getParams().getInt(PARAMETER_MIN_RESULT_COUNT, 1);
        if (responseBuilder.getResults().docSet.size() < minResultCount) {
            return;
        }

        int size = responseBuilder.req.getParams().getInt(PARAMETER_SIZE, DEFAULT_SIZE);
        if (size > this.maxSize) {
            throw new IllegalArgumentException(
                "The requested size is larger than " + this.maxSize + ". Consider changing " + PARAMETER_MAX_SIZE
                    + " in the plugin configuration.");
        }

        if (size < MIN_SIZE) {
            throw new IllegalArgumentException("The requested size must be at least " + MIN_SIZE + ".");
        }

        DblClusters<String> clusters = this.createClusters(responseBuilder, size);

        Object result;

        if (responseBuilder.req.getParams().getBool(PARAMETER_COMPACT_FORMAT, false)) {
            result = ResultFormatter.createCompactClusterResult(clusters);
        } else {
            result = ResultFormatter.createClusterResult(clusters);

        }
        responseBuilder.rsp.add("spatial-clustering", result);
    }

    private DblClusters<String> createClusters(ResponseBuilder responseBuilder, int size) throws IOException {
        DblClusters<String> result = new DblClusters<>(2, size);

        for (Iterator<Integer> iterator = responseBuilder.getResults().docSet.iterator(); iterator.hasNext();) {
            Document doc = responseBuilder.req.getSearcher().doc(iterator.next(), this.fields);

            Number latitude = getFieldNumber(doc, this.fieldNameLat);
            if (latitude == null) {
                continue;
            }

            Number longitude = getFieldNumber(doc, this.fieldNameLon);
            if (longitude == null) {
                continue;
            }

            result.add(1, new double[] {longitude.doubleValue(), latitude.doubleValue()}, getFieldString(doc, this.fieldNameId));
        }

        return result;
    }
}