/*
 * Copyright (C) 2006, 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.reader.osm.boundary;

import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import uk.me.parabola.imgfmt.ExitException;
import uk.me.parabola.imgfmt.FormatException;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.reader.osm.Tags;
import uk.me.parabola.mkgmap.reader.osm.Way;
import uk.me.parabola.util.EnhancedProperties;
import uk.me.parabola.util.Java2DConverter;
import uk.me.parabola.util.MultiHashMap;
import uk.me.parabola.util.ShapeSplitter;

public class BoundaryUtil {
	private static final Logger log = Logger.getLogger(BoundaryUtil.class);

	private static final int UNKNOWN_DATA_FORMAT = 0;
	private static final int RAW_DATA_FORMAT_V1 = 2;
	private static final int QUADTREE_DATA_FORMAT_V1 = 3;
	public static final double MIN_DIMENSION = 0.0000001;
	/**
	 * Calculate the polygons that describe the area.
	 * @param area the Area instance
	 * @param id an id that is used to create meaningful messages, typically a boundary Id
	 * @return A list of BoundaryElements (can be empty)
	 */
	public static List<BoundaryElement> splitToElements(Area area, String id) {
		if (area.isEmpty()) {
			return Collections.emptyList();
		}

		Area testArea = area;
		boolean tryAgain = true;
		while (true){
			List<List<Coord>> areaElements = Java2DConverter.areaToShapes(testArea);

			if (areaElements.isEmpty()) {
				// this may happen if a boundary overlaps a raster tile in a very small area
				// so that it is has no dimension
				log.debug("Area has no dimension. Area:",area.getBounds());
				return Collections.emptyList();
			}

			List<BoundaryElement> bElements = new ArrayList<>();
			for (List<Coord> singleElement : areaElements) {
				if (singleElement.size() <= 3) {
					// need at least 4 items to describe a polygon
					continue;
				}
				boolean outer = Way.clockwise(singleElement);
				bElements.add(new BoundaryElement(outer, singleElement));
			}

			if (bElements.isEmpty()) {
				// should not happen because empty polygons should be removed by
				// the Java2DConverter
				log.error("Empty boundary elements list after conversion. Area: "+area.getBounds());
				return Collections.emptyList();
			}


			// reverse the list because it starts with the inner elements first and
			// we need the other way round
			Collections.reverse(bElements);

			if (bElements.get(0).isOuter())
				return bElements;

			// result is not usable if first element is not outer 
			if (!tryAgain){
				// cannot convert this area
				log.error(" first element is not outer. "+ bElements.get(0));
				
				//createJavaCodeSnippet(area);
				//String fname = "bnd_gpx/first_not_outer" + id ;
				//GpxCreator.createGpx(fname, bElements.get(0).getPoints());
				return Collections.emptyList();
			}
			// try converting the area with rounded float values
			Path2D.Float path = new Path2D.Float(area);
			testArea = new Area(path);
			tryAgain = false;
		}
	}

	/**
	 * Wrapper for {@link #loadQuadTrees(String, List, uk.me.parabola.imgfmt.app.Area, EnhancedProperties)}
	 * @param boundaryDirName a directory name or zip file containing the *.bnd file
	 * @param boundaryFileName the *.bnd file name
	 * @return the quadtree or null in case of errors
	 */
	public static BoundaryQuadTree loadQuadTree (String boundaryDirName, 
			String boundaryFileName){
		Map<String,BoundaryQuadTree> trees = loadQuadTrees (boundaryDirName, Collections.singletonList(boundaryFileName), null, null);
		return trees.get(boundaryFileName);
	}
	
	/**
	 * Create a BoundaryQuadTree for each file listed in boundaryFileNames. 
	 * @param boundaryDirName a directory name or zip file containing the *.bnd file
	 * @param boundaryFileNames the list of *.bnd file names
	 * @param searchBbox null or a bounding box. Data outside of this box is ignored.
	 * @param props null or the properties to be used for the locator
	 * @return a map with quadtrees which can be empty
	 */
	public static Map<String,BoundaryQuadTree> loadQuadTrees (String boundaryDirName, 
			List<String> boundaryFileNames, 
			uk.me.parabola.imgfmt.app.Area searchBbox, EnhancedProperties props){
		Map<String,BoundaryQuadTree>  trees = new HashMap<>();
		File boundaryDir = new File(boundaryDirName);
		BoundaryQuadTree bqt;
		if (boundaryDir.isDirectory()){
			for (String boundaryFileName: boundaryFileNames){
				log.info("loading boundary file:", boundaryFileName);
				// no support for nested directories
				File boundaryFile = new File(boundaryDir, boundaryFileName);
				if (boundaryFile.exists()){
					try(InputStream stream = new FileInputStream(boundaryFile)){
						bqt = BoundaryUtil.loadQuadTreeFromStream(stream, boundaryFileName, searchBbox, props);
						if (bqt != null)
							trees.put(boundaryFileName,bqt);
					} catch (IOException exp) {
						log.error("Cannot load boundary file " +  boundaryFileName + "." + exp);
					}
				}					
			}
		} else if (boundaryDirName.endsWith(".zip")) {
			String  currentFileName = "";
			try(ZipFile zipFile = new ZipFile(boundaryDir)){
				for (String boundaryFileName : boundaryFileNames){
					log.info("loading boundary file:", boundaryFileName);
					currentFileName = boundaryFileName;
					// direct access  
					ZipEntry entry = zipFile.getEntry(boundaryFileName);
					if (entry != null){ 
						try(InputStream stream = zipFile.getInputStream(entry)){
							bqt = BoundaryUtil.loadQuadTreeFromStream(stream, boundaryFileName, searchBbox, props);
							if (bqt != null)
								trees.put(boundaryFileName,bqt);
						}
					}
				}
			} catch (IOException exp) {
				log.error("Cannot load boundary file " + currentFileName + "." + exp);
			}
		} else{ 
			log.error("Cannot read " + boundaryDirName);
		}
		return trees;
	}

	
	/**
	 * read path iterator info from stream and create Area. 
	 * Data is stored with varying length doubles.
	 * @param inpStream the already opened DataInputStream 
	 * @return a new Area object or null if not successful 
	 * @throws IOException
	 */
	public static Area readAreaAsPath(DataInputStream inpStream) throws IOException{
		double[] res = new double[2];
		Path2D.Double path = new Path2D.Double(PathIterator.WIND_NON_ZERO, 1024);
		int windingRule = inpStream.readInt();
		path.setWindingRule(windingRule);
		int type = inpStream.readInt(); 
		double minX = Double.MAX_VALUE,maxX = Double.MIN_VALUE;
		double minY = Double.MAX_VALUE,maxY = Double.MIN_VALUE;
		while (type >= 0) {
			switch (type) {
			case PathIterator.SEG_LINETO:
				int len = inpStream.readInt();
				while(len > 0){
					for (int ii = 0; ii < 2; ii++){
						double delta = readVarDouble(inpStream);
						if (delta == BoundarySaver.RESET_DELTA)
							res[ii] = readVarDouble(inpStream);
						else
							res[ii] = res[ii] + delta;
					}
					if (res[0] < minX)
						minX = res[0];
					if (res[0] > maxX)
						maxX = res[0];
					if (res[1] < minY)
						minY = res[1];
					if (res[1] > maxY)
						maxY = res[1];
					path.lineTo(res[0],res[1]);
					--len;
				}
				break;
			case PathIterator.SEG_MOVETO:
				for (int ii = 0; ii < 2; ii++){
					double delta = readVarDouble(inpStream);
					if (delta == BoundarySaver.RESET_DELTA)
						res[ii] = readVarDouble(inpStream);
					else
						res[ii] = res[ii] + delta;
				}
				if (res[0] < minX)
					minX = res[0];
				if (res[0] > maxX)
					maxX = res[0];
				if (res[1] < minY)
					minY = res[1];
				if (res[1] > maxY)
					maxY = res[1];
				path.moveTo(res[0],res[1]);
				break;
			case PathIterator.SEG_CLOSE:
				path.closePath();
				break;
			default:
				log.error("Unsupported path iterator type " + type
						+ ". This is an mkgmap error.");
				return null;
			}

			type = inpStream.readInt();
		}
		if (type != -1){
			log.error("Final type value != -1: " + type);
		}
		else{
			if (maxX - minX >= MIN_DIMENSION || maxY - minY >= MIN_DIMENSION)
				return new Area(path);
			else {
				// ignore micro area caused by rounding errors in awt area routines
			}
				
		}
		return null;
	}
	
	/**
	 * Read boundary info saved in RAW_DATA_FORMAT 
	 * (written by 1st pass of preparer)
	 * @param inpStream the already opened DataInputStream
	 * @param fname the related file name of the *.bnd file
	 * @param bbox a bounding box. Data outside of this box is ignored.
	 * @return
	 * @throws IOException
	 */
	private static List<Boundary> readStreamRawFormat(
			DataInputStream inpStream, String fname,
			uk.me.parabola.imgfmt.app.Area bbox) throws IOException			{
		List<Boundary> boundaryList = new ArrayList<>();

		try {
			while (true) {
				int minLat = inpStream.readInt();
				int minLong = inpStream.readInt();
				int maxLat = inpStream.readInt();
				int maxLong = inpStream.readInt();
				if (log.isDebugEnabled())
					log.debug("Next boundary. Lat min:",minLat,"max:",maxLat,"Long min:",minLong,"max:",maxLong);
				uk.me.parabola.imgfmt.app.Area rBbox = new uk.me.parabola.imgfmt.app.Area(
						minLat, minLong, maxLat, maxLong);
				int bSize = inpStream.readInt();
				log.debug("Size:",bSize);

				if ( bbox == null || bbox.intersects(rBbox)) {
					log.debug("Bbox intersects. Load the boundary");
					String id = inpStream.readUTF();
					Tags tags = new Tags();
					int noOfTags = inpStream.readInt();
					for (int i = 0; i < noOfTags; i++) {
						String name = inpStream.readUTF();
						String value = inpStream.readUTF();
						tags.put(name, value.intern()); 					
					}
					Area area = readAreaAsPath(inpStream);
					if (area != null) {
						Boundary boundary = new Boundary(area, tags,id);
						boundaryList.add(boundary);
					} else {
						log.warn("Boundary "+tags+" does not contain any valid area in file " + fname);
					}

				} else {
					log.debug("Bbox does not intersect. Skip",bSize);
					inpStream.skipBytes(bSize);
				}
			}
		} catch (EOFException exp) {
			// it's always thrown at the end of the file
			// log.error("Got EOF at the end of the file");
		}  		
		return boundaryList;
	}

	/**
	 * For a given bounding box, calculate the list of file names that have to be read 
	 * @param bbox the bounding box
	 * @return a List with the names
	 */
	public static List<String> getRequiredBoundaryFileNames(uk.me.parabola.imgfmt.app.Area bbox) {
		List<String> names = new ArrayList<>();
		if (!bbox.isEmpty()) {
			for (int latSplit = getSplitBegin(bbox.getMinLat()); latSplit <= BoundaryUtil
					.getSplitBegin(bbox.getMaxLat()); latSplit += RASTER) {
				for (int lonSplit = getSplitBegin(bbox.getMinLong()); lonSplit <= BoundaryUtil
						.getSplitBegin(bbox.getMaxLong()); lonSplit += RASTER) {
					names.add("bounds_"+ getKey(latSplit, lonSplit) + ".bnd");
				}
			}
		}
		return names;
	}

	/** 
	 * Check content of directory or zip file with precompiled boundary data, 
	 * dirName Name has to be a directory or a zip file.
	 * @param dirName : path to a directory or a zip file containing the *.bnd files
	 * @return the available *.bnd files in dirName.
	 */
	public static List<String> getBoundaryDirContent(String dirName) {
		List<String> names = new ArrayList<>();
		File boundaryDir = new File(dirName);
		if (!boundaryDir.exists())
			log.error("boundary directory/zip does not exist: " + dirName);
		else{		
			if (boundaryDir.isDirectory()){
				// boundaryDir.list() is much quicker than boundaryDir.listFiles(FileFilter)
				String[] allNames = boundaryDir.list();
				for (String name: allNames){
					if (name.endsWith(".bnd"))
						names.add(name);
				}
			}
			else if (boundaryDir.getName().endsWith(".zip")){
				try (ZipFile zipFile = new ZipFile(boundaryDir)){
					Enumeration<? extends ZipEntry> entries = zipFile.entries();
					boolean isFlat = true;
					while(entries.hasMoreElements()) {
						ZipEntry entry = entries.nextElement();
						if (entry.isDirectory()){
							isFlat = false;
						}
						if (entry.getName().endsWith(".bnd"))
							names.add(entry.getName());
					}
					if (!isFlat){
						log.error("boundary zip file contains directories. Files in directories will be ignored." + dirName);			
					}
				} catch (IOException ioe) {
					log.error("Cannot read",dirName,ioe);
//					ioe.printStackTrace();
					throw new ExitException("Failed to read required file " + dirName);				}
			}
		}
		return names;
	}
	
	public static final int RASTER = 50000;

	public static int getSplitBegin(int value) {
		int rem = value % RASTER;
		if (rem == 0) {
			return value;
		} else if (value >= 0) {
			return value - rem;
		} else {
			return value - RASTER - rem;
		}
	}

	public static int getSplitEnd(int value) {
		int rem = value % RASTER;
		if (rem == 0) {
			return value;
		} else if (value >= 0) {
			return value + RASTER - rem;
		} else {
			return value - rem;
		}
	}

	public static String getKey(int lat, int lon) {
		return lat + "_" + lon;
	}
	
	/**
	 * Retrieve the bounding box of the given boundary file.
	 * @param boundaryFileName the name of the boundary file
	 * @return the bounding box
	 */
	public static uk.me.parabola.imgfmt.app.Area getBbox(String boundaryFileName) {
		String filename = new String(boundaryFileName);
		// cut off the extension
		filename = filename.substring(0,filename.length()-4);
		String[] fParts = filename.split(Pattern.quote("_"));
		
		int lat = Integer.parseInt(fParts[1]);
		int lon = Integer.parseInt(fParts[2]);
		
		return new uk.me.parabola.imgfmt.app.Area(lat, lon, lat+RASTER, lon+RASTER);
	}

	/**
	 * Create and fill a BoundaryQuadTree. Read the header of the stream to detect
	 * the proper reading routine for the different supported formats.
	 * @param stream an already opened InputStream
	 * @param fname the file name of the corresponding *.bnd file
	 * @param searchBbox a bounding box or null. If not null, area info outside of this
	 * bounding box is ignored. 
	 * @param props properties to be used or null 
	 * @return on success it returns a new BoundaryQuadTree, else null 
	 * @throws IOException
	 */
	private static BoundaryQuadTree loadQuadTreeFromStream(InputStream stream, 
			String fname,
			uk.me.parabola.imgfmt.app.Area searchBbox, 
			EnhancedProperties props)throws IOException{
		BoundaryQuadTree bqt = null;
		uk.me.parabola.imgfmt.app.Area qtBbox = getBbox(fname);
		try (DataInputStream inpStream = new DataInputStream(new BufferedInputStream(stream, 1024 * 1024))){
			try {
				// 1st read the mkgmap release the boundary file is created by
				String mkgmapRel = "?";
				String firstId = inpStream.readUTF();
				if (!"BND".equals(firstId)) {
					throw new FormatException("Unsupported boundary data type "+firstId);
				}

				int format = UNKNOWN_DATA_FORMAT;
				long createTime = inpStream.readLong();
				int headerLength = inpStream.readInt();
				byte[] header = new byte[headerLength];
				int bytesRead = 0;
				while (bytesRead < headerLength) {
					int nBytes = inpStream.read(header, bytesRead, headerLength-bytesRead);
					if (nBytes<0) {
						throw new IOException("Cannot read header with size "+headerLength);
					}
					bytesRead += nBytes;
				}
					
				ByteArrayInputStream rawHeaderStream = new ByteArrayInputStream(header);
				DataInputStream headerStream =new DataInputStream(rawHeaderStream);
				String dataFormat = (rawHeaderStream.available() > 0 ? headerStream.readUTF() : "RAW");
				int recordVersion = (rawHeaderStream.available() > 0 ? headerStream.readInt() : RAW_DATA_FORMAT_V1);
				mkgmapRel = (rawHeaderStream.available() > 0 ? headerStream.readUTF() : "unknown");
				
				if ("RAW".equals(dataFormat) && recordVersion == 1)
					format = RAW_DATA_FORMAT_V1;
				else if ("QUADTREE".equals(dataFormat) && recordVersion == 1)
					format = QUADTREE_DATA_FORMAT_V1;

				if (log.isDebugEnabled()) {
					log.debug("File created by mkgmap release",mkgmapRel,"at",new Date(createTime));
				}
				
				switch (format) {
				case QUADTREE_DATA_FORMAT_V1:
					bqt = new BoundaryQuadTree(inpStream, qtBbox, searchBbox, props);
					break;
				case RAW_DATA_FORMAT_V1:
					List<Boundary> boundaryList = readStreamRawFormat(inpStream, fname,searchBbox);
					if (boundaryList == null || boundaryList.isEmpty())
						return null;
					boundaryList = mergePostalCodes(boundaryList);
					bqt = new BoundaryQuadTree(qtBbox, boundaryList, props);
					break;
				default:
					throw new FormatException("Unsupported boundary file format: "+format);
				}
			} catch (EOFException exp) {
				// it's always thrown at the end of the file
				//				log.error("Got EOF at the end of the file");
			} 
			catch (FormatException exp) {
				log.error("Failed to read boundary file " + fname + " " + exp.getMessage());
			} 
		} 
		return bqt;
	}
	
	/**
	 * Merges boundaries with the same postal code.
	 * @param boundaries a list of boundaries
	 * @return the boundary list with postal code areas merged
	 */
	private static List<Boundary> mergePostalCodes(List<Boundary> boundaries) {
		List<Boundary> mergedList = new ArrayList<>(boundaries.size());
		
		MultiHashMap<String, Boundary> equalPostalCodes = new MultiHashMap<>();
		for (Boundary boundary : boundaries) {
			String postalCode = getPostalCode(boundary.getTags());
			if (postalCode == null) {
				// no postal code boundary
				mergedList.add(boundary);
			} else {
				// postal code boundary => merge it later
				equalPostalCodes.add(postalCode, boundary);
			}
		}

		for (Entry<String, List<Boundary>> postCodeBoundary : equalPostalCodes
				.entrySet()) {
			if (postCodeBoundary.getValue().size() == 1) {
				// nothing to merge
				mergedList.addAll(postCodeBoundary.getValue());
				continue;
			}
			
			// there are more than 2 boundaries with the same post code
			// => merge them
			Area newPostCodeArea = new Area();
			for (Boundary b : postCodeBoundary.getValue()) {
				newPostCodeArea.add(b.getArea());
				
				// remove the post code tags from the original boundary
				if (b.getTags().get("postal_code") != null) {
					b.getTags().remove("postal_code");
				} else if ("postal_code".equals(b.getTags().get("boundary"))) {
					b.getTags().remove("boundary");
					b.getTags().remove("name");
				}
				
				// check if the boundary contains other boundary information
				if (isAdministrativeBoundary(b)) {
					mergedList.add(b);
				} else {
					log.info("Boundary", b.getId(), b.getTags(), "contains no more boundary tags. Skipping it.");
				}
			}
			
			Tags postalCodeTags = new Tags();
			postalCodeTags.put("postal_code", postCodeBoundary.getKey());
			Boundary postalCodeBoundary = new Boundary(newPostCodeArea, postalCodeTags, "p"+postCodeBoundary.getKey());

			log.info("Merged", postCodeBoundary.getValue().size(), "postal code boundaries for postal code", postCodeBoundary.getKey());
			mergedList.add(postalCodeBoundary);
		}
		
		return mergedList;
	}
	
	/**
	 * Checks if the given boundary contains tags for an administrative boundary.
	 * @param b a boundary
	 * @return <code>true</code> administrative boundary or postal code; 
	 * <code>false</code> element cannot be used for precompiled bounds 
	 */
	public static boolean isAdministrativeBoundary(Boundary b) {
		
		if (b.getId().startsWith("r")) {
			String type = b.getTags().get("type");
			
			if ("boundary".equals(type) || "multipolygon".equals(type)) {
				String boundaryVal = b.getTags().get("boundary");
				if (!"administrative".equals(boundaryVal)) 
					return false;
				// for boundary=administrative the admin_level must be set
				if (b.getTags().get("admin_level") == null) {
					return false;
				}
				// and a name must be set (check only for a tag containing name
				Iterator<Entry<String,String>> tagIterator = b.getTags().entryIterator();
				while (tagIterator.hasNext()) {
					Entry<String,String> tag  = tagIterator.next();
					if (tag.getKey().contains("name")) {
						return true;
					}
				}
				// does not contain a name tag => do not use it
			}
		} else if (b.getId().startsWith("w")) {
			// the boundary tag must be "administrative" or "postal_code"
			String boundaryVal = b.getTags().get("boundary");
			if ("administrative".equals(boundaryVal)) {
				// for boundary=administrative the admin_level must be set
				if (b.getTags().get("admin_level") == null) {
					return false;
				}
				// and a name must be set (check only for a tag containing name)
				Iterator<Entry<String,String>> tagIterator = b.getTags().entryIterator();
				while (tagIterator.hasNext()) {
					Entry<String,String> tag  = tagIterator.next();
					if (tag.getKey().contains("name")) {
						return true;
					}
				}
				// does not contain a name tag => do not use it
			}
		} 
		return false;
	}
	
	
	private static String getPostalCode(Tags tags) {
		String zip = tags.get("postal_code");
		if (zip == null) {
			if ("postal_code".equals(tags.get("boundary"))){
				String name = tags.get("name"); 
				if (name != null) {
					String[] nameParts = name.split(Pattern.quote(" "));
					if (nameParts.length > 0) {
						zip = nameParts[0].trim();
					}
				}
			}
		}
		return zip;
	}
	/**
	 * Helper to ease the reporting of errors. Creates a java code snippet 
	 * that can be compiled to have the same area.
	 * @param area the area for which the code should be produced
	 */
	public static void createJavaCodeSnippet(Area area) {
		double[] res = new double[6];
		PathIterator pit = area.getPathIterator(null);
		System.out.println("Path2D.Double path = new Path2D.Double();");
		System.out.println("path.setWindingRule(" + pit.getWindingRule() + ");");
		while (!pit.isDone()) {
			int type = pit.currentSegment(res);
			switch (type) {
			case PathIterator.SEG_LINETO:
				System.out.println("path.lineTo(" + res[0] + "d, " + res[1] + "d);");
				break;
			case PathIterator.SEG_MOVETO: 
				System.out.println("path.moveTo(" + res[0] + "d, " + res[1] + "d);");
				break;
			case PathIterator.SEG_CLOSE:
				System.out.println("path.closePath();");
				break;
			default:
				log.error("Unsupported path iterator type " + type
						+ ". This is an mkgmap error.");
			}

			pit.next();
		}
		System.out.println("Area area = new Area(path);");
		
	}

	/**
	 * read a varying length double. See BoundarySaver.writeVarDouble().
	 * @param inp the already opened DataInputStream
	 * @return the extracted double value
	 * @throws IOException
	 */
	static double readVarDouble(DataInputStream inp) throws IOException{
		byte b;
		long res = 0;
		long toShift = 64 - 7;
		while (((b = inp.readByte()) & 0x80) != 0){ // more bytes will follow
			res |= (b & 0x7f);
			toShift -= 7;
			if (toShift > 0)
				res <<= 7;
		}
		if (toShift > 0){
			res |= b;
			res <<= toShift;
		}
		else {
			// special case: all 64 bits were written, 64 = 9*7 + 1
			res <<= 1;
			res |= 1;
		}
		return Double.longBitsToDouble(res);
	}

	/**
	 * Raster a given area. This is the non-recursive public method.
	 * @param areaToSplit the area
	 * @return a map with the divided shapes 
	 */
	public static Map<String, Shape> rasterArea(Area areaToSplit) {
		return rasterShape(areaToSplit, new HashMap<>());
	}

	/**
	 * Raster a given shape. This method calls itself recursively.
	 * @param shapeToSplit the shape
	 * @param splits a map that will contain the resulting shapes
	 * @return a reference to the map 
	 */
	private static Map<String, Shape> rasterShape(Shape shapeToSplit, Map<String, Shape> splits) {
		double minX = Double.POSITIVE_INFINITY,minY = Double.POSITIVE_INFINITY, 
				maxX = Double.NEGATIVE_INFINITY,maxY = Double.NEGATIVE_INFINITY;
		PathIterator pit = shapeToSplit.getPathIterator(null);
		double[] points = new double[512];
		double[] res = new double[6];
		int num = 0;
		while (!pit.isDone()) {
			int type = pit.currentSegment(res);
			double x = res[0];
			double y = res[1];
			if (x < minX) minX = x;
			if (x > maxX) maxX = x;
			if (y < minY) minY = y;
			if (y > maxY) maxY = y;
			switch (type) {
			case PathIterator.SEG_LINETO:
			case PathIterator.SEG_MOVETO:
				if (num  + 2 >= points.length) {
					points = Arrays.copyOf(points, points.length * 2);
				}
				points[num++] = x;
				points[num++] = y;
				break;
			case PathIterator.SEG_CLOSE:
				int sMinLong = getSplitBegin((int)Math.round(minX));
				int sMinLat = getSplitBegin((int)Math.round(minY));
				int sMaxLong = getSplitEnd((int)Math.round(maxX));
				int sMaxLat = getSplitEnd((int)Math.round(maxY));
	
				int dLon = sMaxLong- sMinLong;
				int dLat = sMaxLat - sMinLat;
				Rectangle2D.Double bbox = new Rectangle2D.Double(minX,minY,maxX-minX,maxY-minY);
				if (dLon > RASTER || dLat > RASTER) {
					// split into two halves
					Rectangle clip1,clip2;
					if (dLon > dLat) {
						int midLon = getSplitEnd(sMinLong+dLon/2);
						clip1 = new Rectangle(sMinLong, sMinLat, midLon-sMinLong, dLat);
						clip2 = new Rectangle(midLon, sMinLat, sMaxLong-midLon, dLat);
					} else {
						int midLat = getSplitEnd(sMinLat+dLat/2);
						clip1 = new Rectangle(sMinLong, sMinLat, dLon, midLat-sMinLat);
						clip2 = new Rectangle(sMinLong, midLat, dLon, sMaxLat-midLat);
					}
	
					// intersect with the both halves
					// and split both halves recursively
					Path2D.Double clippedPath = ShapeSplitter.clipSinglePathWithSutherlandHodgman (points, num, clip1, bbox);
					if (clippedPath != null)
						rasterShape(clippedPath, splits);
					clippedPath = ShapeSplitter.clipSinglePathWithSutherlandHodgman (points, num, clip2, bbox);
					if (clippedPath != null)
						rasterShape(clippedPath, splits);
				} 
				else {
					String key = getKey(sMinLat, sMinLong);
					// no need to split, path fits into one tile
					Path2D.Double segment = ShapeSplitter.pointsToPath2D(points, num);
					if (segment != null){
						Path2D.Double path = (Path2D.Double) splits.get(key);
						if (path == null)
							splits.put(key, segment);
						else 
							path.append(segment, false);
					}
				}
				num = 0;
				minX = minY = Double.POSITIVE_INFINITY; 
				maxX = maxY = Double.NEGATIVE_INFINITY;
				break;
			default:
				log.error("Unsupported path iterator type " + type
						+ ". This is an mkgmap error.");
			}
	
			pit.next();
		}
		return splits;
	}
    
}
