/*
 * Copyright (C) 2006 Steve Ratcliffe
 * 
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 * 
 *  This program 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 General Public License for more details.
 * 
 * 
 * Author: Steve Ratcliffe
 * Create date: 17-Dec-2006
 */
package uk.me.parabola.mkgmap.reader.osm;

import java.awt.Polygon;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.filters.ShapeMergeFilter;

/**
 * Represent a OSM way in the 0.5 api.  A way consists of an ordered list of
 * nodes.
 *
 * @author Steve Ratcliffe
 */
public class Way extends Element {
	private static final Logger log = Logger.getLogger(Way.class);
	private final List<Coord> points;
	private long fullArea = Long.MAX_VALUE; // meaning unset
	private MultiPolygonRelation mpRel; 

	// This will be set if a way is read from an OSM file and the first node is the same node as the last
	// one in the way. This can be set to true even if there are missing nodes and so the nodes that we
	// have do not form a closed loop.
	// Note: this is not always set
	private boolean closedInOSM;

	// This is set to false if, we know that there are nodes missing from this way.
	// If you set this to false, then you *must* also set closed to the correct value.
	private boolean complete  = true;
	
	private boolean isViaWay;

	public Way(long id) {
		points = new ArrayList<>(5);
		setId(id);
	}

	public Way(long id, List<Coord> points) {
		this.points = new ArrayList<>(points);
		setId(id);
	}

	@Override
	public Way copy() {
		Way dup = new Way(getId(), points);
		dup.copyIds(this);
		dup.copyTags(this);
		dup.closedInOSM = this.closedInOSM;
		dup.complete = this.complete;
		dup.isViaWay = this.isViaWay;
		dup.fullArea = this.getFullArea();
		dup.mpRel = this.mpRel;
		return dup;
	}

	/**
	 * Get the points that make up the way.  We attempt to re-order the segments
	 * and return a list of points that traces the route of the way.
	 *
	 * @return A simple list of points that form a line.
	 */
	public List<Coord> getPoints() {
		return points;
	}

	/**
	 * @return first point or null if points is empty
	 */
	public Coord getFirstPoint() {
		return points.isEmpty() ? null:points.get(0);
	}

	/**
	 * @return last point or null if points is empty
	 */
	public Coord getLastPoint() {
		return points.isEmpty() ? null:points.get(points.size() - 1);
	}

	public void addPoint(Coord co) {
		points.add(co);
	}

	public void addPointIfNotEqualToLastPoint(Coord co) {
		if(points.isEmpty() || !co.highPrecEquals(getLastPoint())) 
			points.add(co);
	}

	public void reverse() {
		Collections.reverse(points);
	}

	/**
	 * Returns true if the way is really closed in OSM.
	 *
	 * Will return true even if the way is incomplete in the tile that we are reading, but the way is
	 * really closed in OSM.
	 *
	 * @return True if the way is really closed.
	 */
	public boolean isClosed() {
		if (!isComplete())
			return closedInOSM;

		return !points.isEmpty() && hasIdenticalEndPoints();
	}

	/**
	 * 
	 * @return true if the way is really closed in OSM,
	 * false if the way was created by mkgmap or read from polish
	 * input file (*.mp). 
	 * 
	 */
	public boolean isClosedInOSM() {
		return closedInOSM;
	}

	/**
	 *  
	 * @return Returns true if the first point in the way is identical to the last.
	 */
	public boolean hasIdenticalEndPoints() {
		return !points.isEmpty() && points.get(0) == points.get(points.size()-1);
	}

	/**
	 *  
	 * @return Returns true if the first point in the way is identical to the last.
	 */
	public boolean hasEqualEndPoints() {
		return !points.isEmpty() && points.get(0).equals(points.get(points.size()-1));
	}

	public void setClosedInOSM(boolean closed) {
		this.closedInOSM = closed;
	}

	public boolean isComplete() {
		return complete;
	}

	/**
	 * Set this to false if you know that the way does not have its complete set of nodes.
	 *
	 * If you do set this to false, then you must also call {@link #setClosed} to indicate if the way
	 * is really closed or not.
	 */
	public void setComplete(boolean complete) {
		this.complete = complete;
	}

