/*
 * Copyright (C) 2006 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 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.
 * 
 * 
 * Author: Steve Ratcliffe
 * Create date: 16-Dec-2006
 */
package uk.me.parabola.mkgmap.reader.polish;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import uk.me.parabola.imgfmt.FormatException;
import uk.me.parabola.imgfmt.MapFailedException;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.imgfmt.app.net.AccessTagsAndBits;
import uk.me.parabola.imgfmt.app.net.NumberStyle;
import uk.me.parabola.imgfmt.app.net.Numbers;
import uk.me.parabola.imgfmt.app.trergn.ExtTypeAttributes;
import uk.me.parabola.imgfmt.app.trergn.TREHeader;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.filters.LineSplitterFilter;
import uk.me.parabola.mkgmap.general.CityInfo;
import uk.me.parabola.mkgmap.general.LevelInfo;
import uk.me.parabola.mkgmap.general.LoadableMapDataSource;
import uk.me.parabola.mkgmap.general.MapElement;
import uk.me.parabola.mkgmap.general.MapLine;
import uk.me.parabola.mkgmap.general.MapPoint;
import uk.me.parabola.mkgmap.general.MapRoad;
import uk.me.parabola.mkgmap.general.MapShape;
import uk.me.parabola.mkgmap.general.ZipCodeInfo;
import uk.me.parabola.mkgmap.reader.MapperBasedMapDataSource;
import uk.me.parabola.mkgmap.reader.osm.FakeIdGenerator;
import uk.me.parabola.mkgmap.reader.osm.FeatureKind;
import uk.me.parabola.mkgmap.reader.osm.GType;
import uk.me.parabola.mkgmap.reader.osm.GeneralRelation;
import uk.me.parabola.mkgmap.reader.osm.MultiPolygonRelation;
import uk.me.parabola.mkgmap.reader.osm.Way;

/**
 * Read an data file in Polish format.  This is the format used by a number
 * of other garmin map making programs notably cGPSmapper.
 * <p>
 * As the input format is designed for garmin maps, it is fairly easy to read
 * into mkgmap.  Not every feature of the format is read yet, but it shouldn't
 * be too difficult to add them in as needed.
 * <p>
 * Now will place elements at the level specified in the file and not at the
 * automatic level that is used in eg. the OSM reader.
 */
public class PolishMapDataSource extends MapperBasedMapDataSource implements LoadableMapDataSource {
	private static final Logger log = Logger.getLogger(PolishMapDataSource.class);

	private static final Charset READING_CHARSET = StandardCharsets.ISO_8859_1;

	private static final int S_IMG_ID = 1;
	private static final int S_POINT = 2;
	private static final int S_POLYLINE = 3;
	private static final int S_POLYGON = 4;
    private static final int S_RESTRICTION = 5;

	private MapPoint point;
	private MapLine polyline;
	private MapShape shape;

    private PolishTurnRestriction restriction;

    /** contains the information from the lines starting with DATA */
	private final Map<Integer, List<List<Coord>>> lineStringMap = new LinkedHashMap<>();

	private final RoadHelper roadHelper = new RoadHelper();
    private final RestrictionHelper restrictionHelper = new RestrictionHelper();

	private Map<String, String> extraAttributes;

	private String copyright;
	private int section;
	private LevelInfo[] levels;
	private int endLevel;
	private char elevUnits;
	private int currentLevel;
	private boolean dataHighLevel;
	private boolean background;
	private int poiDispFlag;
	private String defaultCountry;
	private String defaultRegion;
	private static final double METERS_TO_FEET = 3.2808399;

	private int lineNo;

	private boolean havePolygon4B;

	/** if false, assume that lines with routable types are roads and create corresponding NET data */ 
	private boolean routing;
	private int roadIdGenerated;

	// Use to decode labels if they are not in cp1252
	private CharsetDecoder dec;

	public boolean isFileSupported(String name) {
		// Supported if the extension is .mp
		return name.endsWith(".mp") || name.endsWith(".MP") || name.endsWith(".mp.gz");
	}

