/*
 * 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.test.hamcrest;

import com.spatial4j.core.shape.Shape;
import com.spatial4j.core.shape.ShapeCollection;
import com.spatial4j.core.shape.impl.GeoCircle;
import com.spatial4j.core.shape.impl.RectangleImpl;
import com.spatial4j.core.shape.jts.JtsGeometry;
import com.spatial4j.core.shape.jts.JtsPoint;
import com.vividsolutions.jts.geom.*;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.geo.GeoDistance;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.builders.ShapeBuilder;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContentParser;
import org.hamcrest.Matcher;
import org.junit.Assert;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

public class ElasticsearchGeoAssertions {

    private static int top(Coordinate...points) {
        int top = 0;
        for (int i = 1; i < points.length; i++) {
            if(points[i].y < points[top].y) {
                top = i;
            } else if(points[i].y == points[top].y) {
                if(points[i].x <= points[top].x) {
                    top = i;
                }
            }
        }
        return top;
    }
    
    private static int prev(int top, Coordinate...points) {
        for (int i = 1; i < points.length; i++) {
            int p = (top + points.length - i) % points.length;
            if((points[p].x != points[top].x) || (points[p].y != points[top].y)) {
                return p;
            } 
        }
        return -1;
    }
    
    private static int next(int top, Coordinate...points) {
        for (int i = 1; i < points.length; i++) {
            int n = (top + i) % points.length;
            if((points[n].x != points[top].x) || (points[n].y != points[top].y)) {
                return n;
            } 
        }
        return -1;
    }
    
    private static Coordinate[] fixedOrderedRing(List<Coordinate> coordinates, boolean direction) {
        return fixedOrderedRing(coordinates.toArray(new Coordinate[coordinates.size()]), direction); 
    }

    private static Coordinate[] fixedOrderedRing(Coordinate[] points, boolean direction) {

        final int top = top(points);
        final int next = next(top, points);
        final int prev = prev(top, points);
        final boolean orientation = points[next].x < points[prev].x;

        if(orientation != direction) {
            List<Coordinate> asList = Arrays.asList(points);
            Collections.reverse(asList);
            return fixedOrderedRing(asList, direction);
        } else {
            if(top>0) {
                Coordinate[] aligned = new Coordinate[points.length];
                System.arraycopy(points, top, aligned, 0, points.length-top-1);
                System.arraycopy(points, 0, aligned, points.length-top-1, top);
                aligned[aligned.length-1] = aligned[0];
                return aligned;
            } else {
                return points;
            }
        }
        
    }

    public static void assertEquals(Coordinate c1, Coordinate c2) {
        assertTrue("expected coordinate " + c1 + " but found " + c2, c1.x == c2.x && c1.y == c2.y);
    }

    private static boolean isRing(Coordinate[] c) {
        return (c[0].x == c[c.length-1].x) && (c[0].y == c[c.length-1].y);
    }
    
    public static void assertEquals(Coordinate[] c1, Coordinate[] c2) {
        Assert.assertEquals(c1.length, c2.length);

        if(isRing(c1) && isRing(c2)) {
            c1 = fixedOrderedRing(c1, true);
            c2 = fixedOrderedRing(c2, true);
        }

        for (int i = 0; i < c2.length; i++) {
            assertEquals(c1[i], c2[i]);
        }
    }

    public static void assertEquals(LineString l1, LineString l2) {
        assertEquals(l1.getCoordinates(), l2.getCoordinates());
    }

    public static void assertEquals(MultiLineString l1, MultiLineString l2) {
        assertEquals(l1.getCoordinates(), l2.getCoordinates());
    }

    public static void assertEquals(Polygon p1, Polygon p2) {
        Assert.assertEquals(p1.getNumInteriorRing(), p2.getNumInteriorRing());

        assertEquals(p1.getExteriorRing(), p2.getExteriorRing());

        // TODO: This test do not check all permutations of linestrings. So the test
        // fails if the holes of the polygons are not ordered the same way
        for (int i = 0; i < p1.getNumInteriorRing(); i++) {
            assertEquals(p1.getInteriorRingN(i), p2.getInteriorRingN(i));
        }
    }

    public static void assertEquals(MultiPolygon p1, MultiPolygon p2) {
        Assert.assertEquals(p1.getNumGeometries(), p2.getNumGeometries());

        // TODO: This test do not check all permutations. So the Test fails
        // if the inner polygons are not ordered the same way in both Multipolygons
        for (int i = 0; i < p1.getNumGeometries(); i++) {
            Geometry a = p1.getGeometryN(i);
            Geometry b = p2.getGeometryN(i);
            assertEquals(a, b);
        }
    }

    public static void assertEquals(Geometry s1, Geometry s2) {
        if(s1 instanceof LineString && s2 instanceof LineString) {
            assertEquals((LineString) s1, (LineString) s2);

        } else if (s1 instanceof Polygon && s2 instanceof Polygon) {
            assertEquals((Polygon) s1, (Polygon) s2);

        } else if (s1 instanceof MultiPoint && s2 instanceof MultiPoint) {
            Assert.assertEquals(s1, s2);

        } else if (s1 instanceof MultiPolygon && s2 instanceof MultiPolygon) {
            assertEquals((MultiPolygon) s1, (MultiPolygon) s2);

        } else if (s1 instanceof MultiLineString && s2 instanceof MultiLineString) {
            assertEquals((MultiLineString) s1, (MultiLineString) s2);

        } else {
            throw new RuntimeException("equality of shape types not supported [" + s1.getClass().getName() + " and " + s2.getClass().getName() + "]");
        }
    }

    public static void assertEquals(JtsGeometry g1, JtsGeometry g2) {
        assertEquals(g1.getGeom(), g2.getGeom());
    }

    public static void assertEquals(ShapeCollection s1, ShapeCollection s2) {
        Assert.assertEquals(s1.size(), s2.size());
        for (int i = 0; i < s1.size(); i++) {
            assertEquals(s1.get(i), s2.get(i));
        }
    }

    public static void assertEquals(Shape s1, Shape s2) {
        if(s1 instanceof JtsGeometry && s2 instanceof JtsGeometry) {
            assertEquals((JtsGeometry) s1, (JtsGeometry) s2);
        } else if(s1 instanceof JtsPoint && s2 instanceof JtsPoint) {
            JtsPoint p1 = (JtsPoint) s1;
            JtsPoint p2 = (JtsPoint) s2;
            Assert.assertEquals(p1, p2);
        } else if (s1 instanceof ShapeCollection && s2 instanceof ShapeCollection) {
            assertEquals((ShapeCollection)s1, (ShapeCollection)s2);
        } else if (s1 instanceof GeoCircle && s2 instanceof GeoCircle) {
            Assert.assertEquals((GeoCircle)s1, (GeoCircle)s2);
        } else if (s1 instanceof RectangleImpl && s2 instanceof RectangleImpl) {
            Assert.assertEquals((RectangleImpl)s1, (RectangleImpl)s2);
        } else {
            //We want to know the type of the shape because we test shape equality in a special way...
            //... in particular we test that one ring is equivalent to another ring even if the points are rotated or reversed.
            throw new RuntimeException(
                    "equality of shape types not supported [" + s1.getClass().getName() + " and " + s2.getClass().getName() + "]");
        }
    }

    private static Geometry unwrap(Shape shape) {
        assertThat(shape, instanceOf(JtsGeometry.class));
        return ((JtsGeometry)shape).getGeom();
    }

    public static void assertMultiPolygon(Shape shape) {
        assert(unwrap(shape) instanceof MultiPolygon): "expected MultiPolygon but found " + unwrap(shape).getClass().getName();
    }

    public static void assertPolygon(Shape shape) {
        assert(unwrap(shape) instanceof Polygon): "expected Polygon but found " + unwrap(shape).getClass().getName();
    }

    public static void assertLineString(Shape shape) {
        assert(unwrap(shape) instanceof LineString): "expected LineString but found " + unwrap(shape).getClass().getName();
    }

    public static void assertMultiLineString(Shape shape) {
        assert(unwrap(shape) instanceof MultiLineString): "expected MultiLineString but found " + unwrap(shape).getClass().getName();
    }
    
    public static void assertDistance(String geohash1, String geohash2, Matcher<Double> match) {
        GeoPoint p1 = new GeoPoint(geohash1);
        GeoPoint p2 = new GeoPoint(geohash2);
        assertDistance(p1.lat(), p1.lon(), p2.lat(),p2.lon(), match);
    }

    public static void assertDistance(double lat1, double lon1, double lat2, double lon2, Matcher<Double> match) {
        assertThat(distance(lat1, lon1, lat2, lon2), match);
    }
    
    private static double distance(double lat1, double lon1, double lat2, double lon2) {
        return GeoDistance.ARC.calculate(lat1, lon1, lat2, lon2, DistanceUnit.DEFAULT);
    }

    public static void assertValidException(XContentParser parser, Class expectedException) {
        try {
            ShapeBuilder.parse(parser).build();
            Assert.fail("process completed successfully when " + expectedException.getName() + " expected");
        } catch (Exception e) {
            assert(e.getClass().equals(expectedException)):
                    "expected " + expectedException.getName() + " but found " + e.getClass().getName();
        }
    }
}