	/**
	 * A simple representation of this way.
	 * @return A string with the name, start point and end point
	 */
	public String toString() {
		StringBuilder sb = new StringBuilder(super.toString());
		if (getName() != null) {
			sb.append(' ').append(getName());
		}
		if (points.isEmpty())
			sb.append(" empty");
		else {
			Coord coord = getFirstPoint();
			if (hasEqualEndPoints()) {
				sb.append(" starting and ending at ").append(coord);
			}
			else {
				sb.append(" starting at ").append(coord).append(" and ending at ").append(getLastPoint());
			}
		}
		sb.append(' ').append(toTagString());
		return sb.toString();
	}

	public int hashCode() {
		return (int) getId();
	}

	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;

		return getId() == ((Way) o).getId();
	}

	/**
	 * calculate weighted centre of way, using high precision
	 * @return
	 */
	public Coord getCofG() {
		int numPoints = points.size();
		if(numPoints < 1)
			return null;

		double lat = 0;
		double lon = 0;
		if (hasIdenticalEndPoints())
			numPoints--;
		for (int i = 0; i < numPoints; i++){
			Coord p = points.get(i);
			lat += (double)p.getHighPrecLat()/numPoints;
			lon += (double)p.getHighPrecLon()/numPoints;
		}
		return Coord.makeHighPrecCoord((int)Math.round(lat), (int)Math.round(lon));
	}

	@Override
	public String kind() {
		return "way";
	}

	// returns true if the way is a closed polygon with a clockwise
	// direction
	public static boolean clockwise(List<Coord> points) {

		
		if(points.size() < 3 || !points.get(0).equals(points.get(points.size() - 1)))
			return false;
		if (!points.get(0).highPrecEquals(points.get(points.size() - 1))) {
			log.error("Way.clockwise was called for way that is not closed in high precision");
		}
		
		long area = 0;
		Coord p1 = points.get(0);
		for(int i = 1; i < points.size(); ++i) {
			Coord p2 = points.get(i);
			area += ((long)p1.getHighPrecLon() * p2.getHighPrecLat() - 
					 (long)p2.getHighPrecLon() * p1.getHighPrecLat());
			p1 = p2;
		}

		// this test looks to be inverted but gives the expected result!
		// empty linear areas are defined as clockwise 
		return area <= 0;
	}

	// simplistic check to see if this way "contains" another - for
	// speed, all we do is check that all of the other way's points
	// are inside this way's polygon
	public boolean containsPointsOf(Way other) {
		Polygon thisPoly = new Polygon();
		for(Coord p : points)
			thisPoly.addPoint(p.getHighPrecLon(), p.getHighPrecLat());
		for(Coord p : other.points)
			if(!thisPoly.contains(p.getHighPrecLon(), p.getHighPrecLat()))
				return false;
		return true;
	}

	public boolean isViaWay() {
		return isViaWay;
	}

	public void setViaWay(boolean isViaWay) {
		this.isViaWay = isViaWay;
	}

	/**
	 * Allows to manipulate the area size which might be used to sort shapes when
	 * option --order-by-decreasing-area is active. 
	 * @param fullArea
	 */
	public void setFullArea(long fullArea) {
		this.fullArea = fullArea;
	}

	public long getFullArea() { // this is unadulterated size, positive if clockwise
		if (this.fullArea == Long.MAX_VALUE && points.size() >= 4 && getFirstPoint().highPrecEquals(getLastPoint())) {
			this.fullArea = ShapeMergeFilter.calcAreaSizeTestVal(points);
		}
		return this.fullArea;
	}
	
	public double calcLengthInMetres() {
		double length = 0;
		if (points.size() > 1) {
			Coord p0 = points.get(0);
			for (int i = 1; i < points.size(); i++) {
				Coord p1 = points.get(i);
				length += p0.distance(p1);
				p0 = p1;
			}
		}
		return length;
	}

	/**
	 * @return the mpRel, null if the way was not created from a multipolygon with inner rings
	 */
	public MultiPolygonRelation getMpRel() {
		return mpRel;
	}

	/**
	 * @param mpRel the mpRel to set
	 */
	public void setMpRel(MultiPolygonRelation mpRel) {
		this.mpRel = mpRel;
	}
}
