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

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

import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.general.CityInfo;
import uk.me.parabola.mkgmap.general.MapRoad;
import uk.me.parabola.mkgmap.general.ZipCodeInfo;
import uk.me.parabola.mkgmap.reader.osm.Node;
import uk.me.parabola.mkgmap.reader.osm.TagDict;
import uk.me.parabola.mkgmap.reader.osm.Way;

/**
 * Represents a (part of an) addr:interpolation way.
 * It contains the points between two house number elements
 * and the information how numbers are interpolated along
 * the way that is described by these points. 
 * 
 * We try to use the information to find 
 * a) the right road for the houses
 * b) the right road segment 
 * c) the position of the interpolated houses
 * 
 * We have to be aware of several special cases so we use some
 * flags to say for which of the above points the information
 * can be used.
 * 
 * @author Gerd Petermann
 *
 */
public class HousenumberIvl {
	private static final Logger log = Logger.getLogger(HousenumberIvl.class);
	
	/** Gives the maximum distance between house number element and the matching road 
	 * when the number is part of an addr:interpolation way */
	public static final double MAX_INTERPOLATION_DISTANCE_TO_ROAD = 75.0;

	private final String streetName;
	private final Way interpolationWay;
	private MapRoad roadForInterpolatedHouses;
	private final Node n1,n2;
	private List<Coord> points;
	private int step, start, end, steps;
	private HousenumberMatch[] knownHouses = {null, null}; 
	
	private boolean hasMultipleRoads;
	private boolean foundCluster;
	private int interpolated; // counter to detect wrong double use 
	private boolean ignoreForInterpolation;

	private boolean equalEnds;
	private static final short TKM_STREET = TagDict.getInstance().xlate("mkgmap:street");
	private static final short TKM_HOUSENUMBER = TagDict.getInstance().xlate("mkgmap:housenumber");		
	private static final short TK_ADDR_INTERPOLATION = TagDict.getInstance().xlate("addr:interpolation");

	
	public HousenumberIvl(String streetName, Way interpolationWay, Node n1, Node n2) {
		this.streetName = streetName;
		this.interpolationWay = interpolationWay;
		this.n1 = n1;
		this.n2 = n2;
	}
	
	public void setPoints(List<Coord> points) {
		this.points = new ArrayList<>(points);
	}
	public void setStep(int step) {
		this.step = step;
	}
	public int getStep() {
		return step;
	}
	public void setStart(int start) {
		this.start = start;
	}

	public int getStart() {
		return start;
	}
	public void setEnd(int end) {
		this.end = end;
	}
	public int getEnd() {
		return end;
	}
	public void calcSteps() {
		if (start < end){
			steps = (end - start) / step - 1;
		} else {
			steps = (start - end) / step - 1;
		}
	}


	public Node getNode1() {
		return n1;
	}

	public Node getNode2() {
		return n2;
	}

//	public boolean needsSplit(){
//		return needsSplit;
//	}
	
	public void addHousenumberMatch(HousenumberMatch house) {
		if (house.getElement() == n1)
			knownHouses[0] = house;
		else if (house.getElement() == n2)
			knownHouses[1] = house;
		else {
			log.error("cannot add",house,"to",this);
		}
	}
	
	public boolean checkRoads(){
		boolean res = checkRoads2();
		if (!res || equalEnds){
			// the interval is not ok --> ignore the numbers as well
			ignoreNodes();
		}  
		return res;
	}
	
