/*
 * 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: 12-Dec-2006
 */
package uk.me.parabola.imgfmt.app.trergn;

import java.text.DecimalFormat;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import uk.me.parabola.imgfmt.app.Label;
import uk.me.parabola.imgfmt.app.lbl.LBLFile;
import uk.me.parabola.log.Logger;

/*
Add support for extended type attributes.

These are nearly all for marine objects. Attribute values are supplied
as tags with a mkgmap:xt- prefix. These tags are supported:

mkgmap:xt-depth
mkgmap:xt-height
  value is distance with optional units suffix (ft or m)
  applicable to points with types 0x0103xx and 0x0104xx and
  lines with types 0x0103xx and 0x010105-0x010107 and areas
  with types 0x0103xx.

mkgmap:xt-style
  value is 16 bit integer that specifies colour (lower 8 bits)
  and line style (upper 8 bits) - applicable to lines with types
  0x0104xx, 0x0105xx, 0x0106xx and points with type 0x010500 (colour
  only).

mkgmap:xt-colour
  value is one of:
	red
	green
	yellow
	white
	black
	black-yellow
	white-red
	black-red
	white-green
	red-yellow
	red-green
	orange
	black-yellow-black
	yellow-black
	yellow-black-yellow
	red-white
	green-red-green
	red-green-red
	black-red-black
	yellow-red-yellow
	green-red
	black-white
	white-orange
	orange-white
	green-white
  applicable to points with type 0x0102xx (buoys) and it specifies the
  foundation colour of the buoy.

mkgmap:xt-type
  value is one of:
	fixed
	isophase
	flashing
	group flashing
	composite group flashing
	occulting
	group occulting
	composite group occulting
	long flashing
	group long flashing
	(a morse code letter)
	quick
	group quick
	group quick and long flashing
	interrupted quick
	very quick
	group very quick
	group very quick and long flashing
	interrupted very quick
	ultra quick
	interrupted ultra quick
	fixed and occulting
	fixed and group occulting
	fixed and isophase
	fixed and flashing
	fixed and group flashing
	fixed and long flashing
	alternating
	alternating occulting
	alternating flashing
	alternating group flashing
  applicable to points with types 0x0101xx (lights) and 0x0102xx (buoys)
  and it specifies the type of light

mkgmap:xt-light
  one or more light definitions (separated by ;: or /) - each light
  definition is of the form colour,range,angle where colour is one
  of:
	unlit
	red
	green
	white
	blue
	yellow
	violet
	amber
  range is an optional number that specifies the visible range in nm -
  angle is the start angle for the light (only makes sense when more than
  one light is defined.

mkgmap:xt-period
  value is one or more period values (in seconds) (separated by commas) -
  applicable to point types 0x0101xx (lights) and 0x0102xx (buoys).

mkgmap:xt-racon
  when set to yes/true/1 specifies object has a racon - applicable to
  point types 0x0101xx (lights).

mkgmap:xt-height-above-foundation
mkgmap:xt-height-above-datum
  value is distance with optional units (m or ft) - applicable to
  point types 0x0101xx (lights).

mkgmap:xt-leading-angle
  value is a number - applicable to point types 0x0101xx (lights).

mkgmap:xt-note
mkgmap:xt-int-desig
mkgmap:xt-local-desig
  values are strings that are encoded as labels - applicable to
  points of type 0x01xx (lights) and 0x0102xx (buoys).

mkgmap:xt-facilities
  value is a bitmask of facilities available:
    0x000001 boat ramp
    0x000002 drinking water
    0x000004 restrooms
    0x000008 picnic area
    0x000010 campground
    0x000020 marina
    0x000040 fuel
    0x000080 marine supply
    0x000100 bait and tackle
    0x000200 groceries
    0x000400 restaurant
    0x000800 water/electric hook-up
    0x001000 boat/motor rental
    0x002000 guide service
    0x004000 lodging
    0x008000 dump station
    0x010000 handicap accessible
  applicable only to points of type 0x010903 (facility)
 */

public class ExtTypeAttributes {

	private static final Logger log = Logger.getLogger(ExtTypeAttributes.class);

	private final Map<String, String> attributes;
	private final String objectName;

	private byte[] extraBytes;

