/*
 * Copyright (C) 2008 Steve Ratcliffe
 * 
 * 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.imgfmt.app.net;

import java.util.Objects;

import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.general.CityInfo;
import uk.me.parabola.mkgmap.general.ZipCodeInfo;

/**
 * Describes the house numbering from a node in the road.
 * @author Steve Ratcliffe
 */
public class Numbers {
	private static final Logger log = Logger.getLogger(Numbers.class);
	public static final boolean LEFT = true;
	public static final boolean RIGHT = false;

	private static final int MAX_DELTA = 131071; // see NumberPreparer

	/**
	 * Polish format: The point in the road where these numbers apply. Not written to NET!
	 */
	private int polishPointIndex;
	/**
	 * The position in the list of nodes (starting with 0). Negative value means it is not yet set. 
	 * Identifies the node (not point) in the road. This is needed to encode the bitstream in NET.    
	 */
	private int nodeIndex = -1;
	
	/** the data on side of the road */
	private RoadSide leftSide,rightSide;

	private class RoadSide {
		NumDesc numbers;
		// to be added
		CityInfo cityInfo;
		ZipCodeInfo zipCode;
		boolean isEmpty(){
			return cityInfo == null && zipCode == null && numbers == null;
		}
	}

	private class NumDesc{
		NumberStyle numberStyle;
		int start, end;

		public NumDesc(NumberStyle numberStyle, int start, int end) {
			this.numberStyle = numberStyle;
			this.start = start;
			this.end = end;
		}

		public boolean contained(int hn) {
			boolean isEven = (hn % 2 == 0);
			if (numberStyle == NumberStyle.BOTH 
					|| numberStyle == NumberStyle.EVEN && isEven
					|| numberStyle == NumberStyle.ODD && !isEven) {
				if (start <= end) {
					return start <= hn && hn <= end;
				} else {
					return end <= hn && hn <= start;
				}
			}
			return false;
		}

		@Override
		public String toString() {
			return String.format("%s,%d,%d", numberStyle, start,end);
		}

	}

	public Numbers() {
	}

	/**
	 * This constructor takes a comma separated list as in the polish format. Also used in testing as
	 * it is an easy way to set all common parameters at once.
	 *
	 * @param spec Node number, followed by left and then right parameters as in the polish format.
	 */
	public Numbers(String spec) {
		String[] strings = spec.split(",");
		polishPointIndex = Integer.parseInt(strings[0]);
		NumberStyle numberStyle = NumberStyle.fromChar(strings[1]);
		int start = Integer.parseInt(strings[2]);
		int end = Integer.parseInt(strings[3]);
		setNumbers(LEFT, numberStyle, start, end);
		numberStyle = NumberStyle.fromChar(strings[4]);
		start = Integer.parseInt(strings[5]);
		end = Integer.parseInt(strings[6]);
		setNumbers(RIGHT, numberStyle, start, end);

		if (strings.length > 8){
			// zip codes 
			String zip = strings[7];
			if (!"-1".equals(zip))
				setZipCode(LEFT, new ZipCodeInfo(zip));
			zip = strings[8];
			if (!"-1".equals(zip))
				setZipCode(RIGHT, new ZipCodeInfo(zip));
		}
		if (strings.length > 9) {
			String city, region, country;
			int nextPos = 9;
			city = strings[nextPos];
			if (!"-1".equals(city)) {
				region = strings[nextPos + 1];
				country = strings[nextPos + 2];
				setCityInfo(LEFT, new CityInfo(city, region, country));
				nextPos = 12;
			} else {
				nextPos = 10;
			}
			city = strings[nextPos];
			if (!"-1".equals(city)) {
				region = strings[nextPos + 1];
				country = strings[nextPos + 2];
				setCityInfo(RIGHT, new CityInfo(city, region, country));
			}
		}
	}

	public void setNumbers(boolean useLeft, NumberStyle numberStyle, int start, int end){
		if (numberStyle != NumberStyle.NONE || start != -1 || end != -1){
			RoadSide rs = assureSideIsAllocated(useLeft);
			rs.numbers = new NumDesc(numberStyle, start, end);
		} else {
			RoadSide rs = (useLeft) ? leftSide : rightSide;
			if (rs != null)
				rs.numbers = null;
			removeIfEmpty(useLeft);
		}
	}