	private boolean checkRoads2(){
		for (int i = 0; i < 2; i++){
			if (knownHouses[i] == null ){
				log.error("internal error: housenumber matches not properly set", this);
				return false;
			}
			if (knownHouses[i].getRoad() == null || knownHouses[i].getDistance() > 100 ){
				log.warn("cannot find any reasonable road for both nodes, ignoring them",streetName,this);
				return false;
			}
		}
		if (knownHouses[0].getRoad().getRoadDef().getId() == knownHouses[1].getRoad().getRoadDef().getId()){
			if (knownHouses[0].getRoad() != knownHouses[1].getRoad()){
				// special case: interval goes along clipped road, data is probably OK
				hasMultipleRoads = true;
				return true;
			}
			for (MapRoad r : knownHouses[0].getAlternativeRoads()){
				if (r.getRoadDef().getId() == knownHouses[0].getRoad().getRoadDef().getId()){
					// special case: interval may go along clipped road, data is probably OK
					hasMultipleRoads = true;
					return true;
				}
			}			
		}
		MapRoad bestRoad = null;
		// make sure that the closest road is one with a matching name
		for (int i = 0; i < 2; i++){
			while (!streetName.equals(knownHouses[i].getRoad().getStreet()) && knownHouses[i].hasAlternativeRoad()) {
				HousenumberMatch testx = new HousenumberMatch(knownHouses[i]);
				MapRoad r = knownHouses[i].getAlternativeRoads().remove(0);
				if (streetName.equals(r.getStreet())){
					HousenumberGenerator.findClosestRoadSegment(testx, r);
					if (testx.getDistance() < MAX_INTERPOLATION_DISTANCE_TO_ROAD){
						copyRoadData(testx, knownHouses[i]);
					}
				}
			}
		}
		List<MapRoad> toTest = new ArrayList<>();
		toTest.add(knownHouses[0].getRoad());
		toTest.add(knownHouses[1].getRoad());
		for (MapRoad r : knownHouses[0].getAlternativeRoads()){
			if (knownHouses[1].getAlternativeRoads().contains(r))
				toTest.add(r);
		}
		HousenumberMatch[] test = new HousenumberMatch[2];
		HousenumberMatch[] closest = new HousenumberMatch[2];
		boolean foundSingleRoad = false;
		for (MapRoad r : toTest){
			if (!streetName.equals(r.getStreet()))
				continue;
			foundSingleRoad = true;
			for (int i = 0; i < 2; i++){
				test[i] = knownHouses[i];
				if (test[i].getRoad() != r){
					test[i] = new HousenumberMatch(knownHouses[i]);
					HousenumberGenerator.findClosestRoadSegment(test[i], r);
					test[i].calcRoadSide();
				}
				if (test[i].getRoad() == null || test[i].getDistance() > MAX_INTERPOLATION_DISTANCE_TO_ROAD) {
					foundSingleRoad = false;
					break;
				}
			}
			if (foundSingleRoad){
				if (test[0].isLeft() != test[1].isLeft()){
					foundSingleRoad = false;
					continue;
				}
				int s0 = test[0].getSegment();
				int s1 = test[1].getSegment();
				// check if the road and the addr:interpolation way are nearly parallel lines
				double angle1 = Utils.getAngle(test[0].getClosestPointOnRoad(), points.get(0),points.get(1));
				if (Math.abs(angle1) < 30){
					foundSingleRoad = false;
					HousenumberMatch testx = new HousenumberMatch(test[0]);
					for (int s = Math.min(s0,s1); s <= Math.max(s0, s1); s++){
						if (s != test[0].getSegment()){
							HousenumberGenerator.findClosestRoadSegment(testx, r, s,s+1);
							angle1 = Utils.getAngle(testx.getClosestPointOnRoad(), points.get(0),points.get(1));
							if (Math.abs(angle1) >= 30 && testx.getDistance() < 2*test[0].getDistance()){
								test[0] = testx;
								foundSingleRoad = true;
								break;
							}
						}
					}
				}
				double angle2 = Utils.getAngle(points.get(points.size()-2),points.get(points.size()-1),test[1].getClosestPointOnRoad());
				if (Math.abs(angle2) < 30){
					foundSingleRoad = false;
					HousenumberMatch testx = new HousenumberMatch(test[1]);
					for (int s = Math.min(s0,s1); s <= Math.max(s0, s1); s++){
						if (s != test[1].getSegment()){
							HousenumberGenerator.findClosestRoadSegment(testx, r, s,s+1);
							angle2 = Utils.getAngle(points.get(points.size()-2),points.get(points.size()-1),testx.getClosestPointOnRoad());
							if (Math.abs(angle2) >= 30 && testx.getDistance() < 2*test[1].getDistance()){
								test[1] = testx;
								foundSingleRoad = true;
								break;
							}
						}
					}
				}
			}
			if (foundSingleRoad) {
				if (!r.isNamedByHousenumberProcessing())
					break;
				// the closest road was originally unnamed , try to find one that is named in OSM 
				if (bestRoad == null) {
					bestRoad = r;
					closest[0] = test[0];
					closest[1] = test[1];
				}
			}
		}
		if (!foundSingleRoad && bestRoad != null) {
			// not matching road name in original OSM data, use the closest
			foundSingleRoad = true;
			test[0] = closest[0];
			test[1] = closest[1];
		}
		if (!foundSingleRoad){
			if (!streetName.equals(knownHouses[0].getRoad().getStreet()) || !streetName.equals(knownHouses[1].getRoad().getStreet())) {
				log.warn("cannot find reasonable road for both nodes",streetName,this);
				return false;
			}
			hasMultipleRoads = true;
			return true;
		}
		for (int i = 0; i < 2; i++) {
			if (test[i].getSegmentFrac() < 0 || test[i].getSegmentFrac() > 1) {
				// might still be one road that bends but that will not cause trouble 
				hasMultipleRoads = true;
				return true;
			}
		}

		// we found a single road instance that should be used for all interpolated numbers
		roadForInterpolatedHouses = test[0].getRoad();

		// we found a single plausible road, make sure that both nodes are using it
		for (int i = 0; i < 2; i++) {
			if (knownHouses[i].getRoad() != test[i].getRoad() || knownHouses[i].getSegment() != test[i].getSegment()) {
				copyRoadData(test[i], knownHouses[i]);
				knownHouses[i].forgetAlternativeRoads();
			}
		}
		if (knownHouses[0].isLeft() != knownHouses[1].isLeft()) {
			log.warn("addr:interpolation way crosses road", streetName, this);
			return false;
		}
		return true;
	}