	private Label note;
	private Label intDesig;
	private Label localDesig;

	private Byte morseLetter;

	private static final int DISTANCE_FLAG_METRIC_INDEX = 0;
	private static final int DISTANCE_FLAG_TENTHS_INDEX = 1;

	private static final byte FLAGS0_RACON_BIT       = (1);
	private static final byte FLAGS0_NOTE_BIT        = (1 << 1);
	private static final byte FLAGS0_LOCAL_DESIG_BIT = (1 << 2);
	private static final byte FLAGS0_INT_DESIG_BIT   = (1 << 3);
	private static final int[] ZERO_INT_ARRAY = new int[0];

	public ExtTypeAttributes(Map<String, String> attributes, String objectName) {
		this.attributes = attributes;
		this.objectName = objectName;
	}

	public void processLabels(LBLFile lbl) {
		if(note == null) {
			String ns = attributes.get("note");
			if(ns != null)
				note = lbl.newLabel(ns, false);
		}
		if(intDesig == null) {
			String ids = attributes.get("int-desig");
			if(ids != null)
				intDesig = lbl.newLabel(ids, false);
		}
		if(localDesig == null) {
			String lds = attributes.get("local-desig");
			if(lds != null)
				localDesig = lbl.newLabel(lds, false);
		}
	}

	protected byte[] getExtTypeExtraBytes(MapObject mapObject) {
		try {
			return encodeExtraBytes(mapObject);
		} catch (Exception e) {
			log.error(objectName + " (" + e + ")");
			return null;
		}
	}

