// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.osm;

import java.awt.geom.Rectangle2D;
import java.util.Arrays;

import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.coor.QuadTiling;
import org.openstreetmap.josm.tools.Utils;

public class BBox {

    private double xmin = Double.POSITIVE_INFINITY;
    private double xmax = Double.NEGATIVE_INFINITY;
    private double ymin = Double.POSITIVE_INFINITY;
    private double ymax = Double.NEGATIVE_INFINITY;

    /**
     * Constructs a new {@code BBox} defined by a single point.
     *
     * @param x X coordinate
     * @param y Y coordinate
     * @since 6203
     */
    public BBox(final double x, final double y) {
        xmax = xmin = x;
        ymax = ymin = y;
        sanity();
    }

    /**
     * Constructs a new {@code BBox} defined by points <code>a</code> and <code>b</code>.
     * Result is minimal BBox containing both points.
     *
     * @param a first point
     * @param b second point
     */
    public BBox(LatLon a, LatLon b) {
        this(a.lon(), a.lat(), b.lon(), b.lat());
    }

    /**
     * Constructs a new {@code BBox} from another one.
     *
     * @param copy the BBox to copy
     */
    public BBox(BBox copy) {
        this.xmin = copy.xmin;
        this.xmax = copy.xmax;
        this.ymin = copy.ymin;
        this.ymax = copy.ymax;
    }

    public BBox(double a_x, double a_y, double b_x, double b_y)  {

        if (a_x > b_x) {
            xmax = a_x;
            xmin = b_x;
        } else {
            xmax = b_x;
            xmin = a_x;
        }

        if (a_y > b_y) {
            ymax = a_y;
            ymin = b_y;
        } else {
            ymax = b_y;
            ymin = a_y;
        }

        sanity();
    }

    public BBox(Way w) {
        for (Node n : w.getNodes()) {
            LatLon coor = n.getCoor();
            if (coor == null) {
                continue;
            }
            add(coor);
        }
    }

    public BBox(Node n) {
        LatLon coor = n.getCoor();
        if (coor == null) {
            xmin = xmax = ymin = ymax = 0;
        } else {
            xmin = xmax = coor.lon();
            ymin = ymax = coor.lat();
        }
    }

    private void sanity()  {
        if (xmin < -180.0) {
            xmin = -180.0;
        }
        if (xmax >  180.0) {
            xmax =  180.0;
        }
        if (ymin <  -90.0) {
            ymin =  -90.0;
        }
        if (ymax >   90.0) {
            ymax =   90.0;
        }
    }

    public final void add(LatLon c) {
        add(c.lon(), c.lat());
    }

    /**
     * Extends this bbox to include the point (x, y)
     */
    public final void add(double x, double y) {
        xmin = Math.min(xmin, x);
        xmax = Math.max(xmax, x);
        ymin = Math.min(ymin, y);
        ymax = Math.max(ymax, y);
        sanity();
    }

    public final void add(BBox box) {
        xmin = Math.min(xmin, box.xmin);
        xmax = Math.max(xmax, box.xmax);
        ymin = Math.min(ymin, box.ymin);
        ymax = Math.max(ymax, box.ymax);
        sanity();
    }

    public void addPrimitive(OsmPrimitive primitive, double extraSpace) {
        BBox primBbox = primitive.getBBox();
        add(primBbox.xmin - extraSpace, primBbox.ymin - extraSpace);
        add(primBbox.xmax + extraSpace, primBbox.ymax + extraSpace);
    }

    public double height() {
        return ymax-ymin;
    }

    public double width() {
        return xmax-xmin;
    }

    /**
     * Tests, weather the bbox b lies completely inside
     * this bbox.
     */
    public boolean bounds(BBox b) {
        if (!(xmin <= b.xmin) ||
                !(xmax >= b.xmax) ||
                !(ymin <= b.ymin) ||
                !(ymax >= b.ymax))
            return false;
        return true;
    }

    /**
     * Tests, weather the Point c lies within the bbox.
     */
    public boolean bounds(LatLon c) {
        if ((xmin <= c.lon()) &&
                (xmax >= c.lon()) &&
                (ymin <= c.lat()) &&
                (ymax >= c.lat()))
            return true;
        return false;
    }

    /**
     * Tests, weather two BBoxes intersect as an area.
     * I.e. whether there exists a point that lies in both of them.
     */
    public boolean intersects(BBox b) {
        if (xmin > b.xmax)
            return false;
        if (xmax < b.xmin)
            return false;
        if (ymin > b.ymax)
            return false;
        if (ymax < b.ymin)
            return false;
        return true;
    }

    /**
     * Returns the top-left point.
     * @return The top-left point
     */
    public LatLon getTopLeft() {
        return new LatLon(ymax, xmin);
    }

    /**
     * Returns the latitude of top-left point.
     * @return The latitude of top-left point
     * @since 6203
     */
    public double getTopLeftLat() {
        return ymax;
    }

    /**
     * Returns the longitude of top-left point.
     * @return The longitude of top-left point
     * @since 6203
     */
    public double getTopLeftLon() {
        return xmin;
    }

    /**
     * Returns the bottom-right point.
     * @return The bottom-right point
     */
    public LatLon getBottomRight() {
        return new LatLon(ymin, xmax);
    }

    /**
     * Returns the latitude of bottom-right point.
     * @return The latitude of bottom-right point
     * @since 6203
     */
    public double getBottomRightLat() {
        return ymin;
    }

    /**
     * Returns the longitude of bottom-right point.
     * @return The longitude of bottom-right point
     * @since 6203
     */
    public double getBottomRightLon() {
        return xmax;
    }

    public LatLon getCenter() {
        return new LatLon(ymin + (ymax-ymin)/2.0, xmin + (xmax-xmin)/2.0);
    }

    int getIndex(final int level) {

        int idx1 = QuadTiling.index(ymin, xmin, level);

        final int idx2 = QuadTiling.index(ymin, xmax, level);
        if (idx1 == -1) idx1 = idx2;
        else if (idx1 != idx2) return -1;

        final int idx3 = QuadTiling.index(ymax, xmin, level);
        if (idx1 == -1) idx1 = idx3;
        else if (idx1 != idx3) return -1;

        final int idx4 = QuadTiling.index(ymax, xmax, level);
        if (idx1 == -1) idx1 = idx4;
        else if (idx1 != idx4) return -1;

        return idx1;
    }

    public Rectangle2D toRectangle() {
        return new Rectangle2D.Double(xmin, ymin, xmax - xmin, ymax - ymin);
    }

    @Override
    public int hashCode() {
        return (int) (ymin * xmin);
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof BBox) {
            BBox b = (BBox) o;
            return b.xmax == xmax && b.ymax == ymax
                    && b.xmin == xmin && b.ymin == ymin;
        } else
            return false;
    }

    @Override
    public String toString() {
        return "[ x: " + xmin + " -> " + xmax + ", y: " + ymin + " -> " + ymax + " ]";
    }

    public String toStringCSV(String separator) {
        return Utils.join(separator, Arrays.asList(
                LatLon.cDdFormatter.format(xmin),
                LatLon.cDdFormatter.format(ymin),
                LatLon.cDdFormatter.format(xmax),
                LatLon.cDdFormatter.format(ymax)));
    }
}