	public void setCityInfo(boolean left, CityInfo ci){
		if (ci != null){
			RoadSide rs = assureSideIsAllocated(left);
			rs.cityInfo = ci;
		} else {
			RoadSide rs = (left) ? leftSide : rightSide;
			if (rs != null)
				rs.cityInfo = null;
			removeIfEmpty(left);
		}
	}

	public CityInfo getCityInfo(boolean left){
		RoadSide rs = (left) ? leftSide : rightSide;
		return (rs != null) ? rs.cityInfo : null;
	}

	public void setZipCode(boolean left, ZipCodeInfo zipCode){
		if (zipCode != null){
			RoadSide rs = assureSideIsAllocated(left);
			rs.zipCode = zipCode;
		} else {
			RoadSide rs = (left) ? leftSide : rightSide;
			if (rs != null)
				rs.zipCode= null;
			removeIfEmpty(left);
		}
	}


	public ZipCodeInfo getZipCodeInfo (boolean left){
		RoadSide rs = (left) ? leftSide : rightSide;
		return (rs != null) ? rs.zipCode: null;
	}

	private void removeIfEmpty(boolean left){
		if (left && leftSide != null && leftSide.isEmpty())
			leftSide = null;
		if (!left && rightSide != null && rightSide.isEmpty())
			rightSide = null;
	}

	// allocate or return allocated RoadSide instance for the given road side
	private RoadSide assureSideIsAllocated(boolean left){
		if (left && leftSide == null)
			leftSide = new RoadSide();
		if (!left && rightSide == null)
			rightSide = new RoadSide();
		return (left) ? leftSide : rightSide;
	}

	public int getPolishIndex() {
		return polishPointIndex;
	}

	public void setPolishIndex(int n) {
		this.polishPointIndex = n;
	}

	/**
	 * @return The index of the nth number node where these numbers apply.  
	 */
	public int getIndex() {
		if (nodeIndex < 0) {
			log.error("WARNING: node index not set!!");
			return 0;
		}
		return nodeIndex;
	}

	public boolean hasIndex() {
		return nodeIndex >= 0;
	}

	/**
	 * @param index the nth number node 
	 */
	public void setIndex(int index) {
		this.nodeIndex = index;
	}

	private NumDesc getNumbers(boolean left) {
		RoadSide rs = (left) ? leftSide : rightSide;
		return (rs != null) ? rs.numbers : null;  
	}

	public NumberStyle getNumberStyle(boolean left) {
		NumDesc n = getNumbers(left);
		return (n == null) ? NumberStyle.NONE : n.numberStyle;
	}

	public int getStart(boolean left) {
		NumDesc n = getNumbers(left);
		return (n == null) ? -1 : n.start; // -1 is the default in the polish format
	}

	public int getEnd(boolean left) {
		NumDesc n = getNumbers(left);
		return (n == null) ? -1 : n.end; // -1 is the default in the polish format
	}

	public String toString() {
		String nodeStr = "0";
		if (polishPointIndex > 0)
			nodeStr = Integer.toString(polishPointIndex);
		else if (getIndex() > 0)
			nodeStr = String.format("(n%d)", getIndex());

		nodeStr = String.format("%s,%s,%d,%d,%s,%d,%d",
				nodeStr,
				getNumberStyle(LEFT),
				getStart(LEFT),
				getEnd(LEFT),
				getNumberStyle(RIGHT),
				getStart(RIGHT),
				getEnd(RIGHT));

		if (getCityInfo(LEFT) != null || getCityInfo(RIGHT) != null
				|| getZipCodeInfo(LEFT) != null || getZipCodeInfo(RIGHT) != null) {
			nodeStr = String.format("%s,%s,%s", nodeStr,
					getPolishZipCode(LEFT), getPolishZipCode(RIGHT));
			if (getCityInfo(LEFT) != null || getCityInfo(RIGHT) != null) {
				nodeStr = String.format("%s,%s,%s",nodeStr,
						getPolishCityInfo(LEFT),getPolishCityInfo(RIGHT)); 
			}
		}
		return nodeStr; 		
	}

	public NumberStyle getLeftNumberStyle() {
		return getNumberStyle(LEFT);
	}
	public NumberStyle getRightNumberStyle() {
		return getNumberStyle(RIGHT);
	}
	public int getLeftStart(){
		return getStart(LEFT);
	}
	public int getRightStart(){
		return getStart(RIGHT);
	}
	public int getLeftEnd(){
		return getEnd(LEFT);
	}
	public int getRightEnd(){
		return getEnd(RIGHT);
	} 

