/*
 * Copyright (C) 2011 - 2012.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or
 * 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.
 */

package uk.me.parabola.mkgmap.reader.osm;

import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;

import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.reader.osm.boundary.BoundaryGrid;
import uk.me.parabola.mkgmap.reader.osm.boundary.BoundaryQuadTree;
import uk.me.parabola.mkgmap.reader.osm.boundary.BoundaryUtil;
import uk.me.parabola.util.EnhancedProperties;

public class LocationHook implements OsmReadingHooks {
	private static final Logger log = Logger.getLogger(LocationHook.class);
	// the resulting assignments are logged with this extra logger
	// so that it is possible to log only the results of the location hook
	private static final Logger resultLog = Logger.getLogger(LocationHook.class.getName()+".results");
	
	// counters for stats
	private long cntQTSearch = 0;
	private long cntNotFnd = 0;
	private long cntwayNotFnd = 0;
	
	private BoundaryGrid boundaryGrid;

	private ElementSaver saver;
	
	private String boundaryDirName;

	
	/** this static object is used to synchronize the check if the bounds directory contains any bounds */
	private static final Object BOUNDS_CHECK_LOCK = new Object();
	
	/** Stores the name of the bounds dir/file that has been checked. Static so that multiple threads can access. */
	private static String checkedBoundaryDirName;
	/** stores the result of the bounds dir/file check */
	private static boolean checkBoundaryDirOk;
	
	private EnhancedProperties props;

	@Override
	public boolean init(ElementSaver saver, EnhancedProperties props, Style style) {
		boundaryDirName = props.getProperty("bounds");
		
		if (boundaryDirName == null) {
			// bounds property not set
			return false;
		}
		
		this.props = props;
		this.saver = saver;

		long t1 = System.currentTimeMillis();

		synchronized (BOUNDS_CHECK_LOCK) {
			// checking of the boundary dir is expensive
			// check once only and reuse the result
			if (boundaryDirName.equals(checkedBoundaryDirName)) {
				if (!checkBoundaryDirOk) {
					log.error("Disable LocationHook because bounds directory is unusable. Dir: "+boundaryDirName);
					return false;
				}
			} else {
				checkedBoundaryDirName = boundaryDirName;
				checkBoundaryDirOk = false;

				List<String> boundaryFiles = BoundaryUtil.getBoundaryDirContent(boundaryDirName);
				if (boundaryFiles == null || boundaryFiles.isEmpty()) {
					log.error("LocationHook is disabled because no bounds files are available. Dir: "
							+ boundaryDirName);
					return false;
				}
					// passed all checks => boundaries are okay
				checkBoundaryDirOk = true;
			}
		}
		log.info("Checking bounds dir took", (System.currentTimeMillis() - t1), "ms");
		return true;
	}

	@Override
	public void end() {
		long t1 = System.currentTimeMillis();
		log.info("Starting with location hook");

		Area nodesBounds = saver.getDataBoundingBox();
		if (nodesBounds != null) {
			Area bbox = saver.getBoundingBox();
			// calculate the needed bounding box
			Area searchBounds = bbox.intersect(nodesBounds);
			boundaryGrid = new BoundaryGrid(boundaryDirName, searchBounds, props);
			processLocationRelevantElements();

			boundaryGrid = null;
		}
		long dt = (System.currentTimeMillis() - t1);
		log.info("======= LocationHook Stats =====");
		log.info("QuadTree searches    :", cntQTSearch);
		log.info("unsuccesfull         :", cntNotFnd);
		log.info("unsuccesfull for ways:", cntwayNotFnd);
		log.info("Location hook finished in", dt, "ms");
	}