	private byte[] encodeExtraBytes(MapObject mapObject) {

		// if we get called again, just return same result as before
		if(extraBytes != null)
			return extraBytes;

		// first see if a string of raw hex digits has been supplied
		// if so, use that and ignore everything else
		String eb = attributes.get("extra-bytes");
		if(eb != null) {
			extraBytes = new byte[(eb.length() + 1) / 2];
			for(int i = 0; i < eb.length(); ++i) {
				int d = Integer.parseInt(eb.substring(i, i + 1), 16);
				extraBytes[i / 2] |= (byte)(d << (4 * (1 - (i & 1))));
			}
			return extraBytes;
		}

		int type0to15 = mapObject.getType() & 0xffff;
		int type8to15 = type0to15 & 0xff00;

		// seamark:light:character = type [[(group)] colour period 's' height 'm' range 'M']
		if(attributes.get("seamark:light:character") != null) {
			String[] parts = attributes.get("seamark:light:character").split(" ");
			String group = attributes.get("seamark:light:group");

			if(parts.length > 0) {
				String lt = null;
				int i = 1;
				if(parts[0].startsWith("Fl"))
					lt = "flashing";
				else if(parts[0].startsWith("F"))
					lt = "fixed";
				else if(parts[0].startsWith("LFL"))
					lt = "long flashing";
				else if(parts[0].startsWith("Q"))
					lt = "quick";
				else if(parts[0].startsWith("VQ"))
					lt = "very quick";
				else if(parts[0].startsWith("UQ"))
					lt = "ultra quick";
				else if(parts[0].startsWith("Iso"))
					lt = "isophase";
				else if(parts[0].startsWith("Oc"))
					lt = "occulting";
				else if(parts[0].startsWith("IQ"))
					lt = "interrupted quick";
				else if(parts[0].startsWith("IVQ "))
					lt = "interrupted very quick";
				else if(parts[0].startsWith("IUQ"))
					lt = "interrupted ultra quick";
				else if(parts[0].startsWith("AI"))
					lt = "alternating";
				else if(parts[0].startsWith("Mo")) {
					if(parts[0].indexOf('(') == 2)
						lt = parts[0].substring(3, 4);
					else if(parts.length > 1 && parts[i].startsWith("(")) {
						lt = parts[i].substring(1, 2);
						++i;
					}
				}

				String group2 = null;
				if(!parts[0].startsWith("Mo") && parts[0].indexOf('(') > 0) {
					group2 = parts[0].substring(parts[0].indexOf('(') + 1, parts[0].length() - 1);
				}
				else if(i < parts.length && parts[i].startsWith("(")) {
					// should be (group)
					group2 = parts[i].substring(1, parts[i].length() - 1);
					++i;
				}

				if(group2 != null) {
					if(group != null && !group.equals(group2))
						log.warn("Inconsistent light description - seamark:light:group = '" + group + "' but seamark:light:character contains '" + parts[0] + "'");
					else
						group = group2;
				}

				if(lt != null) {
					if(group != null) {
						attributes.put("group", group); // FIXME - implement?
						lt = "group " + lt;
						if(group.split("\\+").length > 1)
							lt = "composite " + lt;
					}
					attributes.put("type", lt);
				}

				String light = null;
				if(i < parts.length) {
					// colour
					String c = parts[i].toUpperCase();
					if(c.startsWith("W"))
						light = "white";
					else if(c.startsWith("R"))
						light = "red";
					else if(c.startsWith("G"))
						light = "green";
					else if(c.startsWith("Y"))
						light = "yellow";
					++i;
				} else {
					light = attributes.get("light");
				}

				String period = null;
				if((i + 1) < parts.length && "s".equals(parts[i+1])) {
					// period
					period = parts[i];
					i += 2;
				}

				String height = null;
				if((i + 1) < parts.length && "m".equals(parts[i+1])) {
					// height
					height = parts[i];
					i += 2;
				}

				String range = null;
				if((i + 1) < parts.length && "M".equals(parts[i+1])) {
					// range
					range = parts[i];
					i += 2;
				} else {
					range = attributes.get("seamark:light:range");
				}

				if(light != null) {
					if(range != null)
						light += "," + range;
					attributes.put("light", light);
					if(period != null)
						attributes.put("period", period);
					if(height != null)
						attributes.put("height-above-datum", height + "m");
				}
			}
		}

		// seamark:light:# = colour ':' sectorStart ':' sectorEnd ':' range
		if(attributes.get("seamark:light:1") != null) {
			// when multiple lights are defined, they must be ordered
			// by increasing sector start angle
			class SeamarkLight implements Comparable<SeamarkLight> {
				String colour;
				int sectorStart;
				int sectorEnd;
				int range;
				SeamarkLight(String desc) {
					String[] parts = desc.split(":");
					if(parts.length == 4) {
						colour = parts[0];
						if ("shore".equalsIgnoreCase(parts[1]) || "shore".equalsIgnoreCase(parts[2])) {
							log.error(objectName + ": shore is no valid sector bound, please annotate a numeric value");
						} else {
							sectorStart = Double.valueOf(parts[1]).intValue();
							while(sectorStart >= 360)
								sectorStart -= 360;
							sectorEnd = Double.valueOf(parts[2]).intValue();
							while(sectorEnd >= 360)
								sectorEnd -= 360;
							range = Double.valueOf(parts[3]).intValue();
						}
					}
				}
				public int compareTo(SeamarkLight other) {
					if (sectorStart == other.sectorStart) return 0;
					else return sectorStart > other.sectorStart? 1: -1;
				}
			}
			List<SeamarkLight> lights = new ArrayList<>();
			// create a SeamarkLight for each light
			for(int n = 1; n <= 100; ++n) {
				String desc = attributes.get("seamark:light:" + n);
				if(desc != null)
					lights.add(new SeamarkLight(desc));
				else
					break;
			}
			// sort the lights by increasing sector start angle
			lights.sort(null);
			// generate the descriptor string - each light is
			// specified as color,range,sectorStartAngle
			StringBuilder light = null;
			for(int i = 0; i < lights.size(); ++i) {
				SeamarkLight sml = lights.get(i);
				if(light == null)
					light = new StringBuilder();
				else
					light.append('/');
				light.append(sml.colour).append(',').append(sml.range).append(',').append(sml.sectorStart);
				if((i + 1) < lights.size()) {
					if(sml.sectorEnd != lights.get(i + 1).sectorStart) {
						// gap between lit sectors
						light.append("/unlit,0,").append(sml.sectorEnd);
					}
				}
				else if(sml.sectorEnd != lights.get(0).sectorStart) {
					// gap to end
					light.append("/unlit,0,").append(sml.sectorEnd);
				}
			}
			if(light != null) {
				attributes.put("light", light.toString());
				if(attributes.get("seamark:light:character") == null)
					attributes.put("type", "fixed");
			}
		}

		String sequence = attributes.get("seamark:light:sequence");

		if(sequence != null) {
			StringBuilder periods = new StringBuilder();
			StringBuilder eclipse = new StringBuilder();
			for(String p : sequence.split("[+,]")) {
				if (p.startsWith("(") && p.endsWith(")")) {
					// phases of eclipse are enclosed in (), remove them
					p = p.substring(1, p.length()-1);
					if(eclipse.length() > 0)
						eclipse.append(",");
					eclipse.append(Double.parseDouble(p));
				} else {
					if(periods.length() > 0)
						periods.append(",");
					periods.append(Double.parseDouble(p));
				}
			}
			attributes.put("period", periods.toString());
			attributes.put("eclipse", eclipse.toString());
		}

		if(mapObject instanceof Point) {

			Light[] lights = parseLights(attributes.get("light"));
			int[] periods = parsePeriods(attributes.get("period"));
			int[] eclipse = parsePeriods(attributes.get("eclipse"));
			if (!(periods.length == eclipse.length || 1 == periods.length)) 
				log.error(objectName + ": number of light and eclipse phases has to be equal");
			
			if(type8to15 == 0x0100) { // lights
				byte flags0 = 0;
				int lightType = lightType("");
				if(meansYes(attributes.get("racon")))
					flags0 |= FLAGS0_RACON_BIT;
				int nob = 6;
				if(note != null) {
					nob += 3;
					flags0 |= FLAGS0_NOTE_BIT;
				}
				if(intDesig != null) {
					nob += 3;
					flags0 |= FLAGS0_INT_DESIG_BIT;
				}
				if(localDesig != null) {
					nob += 3;
					flags0 |= FLAGS0_LOCAL_DESIG_BIT;
				}
				byte flags1 = 0;
				if(lights.length > 1) {
					for(Light l : lights)
						nob += (l.colour != 0)? 3 : 2;
					flags1 |= 0x08; // multiple lights
				}
				if(0 != eclipse.length) {
					for(int p : periods) {
						while(p > 0x3f) {
							++nob;
							p -= 0x3f;
						}
						++nob;
					}
					for(int p : eclipse) {
						while(p > 0x3f) {
							++nob;
							p -= 0x3f;
						}
						++nob;
					}
					flags1 |= 0x01; // further record present?
				}
				else if(morseLetter != null)
					flags1 |= 0x01; // further record present?
				byte lightsDef = 0x22;
				String hafs = attributes.get("height-above-foundation");
				Integer hafi = null;
				if(hafs != null) {
					boolean[] hafsDistFlags = new boolean[2];
					hafi = parseDistance(hafs, hafsDistFlags);
					if(hafsDistFlags[DISTANCE_FLAG_TENTHS_INDEX])
						hafi /= 10;
					nob += (hafi > 255)? 2 : 1;
					if(hafi > 255)
						lightsDef |= 0x80;
					else
						lightsDef |= 0x40;
					if(!hafsDistFlags[DISTANCE_FLAG_METRIC_INDEX])
						lightsDef &= ~0x20;
				}
				String hads = attributes.get("height-above-datum");
				Integer hadi = null;
				if(hads != null) {
					boolean[] hadsDistFlags = new boolean[2];
					hadi = parseDistance(hads, hadsDistFlags);
					if(hadsDistFlags[DISTANCE_FLAG_TENTHS_INDEX])
						hadi /= 10;
					nob += (hadi > 255)? 2 : 1;
					if(hadi > 255)
						lightsDef |= 0x08;
					else
						lightsDef |= 0x04;
					if(!hadsDistFlags[DISTANCE_FLAG_METRIC_INDEX])
						lightsDef &= ~0x02;
				}
				String las = attributes.get("leading-angle");
			    Integer leadingAngle = null;
				if(las != null) {
					leadingAngle = (int)(Double.parseDouble(las.trim()) * 10);
					nob += 2;
					flags1 |= 0x02; // leading angle present
				}
				int period = 0;
				for(int p : periods)
					period += p;
				for(int p : eclipse)
					period += p;
				if(period > 255)
					lightType |= 0x40; // 9th bit of period
				else if(period > 511) {
					period = 511;
					log.warn("Can't encode periods greater than 51.1 seconds for lights");
				}

				extraBytes = new byte[nob + 2];
				int i = 0;
				extraBytes[i++] = (byte)(0xe0 | flags0);
				extraBytes[i++] = (byte)((nob << 1) | 1); // bit0 always set?
				extraBytes[i++] = (byte)(0x80 | lightType);
				extraBytes[i++] = flags1;
				extraBytes[i++] = lightsDef;
				if(hafi != null) {
					extraBytes[i++] = (byte)(int)hafi;
					if(hafi > 255)
						extraBytes[i++] = (byte)(hafi >> 8);
				}
				if(hadi != null) {
					extraBytes[i++] = (byte)(int)hadi;
					if(hadi > 255)
						extraBytes[i++] = (byte)(hadi >> 8);
				}
				extraBytes[i++] = (byte)period;
				if(note != null) {
					int off = note.getOffset();
					extraBytes[i++] = (byte)off;
					extraBytes[i++] = (byte)(off >> 8);
					extraBytes[i++] = (byte)(off >> 16);
				}
				if(localDesig != null) {
					int off = localDesig.getOffset();
					extraBytes[i++] = (byte)off;
					extraBytes[i++] = (byte)(off >> 8);
					extraBytes[i++] = (byte)(off >> 16);
				}
				if(intDesig != null) {
					int off = intDesig.getOffset();
					extraBytes[i++] = (byte)off;
					extraBytes[i++] = (byte)(off >> 8);
					extraBytes[i++] = (byte)(off >> 16);
				}
				if(leadingAngle != null) {
					extraBytes[i++] = (byte)(int)leadingAngle;
					extraBytes[i++] = (byte)(leadingAngle >> 8);
				}
				if(lights.length > 1) {
					for(int l = 0; l < lights.length; ++l) {
						int val = (lights[l].colour << 12) | (int)(lights[l].angle * 10);
						if((l + 1) == lights.length)
							val |= 0x8000;
						extraBytes[i++] = (byte)val;
						extraBytes[i++] = (byte)(val >> 8);
						if(lights[l].colour != 0)
							extraBytes[i++] = (byte)lights[l].range;
					}
				}
				else {
					int lc = 0;
					int lr = 0;
					if(lights.length > 0) {
						lc = lights[0].colour;
						lr = (int)lights[0].range & 0x1f;
					}
					extraBytes[i++] = (byte)((lc << 5) | lr);
				}
				if(periods.length > 1) {
					extraBytes[i++] = (byte)(0x80 + periods.length);
					// first all lights
					for (int p : periods) {
						while(p > 0x3f) {
							extraBytes[i++] = (byte)0x3f;
							p -= 0x3f;
						}
						extraBytes[i++] = (byte)p;
					}
					// second all pause
					for (int p : eclipse) {
						while(p > 0x3f) {
							extraBytes[i++] = (byte)0x3f;
							p -= 0x3f;
						}
						extraBytes[i++] = (byte)p;
					}
				}
				else if(morseLetter != null)
					extraBytes[i++] = morseLetter;
				else
					extraBytes[i++] = 0x01; // terminator?

				return extraBytes;
			}
			else if(type8to15 == 0x0200) { // buoys
				byte flags0 = 0;
				int lt = lightType("");
				if(meansYes(attributes.get("racon"))) {
					// this doesn't get reported on mapsource
					// maybe racons aren't supported for buoys?
					flags0 |= FLAGS0_RACON_BIT;
				}
				int nob = 4;
				if(note != null) {
					nob += 3;
					flags0 |= FLAGS0_NOTE_BIT;
				}
				if(intDesig != null) {
					nob += 3;
					flags0 |= FLAGS0_INT_DESIG_BIT;
				}
				if(localDesig != null) {
					nob += 3;
					flags0 |= FLAGS0_LOCAL_DESIG_BIT;
				}
				if(periods.length > 0)
					++nob;		// for total period
				byte flags1 = 0;
				if(periods.length > 1) {
					for(int p : periods) {
						while(p > 0x3f) {
							++nob;
							p -= 0x3f;
						}
						++nob;
					}
					flags1 |= 0x02; // further record present?
				}
				else if(morseLetter != null)
					flags1 |= 0x02; // further record present?
				extraBytes = new byte[nob + 2];
				int i = 0;
				extraBytes[i++] = (byte)(0xe0 | flags0);
				extraBytes[i++] = (byte)((nob << 1) | 1); // bit0 always set?
				int lc = 0;
				if(lights.length > 0) {
					lc = lights[0].colour;
				}
				extraBytes[i++] = (byte)((lc << 6) | colour(""));
				flags1 |= (byte)((lc >> 2) & 1); // bit 0 is MSB of light colour
				extraBytes[i++] = flags1;
				if(note != null) {
					int off = note.getOffset();
					extraBytes[i++] = (byte)off;
					extraBytes[i++] = (byte)(off >> 8);
					extraBytes[i++] = (byte)(off >> 16);
				}
				if(localDesig != null) {
					int off = localDesig.getOffset();
					extraBytes[i++] = (byte)off;
					extraBytes[i++] = (byte)(off >> 8);
					extraBytes[i++] = (byte)(off >> 16);
				}
				if(intDesig != null) {
					int off = intDesig.getOffset();
					extraBytes[i++] = (byte)off;
					extraBytes[i++] = (byte)(off >> 8);
					extraBytes[i++] = (byte)(off >> 16);
				}
				int period = 0;
				for(int p : periods)
					period += p;
				if(period > 255) {
					period = 255;
					log.warn("Can't encode periods greater than 25.5 seconds for buoy lights");
				}
				byte flags2 = 0;
				if(periods.length > 0)
					flags2 |= (byte)0x80;
				extraBytes[i++] = (byte)(flags2 | lt);
				if(periods.length > 0) {
					extraBytes[i++] = (byte)period;
					if(periods.length > 1) {
						if(periods.length > 2)
							extraBytes[i++] = (byte)0x82;
						else
							extraBytes[i++] = (byte)0x81;
						for(int p : periods) {
							while(p > 0x3f) {
								extraBytes[i++] = (byte)0x3f;
								p -= 0x3f;
							}
							extraBytes[i++] = (byte)p;
						}
					}
					else
						extraBytes[i++] = 0x01; // terminator?
				}
				else if(morseLetter != null)
					extraBytes[i++] = morseLetter;
				else
					extraBytes[i++] = 0x01; // terminator?

				return extraBytes;
			}
			else if(type8to15 == 0x0300 || // things with depth/height
					type8to15 == 0x0400) { // obstructions
				String ds = attributes.get("depth");
				if(ds == null)
					ds = attributes.get("height");
				if(ds != null) {
					boolean[] distFlags = new boolean[2];
					Integer di = parseDistance(ds, distFlags);
					if(di != null) {
						if(di > 255) {
							extraBytes = new byte[3];
							extraBytes[0] = (byte)0xa0;
						}
						else {
							extraBytes = new byte[2];
							extraBytes[0] = (byte)0x80;
						}
						if(distFlags[DISTANCE_FLAG_METRIC_INDEX])
							extraBytes[0] |= 0x10;
						if(distFlags[DISTANCE_FLAG_TENTHS_INDEX])
							extraBytes[0] |= 0x08;
						if(type8to15 == 0x0400) { // obstructions
							extraBytes[0] |= position();
						}
						extraBytes[1] = (byte)(int)di;
						if(di > 255)
							extraBytes[2] = (byte)(di >> 8);

						return extraBytes;
					}
				}
			}
			else if(type8to15 == 0x0500) { // label
				String ss = attributes.get("style");
				if(ss != null) {
					int style = Integer.decode(ss.trim());
					// format is 0xCC (CC = colour)
					extraBytes = new byte[1];
					extraBytes[0] = (byte)(style & 0xf);
					return extraBytes;
				}
			}
			else if(type0to15 == 0x0903) { // facility
				String fs = attributes.get("facilities");
				if(fs != null) {
					int facilities = Integer.decode(fs.trim());
					extraBytes = new byte[3];
					extraBytes[0] = (byte)(0xa0 | (facilities & 0x1f));
					extraBytes[1] = (byte)(facilities >> 5);
					extraBytes[2] = (byte)(((facilities >> 13) & 0x07) |
										   ((facilities >> 12) & 0x18));
					return extraBytes;
				}
			}
		}
		else if(mapObject instanceof Polyline) {

			if(type8to15 == 0x0300 ||	// depth areas
			   (!(mapObject instanceof Polygon) &&
				(type0to15 == 0x0105 || // contour line
				 type0to15 == 0x0106 || // overhead cable
				 type0to15 == 0x0107))) { // bridge
				String ds = attributes.get("depth");
				if(ds == null)
					ds = attributes.get("height");
				if(ds != null) {
					boolean[] distFlags = new boolean[2];
					Integer di = parseDistance(ds, distFlags);
					if(di != null) {
						if(di > 255) {
							extraBytes = new byte[3];
							extraBytes[0] = (byte)0xa0;
						}
						else {
							extraBytes = new byte[2];
							extraBytes[0] = (byte)0x80;
						}
						if(distFlags[DISTANCE_FLAG_METRIC_INDEX])
							extraBytes[0] |= 0x10;
						if(distFlags[DISTANCE_FLAG_TENTHS_INDEX])
							extraBytes[0] |= 0x08;
						if(type8to15 == 0x04) { // obstructions
							extraBytes[0] |= position();
						}
						extraBytes[1] = (byte)(int)di;
						if(di > 255)
							extraBytes[2] = (byte)(di >> 8);

						return extraBytes;
					}
				}
			}
			else if(!(mapObject instanceof Polygon) &&
					(type8to15 == 0x0400 || // various lines
					 type8to15 == 0x0500 ||
					 type8to15 == 0x0600)) {
				String ss = attributes.get("style");
				if(ss != null) {
					int style = Integer.decode(ss.trim());
					if ((style & 0xff00) == 0) {
						// format is 0xCC (CC = colour)
						extraBytes = new byte[1];
						extraBytes[0] = (byte) (style & 0xf);
					} else {
						// format is 0xSSCC (SS = style, CC = colour)
						extraBytes = new byte[2];
						extraBytes[0] = (byte) (0x80 | (style & 0xf));
						extraBytes[1] = (byte) (((style >> 9) & 0x18) | ((style >> 8) & 0x3));
					}
					return extraBytes;
				}
			}
		}

		return null;
	}

