/*******************************************************************************
 * Copyright (c) 2018 Eclipse RDF4J contributors.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *******************************************************************************/
package org.eclipse.rdf4j.query.algebra.evaluation.function.geosparql;

import org.locationtech.jts.geom.Geometry;
import org.locationtech.spatial4j.context.jts.JtsSpatialContext;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.jts.JtsShapeFactory;

/**
 * JTS-enabled implementation of spatial algebra, with full support for polygon-related geospatial functions
 */
public class JtsSpatialAlgebra implements SpatialAlgebra {

	private final JtsShapeFactory shapeFactory;

	public JtsSpatialAlgebra(JtsSpatialContext context) {
		this.shapeFactory = context.getShapeFactory();
	}

	@Override
	public Shape buffer(Shape s, double distance) {
		return shapeFactory.makeShapeFromGeometry(shapeFactory.getGeometryFrom(s).buffer(distance));
	}

	@Override
	public Shape convexHull(Shape s) {
		return shapeFactory.makeShapeFromGeometry(shapeFactory.getGeometryFrom(s).convexHull());
	}

	@Override
	public Shape boundary(Shape s) {
		return shapeFactory.makeShapeFromGeometry(shapeFactory.getGeometryFrom(s).getBoundary());
	}

	@Override
	public Shape envelope(Shape s) {
		return shapeFactory.makeShapeFromGeometry(shapeFactory.getGeometryFrom(s).getEnvelope());
	}

	@Override
	public Shape union(Shape s1, Shape s2) {
		return shapeFactory
				.makeShapeFromGeometry(shapeFactory.getGeometryFrom(s1).union(shapeFactory.getGeometryFrom(s2)));
	}

	@Override
	public Shape intersection(Shape s1, Shape s2) {
		Geometry intersection = shapeFactory.getGeometryFrom(s1).intersection(shapeFactory.getGeometryFrom(s2));
		if (intersection.isEmpty()) {
			return shapeFactory.pointXY(Double.NaN, Double.NaN);
		}
		return shapeFactory.makeShapeFromGeometry(intersection);
	}

	@Override
	public Shape symDifference(Shape s1, Shape s2) {
		Geometry symDiff = shapeFactory.getGeometryFrom(s1).symDifference(shapeFactory.getGeometryFrom(s2));
		if (symDiff.isEmpty()) {
			return shapeFactory.pointXY(Double.NaN, Double.NaN);
		}
		return shapeFactory.makeShapeFromGeometry(symDiff);
	}

	@Override
	public Shape difference(Shape s1, Shape s2) {
		Geometry difference = shapeFactory.getGeometryFrom(s1).difference(shapeFactory.getGeometryFrom(s2));
		if (difference.isEmpty()) {
			return shapeFactory.pointXY(Double.NaN, Double.NaN);
		}
		return shapeFactory.makeShapeFromGeometry(difference);
	}

	@Override
	public boolean relate(Shape s1, Shape s2, String intersectionPattern) {
		return shapeFactory.getGeometryFrom(s1).relate(shapeFactory.getGeometryFrom(s2), intersectionPattern);
	}

	@Override
	public boolean sfEquals(Shape s1, Shape s2) {
		return relate(s1, s2, "TFFFTFFFT");
	}

	@Override
	public boolean sfDisjoint(Shape s1, Shape s2) {
		return relate(s1, s2, "FF*FF****");
	}

	@Override
	public boolean sfIntersects(Shape s1, Shape s2) {
		return relate(s1, s2, "T********") || relate(s1, s2, "*T*******") || relate(s1, s2, "***T*****")
				|| relate(s1, s2, "****T****");
	}

	@Override
	public boolean sfTouches(Shape s1, Shape s2) {
		return relate(s1, s2, "FT*******") || relate(s1, s2, "F**T*****") || relate(s1, s2, "F***T****");
	}

	@Override
	public boolean sfCrosses(Shape s1, Shape s2) {
		Geometry g1 = shapeFactory.getGeometryFrom(s1);
		Geometry g2 = shapeFactory.getGeometryFrom(s2);
		int d1 = g1.getDimension();
		int d2 = g2.getDimension();
		if ((d1 == 0 && d2 == 1) || (d1 == 0 && d2 == 2) || (d1 == 1 && d2 == 2)) {
			return g1.relate(g2, "T*T***T**");
		} else if (d1 == 1 && d2 == 1) {
			return g1.relate(g2, "0*T***T**");
		} else {
			return false;
		}
	}

	@Override
	public boolean sfWithin(Shape s1, Shape s2) {
		return relate(s1, s2, "T*F**F***");
	}

	@Override
	public boolean sfContains(Shape s1, Shape s2) {
		return relate(s1, s2, "T*****FF*");
	}

	@Override
	public boolean sfOverlaps(Shape s1, Shape s2) {
		Geometry g1 = shapeFactory.getGeometryFrom(s1);
		Geometry g2 = shapeFactory.getGeometryFrom(s2);
		int d1 = g1.getDimension();
		int d2 = g2.getDimension();
		if ((d1 == 2 && d2 == 2) || (d1 == 0 && d2 == 0)) {
			return g1.relate(g2, "T*T***T**");
		} else if (d1 == 1 && d2 == 1) {
			return g1.relate(g2, "1*T***T**");
		} else {
			return false;
		}
	}

	@Override
	public boolean ehEquals(Shape s1, Shape s2) {
		return ehInside(s1, s2) && ehContains(s1, s2);
	}

	@Override
	public boolean ehDisjoint(Shape s1, Shape s2) {
		return relate(s1, s2, "FF*FF****");
	}

	@Override
	public boolean ehMeet(Shape s1, Shape s2) {
		return relate(s1, s2, "FT*******") || relate(s1, s2, "F**T*****") || relate(s1, s2, "F***T****");
	}

	@Override
	public boolean ehOverlap(Shape s1, Shape s2) {
		return relate(s1, s2, "T*T***T**");
	}

	@Override
	public boolean ehCovers(Shape s1, Shape s2) {
		return relate(s1, s2, "T*TFT*FF*");
	}

	@Override
	public boolean ehCoveredBy(Shape s1, Shape s2) {
		return relate(s1, s2, "TFF*TFT**");
	}

	@Override
	public boolean ehInside(Shape s1, Shape s2) {
		return relate(s1, s2, "TFF*FFT**");
	}

	@Override
	public boolean ehContains(Shape s1, Shape s2) {
		return relate(s1, s2, "T*TFF*FF*");
	}

	@Override
	public boolean rcc8dc(Shape s1, Shape s2) {
		return relate(s1, s2, "FFTFFTTTT");
	}

	@Override
	public boolean rcc8ec(Shape s1, Shape s2) {
		return relate(s1, s2, "FFTFTTTTT");
	}

	@Override
	public boolean rcc8po(Shape s1, Shape s2) {
		return relate(s1, s2, "TTTTTTTTT");
	}

	@Override
	public boolean rcc8tppi(Shape s1, Shape s2) {
		return relate(s1, s2, "TTTFTTFFT");
	}

	@Override
	public boolean rcc8tpp(Shape s1, Shape s2) {
		return relate(s1, s2, "TFFTTFTTT");
	}

	@Override
	public boolean rcc8ntpp(Shape s1, Shape s2) {
		return relate(s1, s2, "TFFTFFTTT");
	}

	@Override
	public boolean rcc8ntppi(Shape s1, Shape s2) {
		return relate(s1, s2, "TTTFFTFFT");
	}

	@Override
	public boolean rcc8eq(Shape s1, Shape s2) {
		return relate(s1, s2, "TFFFTFFFT");
	}

}
