/*-
 *  Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
 * 
 *  This file is part of CityDoctor2.
 *
 *  CityDoctor2 is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  CityDoctor2 is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with CityDoctor2.  If not, see <https://www.gnu.org/licenses/>.
 */
package de.hft.stuttgart.citydoctor2.edge;

import java.util.ArrayList;
import java.util.List;

public class Polygon2d extends BaseEntity {

	List<List<HalfEdge2d>> mHalfEdges = new ArrayList<>();

	public Polygon2d() {
	}

	public Polygon2d(List<HalfEdge2d> halfEdges) {
		for (HalfEdge2d he : halfEdges) {
			addChild(he);
		}
		mHalfEdges.add(halfEdges);
	}

	public boolean isPointInsidePolygon(Point2d rcPoint, double eps) {
		// algorithm idea: cast a ray from rcPoint in u-direction and
		// count how often it hits the polygon borders. If there are an odd number
		// of intersections, the point lays inside the polygon, otherwise not.

		// easy check: check point vs bounding box
		Box2d box = getBoundingBox2d();
		if (!box.isPointInside(rcPoint)) {
			return false;
		}

		List<HalfEdge2d> hes = getHalfEdges();
		double u = rcPoint.getX();
		double v = rcPoint.getY();

		boolean inside = false;
		for (HalfEdge2d it : hes) {
			Point2d start = it.getStart().getPoint();
			// just check the start, the current end point is the start point of the
			// next half edge.
			if (rcPoint.equals(start)) {
				return true;
			}
			Point2d end = it.getEnd().getPoint();
			double u1 = start.getX();
			double v1 = start.getY();
			double u2 = end.getX();
			double v2 = end.getY();

			boolean startVIsBigger = v1 >= v;
			boolean endVIsBigger = v2 >= v;

			// If the v values of the current halfedges are both bigger or smaler
			// than the v coordinate from the given point, than a line, parallel to
			// the u-axis wont hit the half edge
			if (startVIsBigger != endVIsBigger) {
				// half edge and ray might intersect. calculate the intersection point
				// and check if the u-value of the intersection point is smaler than
				// u from given point (no intersection) or bigger or equal (intersection)
				// the formula is derived by caculating the intersection of the function
				// g(x) = v with the half edge
				double uInt = (v * (u2 - u1) - v1 * u2 + v2 * u1) / (v2 - v1);
				if (Math.abs(uInt - u) < eps) {
					return true; // point is lying on edge-> inside
				} else {
					if (uInt > u) {
						inside = !inside;
					}
				}
			}
		}
		return inside;
	}

	private Box2d getBoundingBox2d() {
		List<HalfEdge2d> halfEdges = getHalfEdges();

		Point2d point = halfEdges.get(0).getStart().getPoint();

		double lowerU = point.getX();
		double lowerV = point.getY();
		double upperU = point.getX();
		double upperV = point.getY();

		for (int i = 1; i < halfEdges.size(); i++) {
			HalfEdge2d halfEdge = halfEdges.get(i);

			point = halfEdge.getStart().getPoint();

			lowerU = (point.getX() < lowerU) ? point.getX() : lowerU;
			lowerV = (point.getY() < lowerV) ? point.getY() : lowerV;

			upperU = (point.getX() > upperU) ? point.getX() : upperU;
			upperV = (point.getY() > upperV) ? point.getY() : upperV;
		}

		Box2d box = new Box2d(new Point2d(lowerU, lowerV), new Point2d(upperU, upperV));

		return box;
	}

	public static void connectHalfEdges(List<HalfEdge2d> rHalfEdges) {
		if (rHalfEdges.isEmpty()) {
			throw new IllegalArgumentException("no halfEdges given");
		}

		if (rHalfEdges.size() == 1) {
			HalfEdge2d front = rHalfEdges.get(0);
			front.setNext(front);
			front.setPrevious(front);
		} else {
			for (int i = 0; i < rHalfEdges.size() - 1; i++) {
				HalfEdge2d previous = rHalfEdges.get(i);
				HalfEdge2d next = rHalfEdges.get(i + 1);
				previous.setNext(next);
				next.setPrevious(previous);
			}
			HalfEdge2d back = rHalfEdges.get(rHalfEdges.size() - 1);
			HalfEdge2d front = rHalfEdges.get(0);
			back.setNext(front);
			front.setPrevious(back);
		}

	}

	public boolean isPointInsidePolygon(Point2d rcPoint) {
		return isPointInsidePolygon(rcPoint, Global.getHighAccuracyTolerance());
	}

	public List<Coordinate2d> getCoordinates() {
		List<Coordinate2d> coords = new ArrayList<>();
		HalfEdge2d firstHE = getFirstHalfEdge();
		HalfEdge2d currHE = firstHE;

		do {
			coords.add(currHE.getStart());
			currHE = currHE.getNext();
		} while (currHE != firstHE);

		return coords;
	}

	public Point2d getMidPoint() {
		double x = 0;
		double y = 0;
		List<HalfEdge2d> halfEdges = getChildren(HalfEdge2d.class);

		for (HalfEdge2d he : halfEdges) {
			Point2d point = he.getStart().getPoint();
			x = x + point.getX();
			y = y + point.getY();
		}

		int count = halfEdges.size();
		return new Point2d(x / count, y / count);
	}

	public List<HalfEdge2d> getHalfEdges() {
		return getChildren(HalfEdge2d.class);
	}

	public HalfEdge2d getFirstHalfEdge() {
		List<HalfEdge2d> halfEdges = getHalfEdges();
		if (!halfEdges.isEmpty()) {
			return halfEdges.get(0);
		}

		return null;
	}

}