	private static boolean meansYes(String s) {
		if(s == null)
			return false;
		s = s.toLowerCase();
		return ("yes".startsWith(s) || "true".startsWith(s) || "1".equals(s));
	}

	private static Integer parseDistance(String ds, boolean[] flags) {
		ParsePosition pp = new ParsePosition(0);
		Number dn = new DecimalFormat().parse(ds, pp);
		if(dn != null) {
			double dd = dn.doubleValue();
			int di = dn.intValue();
			flags[DISTANCE_FLAG_METRIC_INDEX] = true;
			flags[DISTANCE_FLAG_TENTHS_INDEX] = false;
			if("ft".equalsIgnoreCase(ds.substring(pp.getIndex()).trim()))
				flags[DISTANCE_FLAG_METRIC_INDEX] = false;
			if((double)di != dd) {
				// number has fractional part
				di = (int)(dd * 10);
				flags[DISTANCE_FLAG_TENTHS_INDEX] = true;
			}

			return di;
		}

		return null;
	}

	private int colour(String prefix) {

		String c = attributes.get(prefix + "colour");
		if(c == null)
			c = attributes.get(prefix + "color");
		if(c == null)
			return 0;

		c = c.trim();

		if(Character.isDigit(c.charAt(0))) {
			return Integer.decode(c);
		}

		String[] colours = {
			"",
			"red",
			"green",
			"yellow",
			"white",
			"black",
			"black-yellow",
			"white-red",
			"black-red",
			"white-green",
			"red-yellow",
			"red-green",
			"orange",
			"black-yellow-black",
			"yellow-black",
			"yellow-black-yellow",
			"red-white",
			"green-red-green",
			"red-green-red",
			"black-red-black",
			"yellow-red-yellow",
			"green-red",
			"black-white",
			"white-orange",
			"orange-white",
			"green-white"
		};

		c = c.toLowerCase().replaceAll("[;, ]+", "-");

		for(int i = 0; i < colours.length; ++i)
			if(colours[i].equals(c))
				return i;

		return 0;
	}

