/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch 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.elasticsearch.search.aggregations.bucket.range.geodistance;

import com.google.common.collect.Lists;
import org.elasticsearch.common.geo.GeoDistance;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilderException;

import java.io.IOException;
import java.util.List;
import java.util.Locale;

/**
 * Builder for the {@link GeoDistance} aggregation.
 */
public class GeoDistanceBuilder extends AggregationBuilder<GeoDistanceBuilder> {

    /**
     * A range of values.
     */
    public static class Range implements ToXContent {

        private String key;
        private Double from;
        private Double to;

        /**
         * Create a new range.
         * @param key   the identifier of this range
         * @param from  the lower bound (inclusive)
         * @param to    the upper bound (exclusive)
         */
        public Range(String key, Double from, Double to) {
            this.key = key;
            this.from = from;
            this.to = to;
        }

        @Override
        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
            builder.startObject();
            if (from != null) {
                builder.field("from", from.doubleValue());
            }
            if (to != null) {
                builder.field("to", to.doubleValue());
            }
            if (key != null) {
                builder.field("key", key);
            }
            return builder.endObject();
        }

    }

    private String field;
    private DistanceUnit unit;
    private GeoDistance distanceType;
    private GeoPoint point;

    private List<Range> ranges = Lists.newArrayList();

    /**
     * Sole constructor.
     */
    public GeoDistanceBuilder(String name) {
        super(name, InternalGeoDistance.TYPE.name());
    }

    /**
     * Set the field to use to compute distances.
     */
    public GeoDistanceBuilder field(String field) {
        this.field = field;
        return this;
    }

    /**
     * Set the unit to use for distances, default is kilometers.
     */
    public GeoDistanceBuilder unit(DistanceUnit unit) {
        this.unit = unit;
        return this;
    }

    /**
     * Set the {@link GeoDistance distance type} to use, defaults to
     * {@link GeoDistance#SLOPPY_ARC}.
     */
    public GeoDistanceBuilder distanceType(GeoDistance distanceType) {
        this.distanceType = distanceType;
        return this;
    }

    /**
     * Set the point to calculate distances from using a
     * <code>lat,lon</code> notation or geohash.
     */
    public GeoDistanceBuilder point(String latLon) {
        return point(GeoPoint.parseFromLatLon(latLon));
    }

    /**
     * Set the point to calculate distances from.
     */
    public GeoDistanceBuilder point(GeoPoint point) {
        this.point = point;
        return this;
    }

    /**
     * Set the point to calculate distances from using its geohash.
     */
    public GeoDistanceBuilder geohash(String geohash) {
        if (this.point == null) {
            this.point = new GeoPoint();
        }
        this.point.resetFromGeoHash(geohash);
        return this;
    }

    /**
     * Set the latitude of the point to calculate distances from.
     */
    public GeoDistanceBuilder lat(double lat) {
        if (this.point == null) {
            point = new GeoPoint();
        }
        point.resetLat(lat);
        return this;
    }

    /**
     * Set the longitude of the point to calculate distances from.
     */
    public GeoDistanceBuilder lon(double lon) {
        if (this.point == null) {
            point = new GeoPoint();
        }
        point.resetLon(lon);
        return this;
    }

    /**
     * Add a new range to this aggregation.
     *
     * @param key  the key to use for this range in the response
     * @param from the lower bound on the distances, inclusive
     * @parap to   the upper bound on the distances, exclusive
     */
    public GeoDistanceBuilder addRange(String key, double from, double to) {
        ranges.add(new Range(key, from, to));
        return this;
    }

    /**
     * Same as {@link #addRange(String, double, double)} but the key will be
     * automatically generated based on <code>from</code> and <code>to</code>.
     */
    public GeoDistanceBuilder addRange(double from, double to) {
        return addRange(null, from, to);
    }

    /**
     * Add a new range with no lower bound.
     *
     * @param key the key to use for this range in the response
     * @param to  the upper bound on the distances, exclusive
     */
    public GeoDistanceBuilder addUnboundedTo(String key, double to) {
        ranges.add(new Range(key, null, to));
        return this;
    }

    /**
     * Same as {@link #addUnboundedTo(String, double)} but the key will be
     * computed automatically.
     */
    public GeoDistanceBuilder addUnboundedTo(double to) {
        return addUnboundedTo(null, to);
    }

    /**
     * Add a new range with no upper bound.
     *
     * @param key  the key to use for this range in the response
     * @param from the lower bound on the distances, inclusive
     */
    public GeoDistanceBuilder addUnboundedFrom(String key, double from) {
        ranges.add(new Range(key, from, null));
        return this;
    }

    /**
     * Same as {@link #addUnboundedFrom(String, double)} but the key will be
     * computed automatically.
     */
    public GeoDistanceBuilder addUnboundedFrom(double from) {
        return addUnboundedFrom(null, from);
    }

    @Override
    protected XContentBuilder internalXContent(XContentBuilder builder, Params params) throws IOException {
        builder.startObject();
        if (ranges.isEmpty()) {
            throw new SearchSourceBuilderException("at least one range must be defined for geo_distance aggregation [" + getName() + "]");
        }
        if (point == null) {
            throw new SearchSourceBuilderException("center point must be defined for geo_distance aggregation [" + getName() + "]");
        }

        if (field != null) {
            builder.field("field", field);
        }

        if (unit != null) {
            builder.field("unit", unit);
        }

        if (distanceType != null) {
            builder.field("distance_type", distanceType.name().toLowerCase(Locale.ROOT));
        }

        builder.startObject("center")
                .field("lat", point.lat())
                .field("lon", point.lon())
                .endObject();

        builder.startArray("ranges");
        for (Range range : ranges) {
            range.toXContent(builder, params);
        }
        builder.endArray();

        return builder.endObject();
    }

}
