/*
 * Copyright (C) 2013.
 *
 * 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.osmstyle.housenumber;

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

import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.mkgmap.general.MapRoad;
import uk.me.parabola.mkgmap.reader.osm.Way;

/**
 * Stores the matching data between a housenumber and its road.
 * @author WanMil
 */
public class HousenumberMatch extends HousenumberElem {
	private MapRoad road;
	private HousenumberRoad housenumberRoad;
	
	private double distance = Double.POSITIVE_INFINITY;
	private int segment = -1;
	private boolean left;
	
	private double segmentFrac;
	
	private boolean ignored;
	private boolean isDuplicate;
	private boolean interpolated;
	private int moved;
	// distance in m between closest point on road and the point that is found in the address search
	private double searchDist = Double.NaN;
	private boolean isFarDuplicate;
	private HousenumberGroup group;
	private List<MapRoad> alternativeRoads;
	private int intervalInfoRefs; // counter
	
	public HousenumberMatch(HousenumberElem he) {
		super(he);
	}

	public MapRoad getRoad() {
		return road;
	}

	public void setRoad(MapRoad road) {
		this.road = road;
	}

	/**
	 * Retrieves the distance to the road.
	 * @return distance in m
	 */
	public double getDistance() {
		return distance;
	}

	/**
	 * Sets the distance to the road
	 * @param distance distance in m
	 */
	public void setDistance(double distance) {
		this.distance = distance;
	}

	/**
	 * Retrieves the segment number the house number belongs to.
	 * @return the segment number
	 */
	public int getSegment() {
		return segment;
	}

	/**
	 * Sets the segment number the house number belongs to.
	 * @param segment the segment number
	 */
	public void setSegment(int segment) {
		this.segment = segment;
	}

	public boolean isLeft() {
		return left;
	}

	/**
	 * Sets if the house number is on the left or right side of the street.
	 * @param left {@code true} left side; {@code false} right side
	 */
	public void setLeft(boolean left) {
		this.left = left;
	}

	/**
	 * Retrieve the relative position of this house number within the segement
	 * of the related road.
	 * @return the relative position within the roads segment
	 */
	public double getSegmentFrac() {
		return segmentFrac;
	}

	/**
	 * Sets the relative position of this house number within its segment
	 * of the related road.
	 * @param segmentFrac relative position within the segment
	 */
	public void setSegmentFrac(double segmentFrac) {
		this.segmentFrac = segmentFrac;
	}

	public boolean hasAlternativeRoad() {
		return alternativeRoads != null && !alternativeRoads.isEmpty();
	}

	public boolean isIgnored() {
		return ignored;
	}

	public void setIgnored(boolean ignored) {
		this.ignored = ignored;
	}

	public boolean isDuplicate() {
		return isDuplicate;
	}

	public void setDuplicate(boolean isDuplicate) {
		this.isDuplicate = isDuplicate;
	}

	public boolean isInterpolated() {
		return interpolated;
	}

	public void setInterpolated(boolean interpolated) {
		this.interpolated = interpolated;
	}

	public int getMoved() {
		return moved;
	}

	public void incMoved() {
		this.moved++;
	}

	public double getSearchDist() {
		return searchDist;
	}

	public void setSearchDist(double searchDist) {
		this.searchDist = searchDist;
	}

	public String toString() {
		String s1 = String.valueOf(getHousenumber());
		if (getSign().length() > 2 + s1.length())
			return s1 + "("+segment+")";
		return getSign() + "("+segment+")";
	}

	public void setFarDuplicate(boolean b) {
		this.isFarDuplicate = b;
		
	}

	public boolean isFarDuplicate() {
		return isFarDuplicate;
	}

	/**
	 * @return either an existing point on the road
	 * or the calculated perpendicular. In the latter case
	 * the highway count is zero.
	 *   
	 */
	public Coord getClosestPointOnRoad(){
		if (segmentFrac <= 0)
			return getRoad().getPoints().get(segment);
		if (segmentFrac >= 1)
			return getRoad().getPoints().get(segment+1);
		Coord c1 = getRoad().getPoints().get(segment);
		Coord c2 = getRoad().getPoints().get(segment+1);
		return c1.makeBetweenPoint(c2, segmentFrac);
	}