	private void copyRoadData(HousenumberMatch source, HousenumberMatch dest) {
		if (log.isInfoEnabled()){
			if (source.getRoad() != dest.getRoad())
				log.info("moving",streetName,dest.getSign(),dest.toBrowseURL(),"from road",dest.getRoad(),"to road",source.getRoad());
			else 
				log.info("moving",streetName,dest.getSign(),dest.toBrowseURL(),"from segment",dest.getSegment(),"to", source.getSegment(),"in road",source.getRoad());
		}
		dest.setRoad(source.getRoad());
		dest.setSegment(source.getSegment());
		dest.setSegmentFrac(source.getSegmentFrac());
		dest.setDistance(source.getDistance());
		dest.calcRoadSide();
	}
	
	public List<HousenumberMatch> getInterpolatedHouses(){
		List<HousenumberMatch> houses = new ArrayList<>();
		if (ignoreForInterpolation|| start == end || steps <= 0)
			return houses;
		List<Coord> interpolatedPoints = getInterpolatedPoints();
		int usedStep = (start < end) ? step : -step;
		int hn = start;
		boolean distanceWarningIssued = false;
		CityInfo ci = knownHouses[0].getCityInfo();
		ZipCodeInfo zip = knownHouses[0].getZipCode();
		if (ci != null && !ci.equals(knownHouses[1].getCityInfo()))
			log.warn("addr:interpolation way connects houses in different cities",streetName,this,"using city",ci,"for all interpolated adresses");
		if (zip != null && !zip.equals(knownHouses[1].getZipCode()))
			log.warn("addr:interpolation way connects houses with differnt zip codes",streetName,this,"using zip code",zip,"for all interpolated adresses");
		
		for (Coord co : interpolatedPoints) {
			hn += usedStep;
			Node generated = new Node(interpolationWay.getId(), co);
			generated.markAsGeneratedFrom(interpolationWay);
			generated.addTag(TKM_STREET, streetName);
			String number = String.valueOf(hn);
			generated.addTag(TKM_HOUSENUMBER, number);
			// TODO: maybe add check that city info and zip code of both houses is equal ?
			// what if not ?
			HousenumberElem houseElem = new HousenumberElem(generated, ci);
			houseElem.setHousenumber(hn);
			houseElem.setZipCode(zip);
			houseElem.setStreet(streetName);
			houseElem.setSign(number);
			HousenumberMatch house = new HousenumberMatch(houseElem);
			if (roadForInterpolatedHouses != null){
				HousenumberGenerator.findClosestRoadSegment(house, roadForInterpolatedHouses);
				if (house.getRoad() == null || house.getDistance() > MAX_INTERPOLATION_DISTANCE_TO_ROAD ){
					if (!distanceWarningIssued){
						log.warn("interpolated house is not close to expected road",this,house);
						distanceWarningIssued = true;
					}
					continue;
				}
				house.calcRoadSide();
			}
			house.setInterpolated(true);
			houses.add(house);
		}
		
		if (log.isDebugEnabled()){
			String addrInterpolationMethod = interpolationWay.getTag(TK_ADDR_INTERPOLATION);
			if (!hasMultipleRoads)
				log.debug(this,"generated",addrInterpolationMethod,"interpolated number(s) for",knownHouses[0].getRoad());
			else 
				log.debug(this,"generated",addrInterpolationMethod,"interpolated number(s) for",streetName);
		}
		return houses;
	}
	/**
	 * Calculate the wanted number of coords on a way so that they have
	 * similar distances to each other (and to the first and last point 
	 * of the way).
	 * @param points list of points that build the way
	 * @param num the wanted number 
	 * @return a list with the number of points or the empty list in 
	 * case of errors
	 */
	public List<Coord> getInterpolatedPoints(){
		if (interpolated > 0){
			log.debug("interpolating numbers again for", this );
		}
		interpolated++;
		if (steps < 1 || points.size() < 2)
			return Collections.emptyList();

		List<Coord> interpolated = new ArrayList<>(steps);
		double wayLen = 0;
		for (int i = 0; i+1 < points.size(); i++){
			wayLen += points.get(i).distance(points.get(i+1));
		}
		double ivlLen = wayLen / (steps+1);
		if (ivlLen < 0.1){
			if (log.isInfoEnabled())
				log.info("addr:interpolation",interpolationWay.toBrowseURL(),"segment ignored, would generate",steps,"houses with distance of",ivlLen,"m");
			return interpolated;
		}
		int pos = 0;
		double rest = 0;
		while (pos+1 < points.size()){
			Coord c1 = points.get(pos);
			Coord c2 = points.get(pos+1);
			pos++;
			double neededPartOfSegment = 0;
			double segmentLen = c1.distance(c2);
			for(;;){
				neededPartOfSegment += ivlLen - rest;
				if (neededPartOfSegment <= segmentLen){
					double fraction = neededPartOfSegment / segmentLen;
					Coord c = c1.makeBetweenPoint(c2, fraction);
					interpolated.add(c);
					if (interpolated.size() >= steps){
						return interpolated;
					}
					rest = 0;
				} else {
					rest = segmentLen - neededPartOfSegment + ivlLen;
					break;
				}
			}
			
		}
		log.warn("addr:interpolation",interpolationWay.toBrowseURL(),"interpolation for segment with nodes",n1.getId(),n2.getId(),"failed");
		return interpolated;
	}
	