	@Override
	public void load(String name, boolean addBackground) throws FileNotFoundException {
		// If no code page is given then we read labels in utf-8
		dec = StandardCharsets.UTF_8.newDecoder();
		dec.onUnmappableCharacter(CodingErrorAction.REPLACE);

		try (BufferedReader in = new BufferedReader(new InputStreamReader(Utils.openFile(name), READING_CHARSET))) {
			String line;
			while ((line = in.readLine()) != null) {
				++lineNo;
				line = line.trim();
				if (line.isEmpty() || line.charAt(0) == ';')
					continue;
				if (line.toUpperCase().startsWith("[END"))
					endSection();
				else if (line.charAt(0) == '[')
					sectionStart(line);
				else
					processLine(line);
			}
		} catch (IOException e) {
			throw new FormatException("Reading file failed", e);
		}

		// Add all restrictions to the map after reading the full map.
		// The reason being, the restrictions section appear in the beginning of the map.
		// All the nodes will only be read later on.
		// Required to pass the road helper instance as it contains all node data.
		restrictionHelper.processAndAddRestrictions(roadHelper, mapper);

		if (addBackground && !havePolygon4B)
			addBackground();
	}

	public LevelInfo[] mapLevels() {
		if (levels == null) {
			// If it has not been set then supply some defaults.
			levels = new LevelInfo[] {
					new LevelInfo(3, 17),
					new LevelInfo(2, 18),
					new LevelInfo(1, 22),
					new LevelInfo(0, 24),
			};
		}
		levels[0].setTop(true);
		return levels;
	}

	
	@Override
	public LevelInfo[] overviewMapLevels() {
		String levelSpec = getConfig().getProperty("overview-levels");
		
		if (levelSpec == null)
			return null;
		LevelInfo[] ovLevels = LevelInfo.createFromString(levelSpec); 
		for (int i = 0; i < ovLevels.length; i++) {
			ovLevels[i] = new LevelInfo(ovLevels.length - i - 1, ovLevels[i].getBits());
		}
		return ovLevels;
	}

	/**
	 * Get the copyright message.  
	 *
	 * @return A string description of the copyright.
	 */
	public String[] copyrightMessages() {
		String copyrightFileName = getConfig().getProperty("copyright-file", null);
		if (copyrightFileName != null) {
			return readCopyrightFile(copyrightFileName);
		}
		if (copyright == null) {
			copyright = getConfig().getProperty("copyright-message", null);
		}
		return new String[] { copyright };
	}

	/**
	 * Record that we are starting a new section.
	 * Section names are enclosed in square brackets.  Inside the section there
	 * are a number of lines with the key=value format.
	 *
	 * @param line The raw line from the input file.
	 */
	private void sectionStart(String line) {
		String name = line.substring(1, line.length() - 1).trim();
		log.debug("section name", name);

		extraAttributes = null;

		if ("IMG ID".equalsIgnoreCase(name)) {
			section = S_IMG_ID;
			poiDispFlag = 0;
		} else if ("POI".equalsIgnoreCase(name) || "RGN10".equals(name) || "RGN20".equals(name)) {
			point = new MapPoint();
			section = S_POINT;
		} else if ("POLYLINE".equalsIgnoreCase(name) || "RGN40".equals(name)) {
			polyline = new MapLine();
			roadHelper.clear();
			section = S_POLYLINE;
		} else if ("POLYGON".equalsIgnoreCase(name) || "RGN80".equals(name)) {
			shape = new MapShape();
			section = S_POLYGON;
		} else if ("Restrict".equalsIgnoreCase(name)) {
            restriction = new PolishTurnRestriction();
            section = S_RESTRICTION;
        }
		else
			log.info("Ignoring unrecognised section: " + name);
	}