	public boolean equals(Object obj) {
		if (!(obj instanceof Numbers))
			return false;

		Numbers other = (Numbers) obj;
		return toString().equals(other.toString());
	}

	public int hashCode() {
		return toString().hashCode();
	} 

	public boolean isPlausible(){
		if (!isPlausible(getLeftNumberStyle(), getLeftStart(), getLeftEnd()))
			return false;
		if (!isPlausible(getRightNumberStyle(), getRightStart(), getRightEnd()))
			return false;
		if (getLeftNumberStyle() == NumberStyle.NONE || getRightNumberStyle() == NumberStyle.NONE) {
			return true; // no need to compare values of road sides
		}
		if (!Objects.equals(getCityInfo(LEFT), getCityInfo(RIGHT))) {
			return true;
		}
		if (!Objects.equals(getZipCodeInfo(LEFT), getZipCodeInfo(RIGHT))) {
			return true;
		}
		// city and zip codes say that the intervals of numbers should not overlap
		if (getLeftNumberStyle() == getRightNumberStyle() 
				|| getLeftNumberStyle() == NumberStyle.BOTH 
				|| getRightNumberStyle() == NumberStyle.BOTH) {
			// check if intervals are overlapping
			int start1, start2, end1, end2;
			if (getLeftStart() < getLeftEnd()) {
				start1 = getLeftStart();
				end1 = getLeftEnd();
			} else {
				start1 = getLeftEnd();
				end1 = getLeftStart();
			}
			if (getRightStart() < getRightEnd()) {
				start2 = getRightStart();
				end2 = getRightEnd();
			} else {
				start2 = getRightEnd();
				end2 = getRightStart();
			}
			if (start2 > end1 || end2 < start1)
				return true;
			// single number on both sides of the road?
			return getLeftStart() == getLeftEnd() && getRightStart() == getRightEnd()
					&& getLeftStart() == getRightStart();
		}

		return true;
	}

	private static boolean isPlausible(NumberStyle style, int start, int end){
		if (Math.abs(start - end) > MAX_DELTA)
			return false;
		if (style == NumberStyle.EVEN)
			return start % 2 == 0 && end % 2 == 0;
		if (style == NumberStyle.ODD)
			return start % 2 != 0 && end % 2 != 0;
		return true;
	}

	public boolean isContained(int hn, boolean useLeft){
		RoadSide rs = useLeft ? leftSide : rightSide;
		if (rs == null || rs.numbers == null)
			return false;
		return rs.numbers.contained(hn);
	}
	
	/**
	 * @param hn a house number
	 * @param left left or right side
	 * @return 0 if the number is not within the intervals, 1 if it is on one side, 2 if it on both sides 
	 */
	public int countMatches(int hn) {
		int matches = 0;
		if (isContained(hn, LEFT))
			matches++;
		if (isContained(hn, RIGHT))
			matches++;
		if (matches > 1 && getLeftStart() == getLeftEnd() && getRightStart() == getRightEnd()) {
			matches = 1; // single number on both sides of the road 
		}
		return matches;
	}

	/** 
	 * Compare all fields that describe the interval, but not the position
	 * @param other
	 * @return true if these fields are equal
	 */
	public boolean isSimilar(Numbers other){
		if (other == null)
			return false;
		return !(getLeftNumberStyle() != other.getLeftNumberStyle() 
				|| getLeftStart() != other.getLeftStart()
				|| getLeftEnd() != other.getLeftEnd() 
				|| getRightNumberStyle() != other.getRightNumberStyle()
				|| getRightStart() != other.getRightStart() 
				|| getRightEnd() != other.getRightEnd());

	}

	public boolean isEmpty(){
		return getLeftNumberStyle() == NumberStyle.NONE && getRightNumberStyle() == NumberStyle.NONE;
	}


	private String getPolishCityInfo (boolean left){
		CityInfo ci = getCityInfo(left);
		if (ci == null)
			return "-1";
		StringBuilder sb = new StringBuilder();
		if (ci.getCity() != null)
			sb.append(ci.getCity());
		sb.append(",");
		if (ci.getRegion() != null)
			sb.append(ci.getRegion());
		sb.append(",");
		if (ci.getCountry() != null)
			sb.append(ci.getCountry());
		return sb.toString();
	}

	private String getPolishZipCode (boolean left){
		ZipCodeInfo zip = getZipCodeInfo(left);
		return (zip != null && zip.getZipCode() != null ) ? zip.getZipCode() : "-1";
	}


}