	public String toString() {
		return interpolationWay.toBrowseURL() + " " + start + ".." + end + ", step=" + step;
	}

	public String getDesc() {
		return streetName + "_" + start + ".." + end + "_" + step;
	}

//	public boolean setNodeRefs(HashMap<Element, HousenumberMatch> houses) {
//		knownHouses[0] = houses.get(n1);
//		knownHouses[1] = houses.get(n2);
//		if (knownHouses[0] == null || knownHouses[1] == null)
//			return false;
//		knownHouses[0].incIntervalInfoRefs();
//		knownHouses[1].incIntervalInfoRefs();
//		return true;
//	}
//
	public void ignoreNodes() {
		for (int i = 0; i < 2; i++){
			if (knownHouses[i] != null){
				knownHouses[i].decIntervalInfoRefs();
				if (knownHouses[i].getIntervalInfoRefs() == 0)
					knownHouses[i].setIgnored(true);
			}
		}
	}

	public long getId() {
		return interpolationWay.getId();
	}

	
	public boolean ignoreForInterpolation() {
		return ignoreForInterpolation;
	}

	public void setIgnoreForInterpolation(boolean ignoreForInterpolation) {
		this.ignoreForInterpolation = ignoreForInterpolation;
	}

	public boolean isBad() {
		return false;
	}


	public boolean inCluster(List<HousenumberMatch> housesNearCluster) {
		int count = 0;
		for (HousenumberMatch house : housesNearCluster){
			if (knownHouses[0] == house || knownHouses[1] == house){
				++count;
				
			}
			if (count == 2)
				break;
		}
		if (count > 0){
			foundCluster = true;
			return true;
		}
		return false;
	}

	public void setEqualEnds() {
		this.equalEnds = true;
		
	}