	/**
	 * At the end of a section, we add what ever element that we have been
	 * building to the map.
	 */
	private void endSection() {
		switch (section) {

		case S_IMG_ID:
			break;

		case S_POINT:
			if (point.getLocation() != null) {
				if(extraAttributes != null && point.hasExtendedType())
					point.setExtTypeAttributes(makeExtTypeAttributes());
				mapper.addToBounds(point.getLocation());
				mapper.addPoint(point);
			} else {
				log.error("skipping POI without coordinates near line " + lineNo );
			}
				
			break;
		case S_POLYLINE:
			if (!lineStringMap.isEmpty()) {
				MapLine origPolyline = polyline.copy();
				
				for (Map.Entry<Integer , List<List<Coord>>> entry : lineStringMap.entrySet()) {
					int level = entry.getKey();
					setResolution(origPolyline, level);
					for (List<Coord> points : entry.getValue()) {
						polyline = origPolyline.copy();
						if (!routing && GType.isSpecialRoutableLineType(polyline.getType())) {
							roadHelper.setRoadId(++roadIdGenerated);
						}
						if (roadHelper.isRoad() && level == 0) {
							polyline.setPoints(points);
							MapRoad r = roadHelper.makeRoad(polyline);
							if (!routing) {
								r.skipAddToNOD(true);
							}
							mapper.addRoad(r);
						}
						else {
							if(extraAttributes != null && polyline.hasExtendedType())
								polyline.setExtTypeAttributes(makeExtTypeAttributes());
							final int maxPointsInLine = LineSplitterFilter.MAX_POINTS_IN_LINE;
							if(points.size() > maxPointsInLine) {
								List<Coord> segPoints = new ArrayList<>(maxPointsInLine);
								for(Coord p : points) {
									segPoints.add(p);
									if(segPoints.size() == maxPointsInLine) {
										MapLine seg = polyline.copy();
										seg.setPoints(segPoints);
										mapper.addLine(seg);
										segPoints = new ArrayList<>(maxPointsInLine);
										segPoints.add(p);
									}
								}
								if(!segPoints.isEmpty()) {
									polyline.setPoints(segPoints);
									mapper.addLine(polyline);
								}
							}
							else {
								polyline.setPoints(points);
								mapper.addLine(polyline);
							}
						}
					}
				}
			}
			break;
		case S_POLYGON:
			if (!lineStringMap.isEmpty()) {
				if (extraAttributes != null && shape.hasExtendedType())
					shape.setExtTypeAttributes(makeExtTypeAttributes());
				if (background && !dataHighLevel)
					endLevel = levels.length -1;
				for (Map.Entry<Integer , List<List<Coord>>> entry : lineStringMap.entrySet()) {
					setResolution(shape, entry.getKey());
					addShapesFromPattern(entry.getValue());
				}
			}
			break;
        case S_RESTRICTION:
            restrictionHelper.addRestriction(restriction);
            break;
		case 0:
			// ignored section
			break;

		default:
			log.warn("unexpected default in switch", section);
			break;
		}

		// Clear the section state.
		section = 0;
		endLevel = 0;
		lineStringMap.clear();
		currentLevel = 0;
		dataHighLevel = false;
		background = false;
	}

	private void addShapesFromPattern(List<List<Coord>> pointsLists) {
		if (pointsLists.size() == 1) {
			MapShape copy = shape.copy();
			copy.setPoints(pointsLists.get(0));
			mapper.addShape(copy);
		} else {
			// we have a multipolygon with multiple rings, use
			// MultiPolygonRelation to compute the geometry
			Map<Long, Way> wayMap = new HashMap<>();
			GeneralRelation gr = new GeneralRelation(FakeIdGenerator.makeFakeId());
			// add one tag so that MultiPolygonRelation doesn't ignore the relation 
			gr.addTag("code", Integer.toHexString(shape.getType())); 
			for (int i = 0; i < pointsLists.size(); i++) {
				Way w = new Way(FakeIdGenerator.makeFakeId(), pointsLists.get(i));
				wayMap.put(w.getId(), w);
				// empty role, let MultiPolygonRelation find out what is inner or outer
				gr.addElement("", w);  
			}
			MultiPolygonRelation mp = new MultiPolygonRelation(gr, wayMap, Area.PLANET);
			mp.processElements();
			for (Way s: wayMap.values()) {
				if (MultiPolygonRelation.STYLE_FILTER_POLYGON.equals(s.getTag(MultiPolygonRelation.STYLE_FILTER_TAG))) {
					MapShape copy = shape.copy();
					copy.setPoints(s.getPoints());
					mapper.addShape(copy);
				}
			}
		}
		
	}

	/**
	 * This should be a line that is a key value pair.  We switch out to a
	 * routine that is dependent on the section that we are in.
	 *
	 * @param line The raw input line from the file.
	 */
	private void processLine(String line) {
		String[] nameVal = line.split("=", 2);
		if (nameVal.length != 2) {
			log.warn("short line? " + line);
			return;
		}
		String name = nameVal[0].trim();
		String value = nameVal[1].trim();

		log.debug("LINE: ", name, "|", value);
		
		switch (section) {
		case S_IMG_ID:
			imgId(name, value);
			break;
		case S_POINT:
			if (!isCommonValue(point, name, value))
				point(name, value);
			break;
		case S_POLYLINE:
			if (!isCommonValue(polyline, name, value))
				line(name, value);
			break;
		case S_POLYGON:
			if (!isCommonValue(shape, name, value))
				shape(name, value);
			break;
        case S_RESTRICTION:
            restriction(name, value);
            break;
		default:
			log.debug("line ignored");
			break;
		}
	}


