/*
 * Copyright (C) 2014 Gerd Petermann
 *
 * 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.util;


import java.util.LinkedHashSet;
import java.util.Set;
import uk.me.parabola.imgfmt.app.Coord;


/**
 * A kd-tree (2D) implementation to solve the nearest neighbor problem.
 * The tree is not explicitly balanced.
 * 
 * @author Gerd Petermann
 *
 */
public class KdTree <T extends Locatable> {
	private static final boolean ROOT_NODE_USES_LONGITUDE = false;
	
	private class KdNode {
		T point;
		KdNode left;
		KdNode right;

		KdNode(T p) {
			point = p;
		}
	}
	// the tree root
    private KdNode root;
    // number of saved objects  
    private int size;

    // helpers 
    private T nextPoint ;
    private double minDist;
    private double maxDist;
    private Set<T> set;

    /**
     *  create an empty tree
     */
	public KdTree() {
		root = null;
	}

	public long size()
	{
		return size;
	}

	
	/**
	 * Start the add action with the root
	 * @param toAdd
	 */
	public void add(T toAdd) {
		size++;
		root = add(toAdd, root, ROOT_NODE_USES_LONGITUDE);
	}

	/**
	 * Compares the given axis of both points. 
	 * @param longitude <code>true</code>: compare longitude; <code>false</code> compare latitude
	 * @param c1 a point
	 * @param c2 another point
	 * @return <code>true</code> the axis value of c1 is smaller than c2; 
	 * 		<code>false</code> the axis value of c1 is equal or larger than c2
	 */
	private boolean isSmaller(boolean longitude, Coord c1, Coord c2) {
		if (longitude) {
			return c1.getLongitude() < c2.getLongitude();
		} else {
			return c1.getLatitude() < c2.getLatitude();
		}
	}
	
	/**
	 * Recursive routine to find the right place for inserting 
	 * into the tree.  
	 * @param toAdd the point
	 * @param tree the subtree root node where to add (maybe <code>null</code>)
	 * @param useLongitude <code>true</code> the tree node uses longitude for comparison; 
	 * 		<code>false</code> the tree node uses latitude for comparison
	 * @return the subtree root node after insertion
	 */
    private KdNode add( T toAdd, KdNode tree,  boolean useLongitude){
        if( tree == null ) {
            tree = new KdNode( toAdd );
        } else {
        	if(isSmaller(useLongitude, toAdd.getLocation(), tree.point.getLocation())) {
        		tree.left = add(toAdd, tree.left, !useLongitude);
        	} else {
        		tree.right = add(toAdd, tree.right, !useLongitude);
        	}
        }
        return tree;
    }
    
	/**
	 * Searches for the point that has smallest distance to the given point.
	 * @param p the point to search for
	 * @return the point with shortest distance to <var>p</var>
	 */
	public T findNextPoint(Locatable p) {
		// reset 
		minDist = Double.MAX_VALUE;
		maxDist = -1;
		set = null;
		nextPoint = null;
		
		// false => first node is a latitude level
		return findNextPoint(p, root, ROOT_NODE_USES_LONGITUDE);
	}

	/**
	 * Searches for the point that has smallest distance to the given point.
	 * @param p the point to search for
	 * @return the point with shortest distance to <var>p</var>
	 */
	public Set<T> findNextPoint(Locatable p, double maxDist) {
		// reset 
		minDist = Double.MAX_VALUE;
		this.maxDist = Math.pow(maxDist * 360 / Coord.U, 2); // convert maxDist in meter to distanceInDegreesSquared
		nextPoint = null;
		this.set = new LinkedHashSet<>();
		// false => first node is a latitude level
		findNextPoint(p, root, ROOT_NODE_USES_LONGITUDE);
		return set;
	}

	private T findNextPoint(Locatable p, KdNode tree, boolean useLongitude) {
		boolean continueWithLeft = false;
		if (tree == null)
			return nextPoint;
		
		if (tree.left == null && tree.right == null){
			double dist = tree.point.getLocation().distanceInDegreesSquared(p.getLocation());
			if (dist <= maxDist && set != null){
				set.add(tree.point);
			}
			if (dist < minDist){
				nextPoint = tree.point;
				if (dist < maxDist)
					minDist = maxDist;
				else
					minDist = dist;
			}
			return nextPoint;
		}
		else {
			if (isSmaller(useLongitude, p.getLocation(), tree.point.getLocation())){
				continueWithLeft = false;
				nextPoint = findNextPoint(p, tree.left, !useLongitude);
			}
			else {
				continueWithLeft = true;
				nextPoint = findNextPoint(p, tree.right, !useLongitude);
			}
		}
		
		double dist = tree.point.getLocation().distanceInDegreesSquared(p.getLocation());
		if (dist <= maxDist && set != null)
			set.add(tree.point);
		if (dist < minDist){
			nextPoint = tree.point;
			minDist = dist;
			if (dist < maxDist)
				minDist = maxDist;
			else
				minDist = dist;
		} 		
		// do we have to search the other part of the tree?
		Coord test;
		if (useLongitude)
			test = Coord.makeHighPrecCoord(p.getLocation().getHighPrecLat(), tree.point.getLocation().getHighPrecLon());
		else
			test = Coord.makeHighPrecCoord(tree.point.getLocation().getHighPrecLat(), p.getLocation().getHighPrecLon());
		if (test.distanceInDegreesSquared(p.getLocation()) < minDist){
			if (continueWithLeft) 
				nextPoint = findNextPoint(p, tree.left, !useLongitude);
			else
				nextPoint = findNextPoint(p, tree.right, !useLongitude);
		}
		return nextPoint;
	}
} 