	public boolean setNodeRefs(Map<Long, Integer> interpolationNodes,
			List<HousenumberElem> houseElems) {
		for (int i = 0; i < 2; i++){
			long id = (i == 0) ? n1.getId(): n2.getId();
			Integer elemPos = interpolationNodes.get(id);
			if (elemPos == null || elemPos >= houseElems.size())
				return false;
			HousenumberElem he = houseElems.get(elemPos);
			if (!(he instanceof HousenumberMatch) || he.getElement().getId() != id)
				return false;
			knownHouses[i] = (HousenumberMatch) he;
			knownHouses[i].incIntervalInfoRefs();
		}
		return true;
	}
	
	public HousenumberMatch[] getHouseNodes (){
		return knownHouses;
	}

	/**
	 * Check if an address node can be used to calculate new intervals 
	 * @param houseToAdd a single address from OSM data 
	 * @return null in case of error or an array with two new {@link HousenumberIvl} instances  
	 */
	public HousenumberIvl[] trySplitAt(HousenumberMatch houseToAdd) {
		if (houseToAdd.isInterpolated())
			return null;
		if (houseToAdd.getRoad() != knownHouses[0].getRoad() && houseToAdd.getRoad() != knownHouses[1].getRoad())
			return null;
		HousenumberMatch s = knownHouses[0];
		HousenumberMatch e = knownHouses[1];
		if (s.getSegment() > e.getSegment() || s.getSegment() == e.getSegment() && s.getSegmentFrac() > e.getSegmentFrac()){
			s = knownHouses[1];
			e = knownHouses[0];
		}
		if (houseToAdd.getSegment() < s.getSegment() || houseToAdd.getSegment() > e.getSegment())
			return null;
		if (houseToAdd.getSegment() == s.getSegment() && houseToAdd.getSegmentFrac() < s.getSegmentFrac())
			return null;
		if (houseToAdd.getSegment() == e.getSegment()  && houseToAdd.getSegmentFrac() > e.getSegmentFrac())
			return null;
		
		for (int i = 0; i+1 < points.size(); i++){
			Coord c1 = points.get(i);
			Coord c2 = points.get(i + 1);
			double frac = HousenumberGenerator.getFrac(c1, c2, houseToAdd.getLocation());
			if (frac < 0 || frac > 1)
				continue;
			HousenumberIvl[] ivls = new HousenumberIvl[2];
			HousenumberMatch hnm = null;
			if (houseToAdd.element instanceof Node){
				hnm = houseToAdd;
			}
			else {
				// create a Node instance 
				Node toAdd = new Node(houseToAdd.getElement().getId(), houseToAdd.getLocation());
				toAdd.markAsGeneratedFrom(houseToAdd.getElement());
				toAdd.copyTags(houseToAdd.element);
				HousenumberElem hnElem = new HousenumberElem(toAdd, houseToAdd.getCityInfo());
				hnm = new HousenumberMatch(hnElem);
				hnm.setZipCode(houseToAdd.getZipCode());
				HousenumberGenerator.findClosestRoadSegment(hnm, houseToAdd.getRoad(), houseToAdd.getSegment(), houseToAdd.getSegment());
			}
			
			List<Coord> points1 = new ArrayList<>();
			List<Coord> points2 = new ArrayList<>();
			points1.addAll(points.subList(0, i+1));
			points1.add(houseToAdd.getLocation());
			points2.add(houseToAdd.getLocation());
			points2.addAll(points.subList(i+1, points.size()));
			
			ivls[0] = new HousenumberIvl(streetName, interpolationWay, n1, (Node)hnm.element);
			ivls[0].setStart(knownHouses[0].getHousenumber());
			ivls[0].setEnd(houseToAdd.getHousenumber());
			ivls[0].setStep(step);
			ivls[0].calcSteps();
			ivls[0].setPoints(points1);
			ivls[0].addHousenumberMatch(knownHouses[0]);
			ivls[0].addHousenumberMatch(hnm);

			ivls[1] = new HousenumberIvl(streetName, interpolationWay, (Node) hnm.element, n2);
			ivls[1].setStart(houseToAdd.getHousenumber());
			ivls[1].setEnd(knownHouses[1].getHousenumber());
			ivls[1].setStep(step);
			ivls[1].calcSteps();
			ivls[1].setPoints(points2);
			ivls[1].addHousenumberMatch(knownHouses[1]);
			ivls[1].addHousenumberMatch(hnm);
			return ivls;
		}
		return null;
	}
}