	/**
	 * This is called for every line within the POI section.  The lines are
	 * key value pairs that have already been decoded into name and value.
	 * For each name we recognise we set the appropriate property on
	 * the <i>point</i>.
	 *
	 * @param name Parameter name.
	 * @param value Its value.
	 */
	private void point(String name, String value) {
		if ("Type".equals(name)) {
			int type = Integer.decode(value);
			if (type <= 0xff)
				type <<= 8;
			point.setType(type);
			checkType(FeatureKind.POINT, point.getType());
		} else if ("SubType".equals(name)) {
			int subtype = Integer.decode(value);
			int type = point.getType();
			point.setType(type | subtype);
			checkType(FeatureKind.POINT, point.getType());
		} else if (name.startsWith("Data") || name.startsWith("Origin")) {
			Coord co = makeCoord(value);
			setResolution(point, name);
			point.setLocation(co);
		}
		else {
			if(extraAttributes == null)
				extraAttributes = new HashMap<>();
			extraAttributes.put(name, value);
		}
	}

	/**
	 * Called for each command in a POLYLINE section.  There will be a Data
	 * line consisting of a number of co-ordinates that must be separated out
	 * into points.
	 *
	 * @param name Command name.
	 * @param value Command value.
	 * @see #point
	 */
	private void line(String name, String value) {
		if ("Type".equals(name)) {
			polyline.setType(decodeType(FeatureKind.POLYLINE, value));
		} else if (name.startsWith("Data")) {
			extractResolution(name);
			addLineString(value, false);
			// If it is a contour line, then fix the elevation if required.
			if (GType.isContourLine(polyline)) {
				fixElevation();
			}
		} else if ("RoadID".equals(name)) {
			if (!routing && roadIdGenerated > 0)
				throw new MapFailedException("found RoadID without Routing=Y in [IMG ID] section in line " + lineNo);
			roadHelper.setRoadId(Integer.parseInt(value));
		} else if (name.startsWith("Nod")) {
			roadHelper.addNode(value);
		} else if ("RouteParam".equals(name) || "RouteParams".equals(name)) {
			roadHelper.setParam(value);
		} else if ("DirIndicator".equals(name)) {
			polyline.setDirection(Integer.parseInt(value) > 0);
		} else if (name.startsWith("Numbers")) {
			roadHelper.addNumbers(parseNumbers(value));
		} else {
			if (extraAttributes == null)
				extraAttributes = new HashMap<>();
			extraAttributes.put(name, value);
		}
	}

	/**
	 * 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 parseNumbers(String spec) {
		Numbers nums = new Numbers();
		String[] strings = spec.split(",");
		nums.setPolishIndex(Integer.parseInt(strings[0]));
		NumberStyle numberStyle = NumberStyle.fromChar(strings[1]);
		int start = Integer.parseInt(strings[2]);
		int end = Integer.parseInt(strings[3]);
		nums.setNumbers(Numbers.LEFT, numberStyle, start, end);
		numberStyle = NumberStyle.fromChar(strings[4]);
		start = Integer.parseInt(strings[5]);
		end = Integer.parseInt(strings[6]);
		nums.setNumbers(Numbers.RIGHT, numberStyle, start, end);

		if (strings.length > 8){
			// zip codes 
			String zip = strings[7];
			if (!"-1".equals(zip))
				nums.setZipCode(Numbers.LEFT, new ZipCodeInfo(zip));
			zip = strings[8];
			if (!"-1".equals(zip))
				nums.setZipCode(Numbers.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];
				nums.setCityInfo(Numbers.LEFT, createCityInfo(city, region, country));
				nextPos = 12;
			} else {
				nextPos = 10;
			}
			city = strings[nextPos];
			if (!"-1".equals(city)){
				region = strings[nextPos + 1];
				country = strings[nextPos + 2];
				nums.setCityInfo(Numbers.RIGHT, createCityInfo(city, region, country));
			} 
		}
		return nums;
	}
	
	private CityInfo createCityInfo(String city, String region, String country) {
		return new CityInfo(recode(city), recode(region), unescape(recode(country)));
	}
	
	private List<Coord> coordsFromString(String value, boolean close) {
		String[] ords = value.split("\\) *, *\\(");
		List<Coord> points = new ArrayList<>();

		for (String s : ords) {
			Coord co = makeCoord(s);
			if (log.isDebugEnabled())
				log.debug(" L: ", co);
			mapper.addToBounds(co);
			points.add(co);
		}
		if (close && points.get(0) != points.get(points.size() - 1)) {
			// not closed, close it
			if (points.get(0).highPrecEquals(points.get(points.size() - 1))) {
				points.set(points.size() - 1, points.get(0));
			} else {
				points.add(points.get(0));
			}
		}
		log.debug(points.size() + " points from " + value);
		return points;
	}

	/**
	 * The elevation needs to be in feet.  So if it is given in meters then
	 * convert it.
	 */
	private void fixElevation() {
		if (elevUnits == 'm') {
			String h = polyline.getName();
			if (h == null)
				return;
			try {
				// Convert to feet.
				double m = Double.parseDouble(h);
				int feet = (int) Math.round(m * METERS_TO_FEET);
				polyline.setName(Integer.toString(feet));

			} catch (NumberFormatException e) {
				log.warn(h + " doesn't seem to be a valid elevation value in m" );
			}
		}
	}