	/**
	 * Iterate over all elements for which the boundary assignment should be performed.
	 */
	private void processLocationRelevantElements() {
		// process all nodes that might be converted to a garmin node (tagcount > 0)
		for (Node node : saver.getNodes().values()) {
			if (node.getTagCount() > 0 && saver.getBoundingBox().contains(node.getLocation())) {
				processElem(node);
				if (resultLog.isDebugEnabled())
					resultLog.debug("N", node.getId(), locationTagsToString(node));
			}
		}

		// process  all ways that might be converted to a garmin way (tagcount > 0)
		for (Way way : saver.getWays().values()) {
			if (way.getTagCount() > 0) {
				processElem(way);
				if (resultLog.isDebugEnabled())
					resultLog.debug("W", way.getId(), locationTagsToString(way));
			}
		}
		
		// process all multipolygons - the add-pois-to-area function uses its
		// center point and its tags so the mp must be tagged itself with the bounds
		// tags
		for (Relation r : saver.getRelations().values()) {
			if (r instanceof MultiPolygonRelation) {
				// check if the mp could be processed
				Coord mpCenter = ((MultiPolygonRelation) r).getCofG();
				if (mpCenter != null && saver.getBoundingBox().contains(mpCenter)){
					// create a fake node for which the bounds information is collected
					Node mpNode = new Node(r.getOriginalId(), mpCenter);
					mpNode.markAsGeneratedFrom(r);
					processElem(mpNode);
					// copy the bounds tags back to the multipolygon
					for (String boundsTag : BoundaryQuadTree.mkgmapTagsArray) {
						String tagValue = mpNode.getTag(boundsTag);
						if (tagValue != null) {
							r.addTag(boundsTag, tagValue);
						}
					}
					if (resultLog.isDebugEnabled())
						resultLog.debug("R", r.getId(), locationTagsToString(r));
				}
			}
		}
	}

	/**
	 * Extract the location info and perform a test 
	 * against the BoundaryGrid. If found, assign the tags.  
	 * @param elem A way or Node
	 */
	private void processElem(Element elem){
		Tags tags = null;

		if (elem instanceof Node){
			Node node = (Node) elem;
			tags = search(node.getLocation());
		} else if (elem instanceof Way) {
			tags = processWayPoints(((Way) elem).getPoints());
			if (tags == null)
				++cntwayNotFnd;
		}

		if (tags == null) {
			++cntNotFnd;
		} else {
			// tag the element with all tags referenced by the boundary
			Iterator<Entry<Short,String>> tagIter = tags.entryShortIterator();
			while (tagIter.hasNext()) {
				Entry<Short,String> tag = tagIter.next();
				if (elem.getTag(tag.getKey()) == null){
					elem.addTag(tag.getKey(),tag.getValue());
				}
			}
		}
	}
	
	private Tags processWayPoints(final List<Coord> points) {
		// try the mid point of the way first
		final int n = points.size();
		final int middle = n / 2;
		final Coord midPoint;
		if (n == 2) {
			midPoint = points.get(0).makeBetweenPoint(points.get(1), 0.5);
		} else {
			midPoint = points.get(middle);
		}
		Tags tags = search(midPoint);
		if (tags == null) {
			// try 1st point next
			tags = search(points.get(0));
		}
		if (tags == null) {
			// try last point next
			tags = search(points.get(n-1));
		}
		if (tags == null) {
			// still not found, try rest
			for (int i = 1; i < n - 1; i++) {
				if (i != middle) {
					tags = search(points.get(i));
					if (tags != null)
						break;
				}
			}
		}
		return tags;
	}

	/**
	 * perform search in grid and maintain statistic counter
	 * @param co a point that is to be searched
	 * @return location relevant tags or null
	 */
	private Tags search(Coord co){
		if (saver.getBoundingBox().contains(co)){
			++cntQTSearch;
			return boundaryGrid.get(co);
		}
		return null;
	}
			
	/**
	 * Debugging:
	 * Create a string with location relevant tags ordered by admin_level.
	 * Can be used to compare results with tools like diff.
	 * @param elem the element 
	 * @return A new String object
	 */
	private static String locationTagsToString(Element elem){
		StringBuilder res = new StringBuilder();
		for (int i = BoundaryQuadTree.mkgmapTagsArray.length-1; i >= 0; --i){
			String tagVal = elem.getTag(BoundaryQuadTree.mkgmapTagsArray[i] );
			if (tagVal != null)
				res.append(tagVal);
			res.append(";");
		}
		return res.toString();
	}

}