	private int lightType(String prefix) {
		String lt = attributes.get(prefix + "type");
		if(lt == null)
			return 0;

		lt = lt.trim();

		if(Character.isDigit(lt.charAt(0))) {
			return Integer.decode(lt);
		}

		if(lt.length() == 1) {
			morseLetter = (byte)lt.charAt(0);
			return 0x0b;
		}

		String[] types = {
			"",
			"fixed",
			"isophase",
			"flashing",
			"group flashing",
			"composite group flashing",
			"occulting",
			"group occulting",
			"composite group occulting",
			"long flashing",
			"group long flashing",
			"morse code letter",
			"quick",
			"group quick",
			"group quick and long flashing",
			"interrupted quick",
			"very quick",
			"group very quick",
			"group very quick and long flashing",
			"interrupted very quick",
			"ultra quick",
			"interrupted ultra quick",
			"fixed and occulting",
			"fixed and group occulting",
			"fixed and isophase",
			"fixed and flashing",
			"fixed and group flashing",
			"fixed and long flashing",
			"alternating",
			"alternating occulting",
			"alternating flashing",
			"alternating group flashing"
		};

		lt = lt.toLowerCase();

		for(int i = 0; i < types.length; ++i)
			if(types[i].equals(lt))
				return i;

		return 0;
	}