	/**
	 * Called for each command in a POLYGON section.  There will be a Data
	 * line consisting of a number of co-ordinates that must be separated out
	 * into points.
	 *
	 * @param name Command name.
	 * @param value Command value.
	 * @see #line
	 */
	private void shape(String name, String value) {
		if ("Type".equals(name)) {
			shape.setType(decodeType(FeatureKind.POLYGON, value));
			if(shape.getType() == 0x4b)
				havePolygon4B = true;
		} else if (name.startsWith("Data")) {
			extractResolution(name);
			addLineString(value, true);
		} else if ("Background".equals(name)) {
			if ("Y".equals(value))
				background = true;
		}
		else {
			if(extraAttributes == null)
				extraAttributes = new HashMap<>();
			extraAttributes.put(name, value);
		}
	}

	private void addLineString (String value, boolean close) {
		lineStringMap.computeIfAbsent(currentLevel, k -> new ArrayList<>()).add(coordsFromString(value, close));
	}
	
	private boolean isCommonValue(MapElement elem, String name, String value) {
		if ("Label".equals(name)) {
			elem.setName(unescape(recode(value)));
		} else if ("Label2".equals(name) || "Label3".equals(name)) {
			elem.add2Name(unescape(recode(value)));
		} else if ("Levels".equals(name) || "EndLevel".equals(name) || "LevelsNumber".equals(name)) {
			try {
				endLevel = Integer.valueOf(value);
			} catch (NumberFormatException e) {
				endLevel = 0;
			}
		} else if ("ZipCode".equals(name)) {
		  elem.setZip(recode(value));
		} else if ("CityName".equals(name)) {
		  elem.setCity(recode(value));		  
		} else if ("StreetDesc".equals(name)) {
		  elem.setStreet(recode(value));
		} else if ("HouseNumber".equals(name)) {
		  elem.setHouseNumber(recode(value));
		} else if ("is_in".equals(name)) {
		  elem.setIsIn(recode(value));		  
		} else if ("Phone".equals(name)) {
		  elem.setPhone(recode(value));			
		} else if ("CountryName".equals(name)) {
		  elem.setCountry(unescape(recode(value)));
		} else if ("RegionName".equals(name)) {
		  elem.setRegion(recode(value));				
		} else {
			return false;
		}

		// We dealt with it
		return true;
	}