	/**
	 * @param other a different house on the same road
	 * @return  the distance in m between the perpendiculars on the road
	 * of two houses.
	 */
	public double getDistOnRoad(HousenumberMatch other) {
		if (getRoad() != other.getRoad()){
			assert false : "cannot compute distance on road for different roads"; 
		}
		List<Coord> points = getRoad().getPoints();
		HousenumberMatch house1 = this;
		HousenumberMatch house2 = other;
		if (house1.segment > house2.segment || house1.segment == house2.segment && house1.segmentFrac > house2.segmentFrac){
			house1 = other;
			house2 = this;
		}
		int s1 = house1.segment;
		int s2 = house2.segment;
		double distOnRoad = 0;
		while (s1 < s2){
			double segLen = points.get(s1).distance(points.get(s1 + 1));
			if (s1 == house1.getSegment() && house1.getSegmentFrac() > 0){
				// rest of first segment
				distOnRoad += Math.max(0, 1-house1.getSegmentFrac()) * segLen;
			} else
				distOnRoad += segLen;
			s1++;
		}
		double segLen = points.get(s1).distance(points.get(s1 + 1));
		if (house2.getSegmentFrac() > 0)
			distOnRoad += Math.min(1, house2.getSegmentFrac()) * segLen;
		if (house1.getSegmentFrac() > 0 && s1 == house1.segment)
			distOnRoad -= Math.min(1, house1.getSegmentFrac()) * segLen;
		return distOnRoad;
	}

	public HousenumberRoad getHousenumberRoad() {
		return housenumberRoad;
	}

	public void setHousenumberRoad(HousenumberRoad housenumberRoad) {
		this.housenumberRoad = housenumberRoad;
	}
	
	public void setGroup(HousenumberGroup housenumberBlock) {
		this.group = housenumberBlock;
	}

	public HousenumberGroup getGroup() {
		return group;
	}

	public void addAlternativeRoad(MapRoad road2) {
		if (alternativeRoads == null){
			alternativeRoads = new ArrayList<>();
		}
		alternativeRoads.add(road2);
	}
	public List<MapRoad> getAlternativeRoads() {
		if (alternativeRoads == null)
			return Collections.emptyList();
		return alternativeRoads;
	}
	public void forgetAlternativeRoads(){
		alternativeRoads = null;
	}

	public int getIntervalInfoRefs() {
		return intervalInfoRefs;
	}

	public void incIntervalInfoRefs() {
		intervalInfoRefs++;
	}

	public void decIntervalInfoRefs() {
		if (intervalInfoRefs > 0)
			--intervalInfoRefs;
	}

	public boolean isDirectlyConnected(HousenumberMatch other){
		if (getElement() instanceof Way && other.getElement() instanceof Way){
			List<Coord> s1 = ((Way) getElement()).getPoints();
			List<Coord> s2 = ((Way) other.getElement()).getPoints();
			for (int i = 0; i+1 < s1.size(); i++){
			    Coord co = s1.get(i);
			    co.setPartOfShape2(false);
			}
			for (int i = 0; i+1 < s2.size(); i++){
			    Coord co = s2.get(i);
			    co.setPartOfShape2(true);
			}
			for (int i = 0; i+1 < s1.size(); i++){
			    Coord co = s1.get(i);
			    if (co.isPartOfShape2())
			    	return true;
			}
		}
		return false;
	}


	public void calcRoadSide(){
		if (getRoad() == null)
			return;
		Coord c1 = getRoad().getPoints().get(getSegment());
		Coord c2 = getRoad().getPoints().get(getSegment()+1);
		setLeft(HousenumberGenerator.isLeft(c1, c2, getLocation()));
		
	}
	
	public boolean isEqualAddress(HousenumberElem other){
		if (getRoad() != other.getRoad())
			return false;
		if (!getSign().equals(other.getSign()))
			return false;
		if (getPlace() != other.getPlace()) {
			if (getPlace() == null)
				return false;
			if (!getPlace().equals(other.getPlace()))
				return false;
		}
		if (getZipCode() != null && other.getZipCode() != null){
			if (!getZipCode().equals(other.getZipCode()))
				return false;
		}
		if (getCityInfo() != null && other.getCityInfo() != null){
			if (!getCityInfo().equals(other.getCityInfo()))
				return false;
		}
		return true;
	}
	
}