	private int position() {
		String ps = attributes.get("position");
		if(ps == null)
			return 0;

		ps = ps.trim();

		if(Character.isDigit(ps.charAt(0))) {
			return Integer.decode(ps);
		}

		String[] positions = {
			"unknown",
			"",
			"doubtful",
			"existence doubtful",
			"approximate",
			"reported"
		};

		ps = ps.toLowerCase();

		for(int i = 0; i < positions.length; ++i)
			if(positions[i].equals(ps))
				return i;

		return 0;
	}

	private static int[] parsePeriods(String ps) {
		if(ps == null)
			return ZERO_INT_ARRAY;
		String [] psa = ps.split(",");
		int [] periods = new int[psa.length];
		for(int i = 0; i < psa.length; ++i)
			periods[i] = (int)(Double.parseDouble(psa[i].trim()) * 10);
		return periods;
	}

	private Light[] parseLights(String ls) {
		if(ls == null)
			return new Light[0];
		ls = ls.trim();
		String[] defs = new String[0];
		if(ls.startsWith("(")) {
			// handle polish syntax "(c,r,a),(c,r,a)..."
			List<String> out = new ArrayList<>();
			int start = 0;
			// start should be on the '(' at the start of the loop
			while(start < ls.length()) {
				int end = ++start;
				while(end < ls.length() && ls.charAt(end) != ')')
					++end;
				out.add(ls.substring(start, end));
				start = end + 1;
				while(start < ls.length() && ls.charAt(start) != '(')
					++start;
			}
			defs = out.toArray(defs);
		}
		else {
			// handle our "simple" syntax
			defs = ls.split("[:;/]");
		}
		Light[] lights = new Light[defs.length];
		for(int i = 0; i < defs.length; ++i) {
			String def = defs[i].trim();
			if(def.length() > 0)
				lights[i] = new Light(def);
		}
		return lights;
	}

	class Light {
		private int    colour;
		private double range;
		private double angle;

		final String[] colours = {
			"unlit",
			"red",
			"green",
			"white",
			"blue",
			"yellow",
			"violet",
			"amber"
		};

		public Light(String desc) {
			String[] parts = desc.split(",");
			if(parts.length > 0) {
				String lc = parts[0].trim().toLowerCase();
				if(Character.isDigit(lc.charAt(0))) {
					colour = Integer.decode(lc);
				}
				else {
					for(int i = 0; i < colours.length; ++i) {
						if(colours[i].equals(lc)) {
							colour = i;
							break;
						}
					}
				}
			}
			if(parts.length > 1 && colour != 0)
				range = Double.parseDouble(parts[1]);
			if(parts.length > 2)
				angle = Double.parseDouble(parts[2]);
		}

		public String toString() {
			return "(" + colours[colour] + "," + range + "," + angle + ")";
		}
	}
}