	/**
	 * Deal with the polish map escape codes of the form ~[0x##].  These
	 * stand for a single character and is usually used for highway
	 * symbols, name separators etc.
	 *
	 * The code ~[0x05] stands for the character \005 for example.
	 * 
	 * @param s The original string that may contain codes.
	 * @return A string with the escape codes replaced by the single character.
	 */
	public static String unescape(String s) {
		int ind = s.indexOf("~[");
		if (ind < 0)
			return s;

		StringBuilder sb = new StringBuilder();
		if (ind > 0)
			sb.append(s.substring(0, ind));

		char[] buf = s.toCharArray();
		while (ind < buf.length) {
			if (ind < buf.length-2 && buf[ind] == '~' && buf[ind+1] == '[') {
				StringBuilder num = new StringBuilder();
				ind += 2; // skip "~["
				while (ind < buf.length && buf[ind++] != ']')
					num.append(buf[ind - 1]);

				try {
					int inum = Integer.decode(num.toString());

					// Convert any that are in 6-bit format
					if (inum == 0x1b2c) inum = 0x1c;
					if (inum >= 0x2a)
						inum -= 0x29;
					sb.append((char) inum);
				} catch (NumberFormatException e) {
					// Input is malformed so lets just ignore it.
				}
			} else {
				sb.append(buf[ind]);
				ind++;
			}
		}
		return sb.toString();
	}

	/**
	 * Convert the value of a label into a string based on the declared
	 * code page in the file.
	 *
	 * This makes assumptions about the way that the .mp file is written
	 * that may not be correct.
	 *
	 * @param value The string that has been read with ISO-8859-1.
	 * @return A possibly different string that is obtained by taking the
	 * bytes in the input string and decoding them as if they had the
	 * declared code page.
	 */
	private String recode(String value) {
		if (dec != null) {
			// Get the bytes that were actually in the file.
			byte[] bytes = value.getBytes(READING_CHARSET);
			ByteBuffer buf = ByteBuffer.wrap(bytes);

			try {
				// Decode from bytes with the correct code page.
				CharBuffer out = dec.decode(buf);
				return out.toString();
			} catch (CharacterCodingException e) {
				log.error("error decoding label", e);
			}
		}
		return value;
	}

	private void setResolution(MapElement elem, String name) {
		
		if (endLevel > 0) {
			elem.setMinResolution(extractResolution(endLevel));
		    elem.setMaxResolution(extractResolution(name));
		} else {
			int res = extractResolution(name);
			elem.setMinResolution(res);
			elem.setMaxResolution(res);
		}
	}

	private void setResolution(MapElement elem, int level) {
		if (endLevel > 0) {
		    elem.setMaxResolution(extractResolution(level));
			if (lineStringMap.size() > 1) {
				for (int i = level+1; i < endLevel; i++) {
					if (lineStringMap.containsKey(i)) {
						elem.setMinResolution(extractResolution(i-1));
						return;
					}
				}
			}
			elem.setMinResolution(extractResolution(endLevel));
		} else {
			int res = extractResolution(level);
			elem.setMinResolution(res);
			elem.setMaxResolution(res);
		}
	}

	/**
	 * Extract the resolution from the Data label.  The name will be something
	 * like Data2: from that we know it is at level 2 and we can look up
	 * the resolution.
	 *
	 * @param name The name tag DataN, where N is a digit corresponding to the
	 * level.
	 *
	 * @return The resolution that corresponds to the level.
	 */
	private int extractResolution(String name) {
		currentLevel = Integer.parseInt(name.substring(name.charAt(0) == 'O'? 6: 4));
		if (currentLevel > 0)
			dataHighLevel = true;
		return extractResolution(currentLevel);
	}

	/**
	 * Extract resolution from the level.
	 *
	 * @param level The level (0..)
	 * @return The resolution.
	 * @see #extractResolution(String name)
	 */
	private int extractResolution(int level) {
		int nlevels = levels.length;

		// Some maps use EndLevel=9 to mean the highest level
		if (level >= nlevels)
			level = nlevels - 1;

		LevelInfo li = levels[nlevels - level - 1];
		return li.getBits();
	}


