// This software is released into the Public Domain.  See copying.txt for details.
package org.openstreetmap.osmosis.pgsnapshot.v0_6.impl;

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

import net.postgis.jdbc.geometry.LineString;
import net.postgis.jdbc.geometry.LinearRing;
import net.postgis.jdbc.geometry.Point;
import net.postgis.jdbc.geometry.Polygon;
import org.openstreetmap.osmosis.core.OsmosisRuntimeException;
import org.openstreetmap.osmosis.core.domain.v0_6.Node;
import org.openstreetmap.osmosis.core.domain.v0_6.Way;
import org.openstreetmap.osmosis.core.domain.v0_6.WayNode;
import org.openstreetmap.osmosis.core.lifecycle.Closeable;
import org.openstreetmap.osmosis.pgsnapshot.common.CompactPersistentNodeLocationStore;
import org.openstreetmap.osmosis.pgsnapshot.common.InMemoryNodeLocationStore;
import org.openstreetmap.osmosis.pgsnapshot.common.NodeLocation;
import org.openstreetmap.osmosis.pgsnapshot.common.NodeLocationStore;
import org.openstreetmap.osmosis.pgsnapshot.common.NodeLocationStoreType;
import org.openstreetmap.osmosis.pgsnapshot.common.PersistentNodeLocationStore;

/**
 * Caches a set of node latitudes and longitudes and uses these to calculate the
 * geometries for ways.
 * 
 * @author Brett Henderson
 */
public class WayGeometryBuilder implements Closeable {
	
	/**
	 * Stores the locations of nodes so that they can be used to build the way
	 * geometries.
	 */
	protected NodeLocationStore locationStore;
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param storeType
	 *            The type of storage to use for holding node locations.
	 */
	public WayGeometryBuilder(NodeLocationStoreType storeType) {
		if (NodeLocationStoreType.InMemory.equals(storeType)) {
			locationStore = new InMemoryNodeLocationStore();
		} else if (NodeLocationStoreType.TempFile.equals(storeType)) {
			locationStore = new PersistentNodeLocationStore();
		} else if (NodeLocationStoreType.CompactTempFile.equals(storeType)) {
			locationStore = new CompactPersistentNodeLocationStore();
		} else {
			throw new OsmosisRuntimeException("The store type " + storeType + " is not recognized.");
		}
	}
	
	
	/**
	 * Adds the location of the node to the internal store.
	 * 
	 * @param node
	 *            The node to add.
	 */
	public void addNodeLocation(Node node) {
		locationStore.addLocation(node.getId(), new NodeLocation(node.getLongitude(), node.getLatitude()));
	}

    /**
     * Get NodeLocation from internal store.
     *
     * @param nodeId
     *              Id of the node we want the location for.
     * @return Location of node
     */
    public NodeLocation getNodeLocation(long nodeId) {
        return locationStore.getNodeLocation(nodeId);
    }
	
	private Polygon createWayBbox(double left, double right, double bottom, double top) {
		Point[] points;
		LinearRing ring;
		Polygon bbox;
		
		points = new Point[5];
		points[0] = new Point(left, bottom);
		points[1] = new Point(left, top);
		points[2] = new Point(right, top);
		points[3] = new Point(right, bottom);
		points[4] = new Point(left, bottom);
		
		ring = new LinearRing(points);
		
		bbox = new Polygon(new LinearRing[] {ring});
		bbox.srid = 4326;
		
		return bbox;
	}
	
	
	/**
	 * Creates a linestring from a list of points.
	 * 
	 * @param points
	 *            The points making up the line.
	 * @return The linestring.
	 */
	public LineString createLinestring(List<Point> points) {
		LineString lineString;
		
		lineString = new LineString(points.toArray(new Point[]{}));
		lineString.srid = 4326;
		
		return lineString;
	}


    /**
     * @param nodeId
     *             Id of the node.
     * @return Point object
     */
    public Point createPoint(long nodeId) {
	    NodeLocation nodeLocation = locationStore.getNodeLocation(nodeId);
        Point point = new Point(nodeLocation.getLongitude(), nodeLocation.getLatitude());
        point.srid = 4326;

        return point;
    }

	
	/**
	 * Builds a bounding box geometry object from the node references in the
	 * specified way. Unknown nodes will be ignored.
	 * 
	 * @param way
	 *            The way to create the bounding box for.
	 * @return The bounding box surrounding the way.
	 */
	public Polygon createWayBbox(Way way) {
		double left;
		double right;
		double top;
		double bottom;
		boolean nodesFound;
		
		nodesFound = false;
		left = 0;
		right = 0;
		bottom = 0;
		top = 0;
		for (WayNode wayNode : way.getWayNodes()) {
			NodeLocation nodeLocation;
			double longitude;
			double latitude;
			
			nodeLocation = locationStore.getNodeLocation(wayNode.getNodeId());
			longitude = nodeLocation.getLongitude();
			latitude = nodeLocation.getLatitude();
			
			if (nodeLocation.isValid()) {
				if (nodesFound) {
					if (longitude < left) {
						left = longitude;
					}
					if (longitude > right) {
						right = longitude;
					}
					if (latitude < bottom) {
						bottom = latitude;
					}
					if (latitude > top) {
						top = latitude;
					}
				} else {
					left = longitude;
					right = longitude;
					bottom = latitude;
					top = latitude;
					
					nodesFound = true;
				}
			}
		}
		
		return createWayBbox(left, right, bottom, top);
	}
	
	
	/**
	 * Builds a linestring geometry object from the node references in the
	 * specified way. Unknown nodes will be ignored.
	 * 
	 * @param way
	 *            The way to create the linestring for.
	 * @param enableKeepPartialLinestring
	 *            If true, the way linestring is build even on invalid or missing
	 *            nodes.
	 * @return The linestring representing the way.
	 */
	public LineString createWayLinestring(Way way, boolean enableKeepPartialLinestring) {
		List<Point> linePoints;
		
		linePoints = new ArrayList<Point>();
		for (WayNode wayNode : way.getWayNodes()) {
			NodeLocation nodeLocation;
			
			nodeLocation = locationStore.getNodeLocation(wayNode.getNodeId());
	
			if (nodeLocation.isValid()) {
				linePoints.add(new Point(nodeLocation.getLongitude(), nodeLocation.getLatitude()));
			} else {
				if (!enableKeepPartialLinestring) {
					return null;
				}
			}
		}
		
		if (enableKeepPartialLinestring && way.isClosed() && linePoints.size() > 1
			&& !linePoints.get(0).equals(linePoints.get(linePoints.size() - 1))) {
			linePoints.add(linePoints.get(0));
		}
		
		return createLinestring(linePoints);
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public void close() {
		locationStore.close();
	}
}