	/**
	 * The initial 'IMG ID' section.  Contains miscellaneous parameters for
	 * the map.
	 *
	 * @param name Command name.
	 * @param value Command value.
	 */
	private void imgId(String name, String value) {
		if ("Copyright".equals(name)) {
			copyright = value;
		} else if ("Levels".equals(name)) {
			int nlev = Integer.parseInt(value);
			levels = new LevelInfo[nlev];
		} else if (name.startsWith("Level")) {
			int level = Integer.parseInt(name.substring(5), 10);
			int bits = Integer.parseInt(value);
			LevelInfo info = new LevelInfo(level, bits);

			int nlevels = levels.length;
			if (level >= nlevels)
				return;

			levels[nlevels - level - 1] = info;
		} else if (name.startsWith("Elevation")) {
			char fc = value.charAt(0);
			if (fc == 'm' || fc == 'M')
				elevUnits = 'm';
		} else if ("CodePage".equalsIgnoreCase(name)) {
			dec = Charset.forName("cp" + value).newDecoder();
			dec.onUnmappableCharacter(CodingErrorAction.REPLACE);
		} else if (name.endsWith("LeftSideTraffic")){
			if ("Y".equals(value)){
				setDriveOnLeft(true);
			} else if ("N".equals(value)){ 
				setDriveOnLeft(false);
			}
		} else if ("Transparent".equals(name)) {
			if ("Y".equals(value) || "S".equals(value))
				poiDispFlag |= TREHeader.POI_FLAG_TRANSPARENT; 
		} else if ("POIZipFirst".equals(name)) {
			if ("Y".equals(value))
				poiDispFlag |= TREHeader.POI_FLAG_POSTALCODE_BEFORE_CITY; 
		} else if ("POINumberFirst".equals(name)) {
			if ("N".equals(value))
				poiDispFlag |= TREHeader.POI_FLAG_STREET_BEFORE_HOUSENUMBER;  
		} else if ("Numbering".equals(name)) {
			// ignore
		} else if ("Routing".equals(name)) {
			if ("Y".equals(value)) {
				routing = true; // don't generate roadid
			}
		} else if ("CountryName".equalsIgnoreCase(name)) {
			defaultCountry = value;
		} else if ("RegionName".equalsIgnoreCase(name)) {
			defaultRegion = value;
		} else {
			log.info("'IMG ID' section: ignoring " + name + " " + value);
		}
		
	}

	/**
	 * Create a coordinate from a string.  The string will look similar:
	 * (2.3454,-0.23), but may not have the leading opening parenthesis.
	 * @param value A string representing a lat,long pair.
	 * @return The coordinate value.
	 */
	private static Coord makeCoord(String value) {
		String[] fields = value.split("[(,)]");

		int i = 0;
		if (fields[0].isEmpty())
			i = 1;

		Double f1 = Double.valueOf(fields[i]);
		Double f2 = Double.valueOf(fields[i+1]);
		return new Coord(f1, f2);
	}

	private ExtTypeAttributes makeExtTypeAttributes() {
		Map<String, String> eta = new HashMap<>();
		int colour = 0;
		int style = 0;

		for(Map.Entry<String, String> entry : extraAttributes.entrySet()) {
			String v = entry.getValue();
			if ("Depth".equals(entry.getKey())) {
				String u = extraAttributes.get("DepthUnit");
				if("f".equals(u))
					v += "ft";
				eta.put("depth", v);
			} else if("Height".equals(entry.getKey())) {
				String u = extraAttributes.get("HeightUnit");
				if("f".equals(u))
					v += "ft";
				eta.put("height", v);
			} else if("HeightAboveFoundation".equals(entry.getKey())) {
				String u = extraAttributes.get("HeightAboveFoundationUnit");
				if("f".equals(u))
					v += "ft";
				eta.put("height-above-foundation", v);
			} else if("HeightAboveDatum".equals(entry.getKey())) {
				String u = extraAttributes.get("HeightAboveDatumUnit");
				if("f".equals(u))
					v += "ft";
				eta.put("height-above-datum", v);
			} else if("Color".equals(entry.getKey())) {
				colour = Integer.decode(v);
			} else if("Style".equals(entry.getKey())) {
				style = Integer.decode(v);
			} else if("Position".equals(entry.getKey())) {
				eta.put("position", v);
			} else if("FoundationColor".equals(entry.getKey())) {
				eta.put("color", v);
			} else if("Light".equals(entry.getKey())) {
				eta.put("light", v);
			} else if("LightType".equals(entry.getKey())) {
				eta.put("type", v);
			} else if("Period".equals(entry.getKey())) {
				eta.put("period", v);
			} else if("Note".equals(entry.getKey())) {
				eta.put("note", v);
			} else if("LocalDesignator".equals(entry.getKey())) {
				eta.put("local-desig", v);
			} else if("InternationalDesignator".equals(entry.getKey())) {
				eta.put("int-desig", v);
			} else if("FacilityPoint".equals(entry.getKey())) {
				eta.put("facilities", v);
			} else if("Racon".equals(entry.getKey())) {
				eta.put("racon", v);
			} else if("LeadingAngle".equals(entry.getKey())) {
				eta.put("leading-angle", v);
			}
		}

		if(colour != 0 || style != 0)
			eta.put("style", "0x" + Integer.toHexString((style << 8) | colour));

		return new ExtTypeAttributes(eta, "Line " + lineNo);
	}

    /**
     * Construct the restrictions object.
     */
    private void restriction(String name, String value) {
        try {
            // Proceed only if the restriction is not already marked as invalid.
            if (restriction.isValid()) {
                if ("Nod".equalsIgnoreCase(name)) {
                    /* ignore */
                } else if ("TraffPoints".equalsIgnoreCase(name)) {
                	restriction.setTrafficPoints(value);
                } else if ("TraffRoads".equalsIgnoreCase(name)) {
                	restriction.setTrafficRoads(value);
                } else if ("RestrParam".equalsIgnoreCase(name)) {
                    restriction.setExceptMask(getRestrictionExceptionMask(value));
                } else if ("Time".equalsIgnoreCase(name)) {
                	log.info("Time in restriction definition is ignored " + restriction);
                }
            }
        } catch (NumberFormatException ex) { // This exception means that this restriction is not properly defined.
            restriction.setValid(false); // Mark this as an invalid restriction.
            log.error("Invalid restriction definition. " + restriction);
        }
    }

    /**
     * Constructs the vehicle exception mask from the restriction params.
     * From cGPSMapper manual :-
     * <p>
     * By default restrictions apply to all kind of vehicles, if
     * RestrParam is used, then restriction will be ignored by
     * specified types of vehicles.
     * </p>
     * <p>
     * [Emergency],[delivery],[car],[bus],[taxi],[pedestrian],[bicycle],[truck]
     * </p>
     * <p>
     * Example:
     * RestrParam=0,1,1,0
     * </p>
     * Above definition will set the restriction to be applied for
     * Emergency, Bus, Taxi, Pedestrian and Bicycle. Restriction
     * will NOT apply for Delivery and Car.
     *
     * @param value Tag value
     * @return the exceptMask in mkgmap internal format
     */
    private static byte getRestrictionExceptionMask(String value) {
        String[] params = value.split(",");
        byte exceptMask = 0x00;
        if (params.length > 0 && params.length <= 8) { // Got to have at least one parameter but not more than 8.
			for (int i = 0; i < params.length; i++) {
                if ("1".equals(params[i])) {
                    switch(i) {
					case 0:
						exceptMask |= AccessTagsAndBits.EMERGENCY;
						break;
					case 1:
						exceptMask |= AccessTagsAndBits.DELIVERY;
						break;
					case 2:
						exceptMask |= AccessTagsAndBits.CAR;
						break;
					case 3:
						exceptMask |= AccessTagsAndBits.BUS;
						break;
					case 4:
						exceptMask |= AccessTagsAndBits.TAXI;
						break;
					case 5:
						exceptMask |= AccessTagsAndBits.FOOT;
						break;
					case 6:
						exceptMask |= AccessTagsAndBits.BIKE;
						break;
					case 7:
						exceptMask |= AccessTagsAndBits.TRUCK;
						break;
                    }
                }
            }
        } else {
            log.error("Invalid RestrParam definition. -> " + value);
        }

        return exceptMask;
    }

	@Override
	public int getPoiDispFlag() {
		return poiDispFlag;
	}

	public String getDefaultCountry() {
		return defaultCountry;
	}

	public String getDefaultRegion() {
		return defaultRegion;
	}

	private int decodeType(FeatureKind kind, String type) {
		try {
			int t = Integer.decode(type);
			if ((kind == FeatureKind.POLYGON || kind == FeatureKind.POLYLINE) && t >= 0x100 && t < 0x10000
					&& (t & 0xff) == 0) {
				// allow 0xYY00 instead of 0xYY
				t >>= 8;
			}
			checkType(kind, t);
			return t;
		} catch (NumberFormatException e) {
			throw new MapFailedException("type not numeric " + type + " for " + kind + ", line " + lineNo);
		}
	}

	
	private void checkType(FeatureKind kind, int type) {
		if (kind == FeatureKind.POLYGON && type == 0x4a) // allow 0x4a polygon for preview map in polish format
			return; 
		if (!GType.checkType(kind, type)) {
			throw new MapFailedException("invalid type " + GType.formatType(type) + " for " + kind + ", line " + lineNo);
		}
	}

}
