package org.traccar.protocol;

import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.traccar.GenericProtocolDecoder;
import org.traccar.helper.Log;
import org.traccar.model.DataManager;
import org.traccar.model.Position;

public class ST210ProtocolDecoder extends GenericProtocolDecoder {

	/**
	 * Initialize
	 */
	public ST210ProtocolDecoder(DataManager dataManager, Integer resetDelay) {
		super(dataManager, resetDelay);
	}

	private enum ST210FIELDS {
		HDR_STATUS("SA200STT;", "Status Report"), HDR_EMERGENCY("SA200EMG;",
				"Emergency Report"), HDR_EVENT("SA200EVT;", "Event Report"), HDR_ALERT(
				"SA200ALT;", "Alert Report"), HDR_ALIVE("SA200ALV;",
				"Alive Report"), DEV_ID("(\\d+);", "Device ID"), SW_VER(
				"(\\d{3});", "Software Release Version"), DATE("(\\d+);",
				"GPS date (yyyymmdd) Year + Month + Day"), TIME(
				"(\\d{2}:\\d{2}:\\d{2});",
				"GPS time (hh:mm:ss) Hour : Minute : Second"), CELL(
				"(\\d{2}\\w\\d{2});",
				"Location Code ID (3 digits hex) + Serving Cell BSIC(2 digits decimal)"), LAT(
				"(-\\d{2}.\\d+);", "Latitude (+/-xx.xxxxxx)"), LON(
				"(-\\d{3}.\\d+);", "Longitude (+/-xxx.xxxxxx)"), SPD(
				"(\\d{3}.\\d{3});",
				"Speed in km/h - This value returns to 0 when it is over than 200,000Km"), CRS(
				"(\\d{3}.\\d{2});", "Course over ground in degree"), SATT(
				"(\\d+);", "Number of satellites"), FIX("(\\d);",
				"GPS is fixed(1)\n" + "GPS is not fixed(0)"), DIST("(\\d+);",
				"Traveled ddistance in meter"), PWR_VOLT("(\\d{2}.\\d{2});",
				"Voltage value of main power"), IO("(\\d+);",
				"Current I/O status of inputs and outputs."), MODE("(\\d);",
				"1 = Idle mode (Parking)\n" + "2 = Active Mode (Driving)"), MSG_NUM(
				"(\\d{4})",
				"Message number - After 9999 is reported, message number returns to 0000"), EMG_ID(
				"(\\d);", "Emergency type"), EVT_ID("(\\d);", "Event type"), ALERT_ID(
				"(\\d);", "Alert type");

		private String pattern;

		private String desc;

		private ST210FIELDS(String pattern, String desc) {
			this.pattern = pattern;
			this.desc = desc;
		}

		public String getPattern() {
			return pattern;
		}

		public String getDesc() {
			return desc;
		}

		public void populatePosition(Position position, String groupValue,
				DataManager dataManager) throws Exception {

			switch (this) {

			case DEV_ID:
				position.setDeviceId(dataManager.getDeviceByImei(groupValue)
						.getId());
				break;

			case LAT:
				position.setLatitude(Double.valueOf(groupValue));
				break;

			case LON:
				position.setLongitude(Double.valueOf(groupValue));
				break;

			case CRS:
				position.setCourse(Double.valueOf(groupValue));
				break;

			case PWR_VOLT:
				position.setPower(Double.valueOf(groupValue));
				break;

			case SPD:
				position.setSpeed(Double.valueOf(groupValue));
				break;

			case DATE: {
				// Date
				Calendar time = Calendar.getInstance(TimeZone
						.getTimeZone("UTC"));
				time.clear();
				time.set(Calendar.YEAR, Integer.valueOf(Integer
						.valueOf(groupValue.substring(0, 4))));
				time.set(Calendar.MONTH, Integer.valueOf(Integer
						.valueOf(groupValue.substring(4, 6))));
				time.set(Calendar.DAY_OF_MONTH, Integer.valueOf(Integer
						.valueOf(groupValue.substring(6, 8))));
				position.setTime(time.getTime());
				break;
			}

			case TIME: {

				Calendar time = Calendar.getInstance(TimeZone
						.getTimeZone("UTC"));
				time.clear();
				time.setTime(position.getTime());

				time.set(Calendar.HOUR, Integer.valueOf(Integer
						.valueOf(groupValue.substring(0, 2))));
				time.set(Calendar.MINUTE, Integer.valueOf(Integer
						.valueOf(groupValue.substring(3, 5))));
				time.set(Calendar.SECOND, Integer.valueOf(Integer
						.valueOf(groupValue.substring(6, 8))));

				position.setTime(time.getTime());
				break;
			}

			default:
				break;
			}

		}
	}

	private enum FIELD_FIX_VALUE {
		FIXED(1, "GPS is fixed"), NOT_FIXED(0, "GPS is not fixed");

		private int value;

		private String desc;

		private FIELD_FIX_VALUE(int value, String desc) {
			this.value = value;
			this.desc = desc;
		}

		public int getValue() {
			return value;
		}

		public String getDesc() {
			return desc;
		}

		public FIELD_FIX_VALUE getValueOf(String indiceStr) {
			int indice = Integer.valueOf(indiceStr);
			return getValueOf(indice);
		}

		public FIELD_FIX_VALUE getValueOf(int indice) {
			switch (indice) {
			case 1:
				return FIXED;
			case 0:
				return NOT_FIXED;
			default:
				throw new IllegalArgumentException("�ndice n�o definido");
			}
		}
	}

	private enum FIELD_MODE_VALUE {
		PARKING(1, "Idle mode (Parking)"), DRIVING(2, "Active Mode (Driving)");

		private int value;

		private String desc;

		private FIELD_MODE_VALUE(int value, String desc) {
			this.value = value;
			this.desc = desc;
		}

		public int getValue() {
			return value;
		}

		public String getDesc() {
			return desc;
		}

		public FIELD_MODE_VALUE getValueOf(String indiceStr) {
			int indice = Integer.valueOf(indiceStr);
			return getValueOf(indice);
		}

		public FIELD_MODE_VALUE getValueOf(int indice) {
			switch (indice) {
			case 1:
				return PARKING;
			case 2:
				return DRIVING;
			default:
				throw new IllegalArgumentException("�ndice n�o definido");
			}
		}
	}

	private enum FIELD_EMG_ID_VALUE {
		PANIC(1, "Emergency by panic button"), PARKING(2,
				"Emergency by parking lock"), MAIN_POWER(3,
				"Emergency by removing main power"), ANTI_THEFT(5,
				"Emergency by anti-theft");

		private int value;

		private String desc;

		private FIELD_EMG_ID_VALUE(int value, String desc) {
			this.value = value;
			this.desc = desc;
		}

		public int getValue() {
			return value;
		}

		public String getDesc() {
			return desc;
		}

		public FIELD_EMG_ID_VALUE getValueOf(String indiceStr) {
			int indice = Integer.valueOf(indiceStr);
			return getValueOf(indice);
		}

		public FIELD_EMG_ID_VALUE getValueOf(int indice) {
			switch (indice) {
			case 1:
				return PANIC;
			case 2:
				return PARKING;
			case 3:
				return MAIN_POWER;
			case 5:
				return ANTI_THEFT;
			default:
				throw new IllegalArgumentException("�ndice n�o definido");
			}
		}
	}

	private enum FIELD_EVT_ID_VALUE {
		INPUT1_GROUND(1, "Input1 goes to ground state"), INPUT1_OPEN(2,
				"Input1 goes to open state"), INPUT2_GROUND(3,
				"Input2 goes to ground state"), INPUT2_OPEN(4,
				"Input2 goes to open state"), INPUT3_GROUND(5,
				"Input3 goes to ground state"), INPUT3_OPEN(6,
				"Input3 goes to open state");

		private int value;

		private String desc;

		private FIELD_EVT_ID_VALUE(int value, String desc) {
			this.value = value;
			this.desc = desc;
		}

		public int getValue() {
			return value;
		}

		public String getDesc() {
			return desc;
		}

		public FIELD_EVT_ID_VALUE getValueOf(String indiceStr) {
			int indice = Integer.valueOf(indiceStr);
			return getValueOf(indice);
		}

		public FIELD_EVT_ID_VALUE getValueOf(int indice) {
			switch (indice) {
			case 1:
				return INPUT1_GROUND;
			case 2:
				return INPUT1_OPEN;
			case 3:
				return INPUT2_GROUND;
			case 4:
				return INPUT2_OPEN;
			case 5:
				return INPUT3_GROUND;
			case 6:
				return INPUT3_OPEN;
			default:
				throw new IllegalArgumentException("�ndice n�o definido");
			}
		}
	}

	private enum FIELD_ALERT_ID_VALUE {
		DRIVING_FASTER(1, "Start driving faster than SPEED_LIMIT"), OVER_SPPED(
				2, "Ended over speed condition"), DISCON_GPS(3,
				"Disconnected GPS antenna"), RECON_GPS(4,
				"Reconnected GPS antenna after disconnected"), OUT_GEO_FENCE(5,
				"The vehicle went out from the geo-fence that has following ID"), INTO_GEO_FENCE(
				6,
				"The vehicle entered into the geo-fence that has following ID"), SHORTED_GPS(
				8, "Shorted GPS antenna"), DEEP_SLEEP_ON(9,
				"Enter to deep sleep mode"), DEEP_SLEEP_OFF(10,
				"Exite from deep sleep mode"), BKP_BATTERY(13,
				"Backup battery error"), BATTERY_DOWN(14,
				"Vehicle battery goes down to so low level"), SHOCKED(15,
				"Shocked"), COLLISION(16, "Occurred some collision"), DEVIATE_ROUT(
				18, "Deviate from predefined rout"), ENTER_ROUT(19,
				"Enter into predefined rout");

		private int value;

		private String desc;

		private FIELD_ALERT_ID_VALUE(int value, String desc) {
			this.value = value;
			this.desc = desc;
		}

		public int getValue() {
			return value;
		}

		public String getDesc() {
			return desc;
		}

		public FIELD_ALERT_ID_VALUE getValueOf(String indiceStr) {
			int indice = Integer.valueOf(indiceStr);
			return getValueOf(indice);
		}

		public FIELD_ALERT_ID_VALUE getValueOf(int indice) {
			switch (indice) {
			case 1:
				return DRIVING_FASTER;
			case 2:
				return OVER_SPPED;
			case 3:
				return DISCON_GPS;
			case 4:
				return RECON_GPS;
			case 5:
				return OUT_GEO_FENCE;
			case 6:
				return INTO_GEO_FENCE;
			case 8:
				return SHORTED_GPS;
			case 9:
				return DEEP_SLEEP_ON;
			case 10:
				return DEEP_SLEEP_OFF;
			case 13:
				return BKP_BATTERY;
			case 14:
				return BATTERY_DOWN;
			case 15:
				return SHOCKED;
			case 16:
				return COLLISION;
			case 18:
				return DEVIATE_ROUT;
			case 19:
				return ENTER_ROUT;
			default:
				throw new IllegalArgumentException("�ndice n�o definido");
			}
		}
	}

	private enum ST210REPORTS {

		STATUS("SA200STT"), EMERGENCY("SA200EMG"), EVENT("SA200EVT"), ALERT(
				"SA200ALT"), ALIVE("SA200ALV");

		private String header;

		private ST210REPORTS(String header) {
			this.header = header;
		}

		public String getHeader() {
			return this.header;
		}

		public List<ST210FIELDS> getProtocol() {

			if (this.equals(STATUS)) {
				return StatusProtocol;
			}

			if (this.equals(EMERGENCY)) {
				return EmergencyProtocol;
			}

			if (this.equals(EVENT)) {
				return EventProtocol;
			}

			if (this.equals(ALERT)) {
				return AlertProtocol;
			}

			if (this.equals(ALIVE)) {
				return AliveProtocol;
			}

			return null;
		}

		public Pattern getProtocolPattern() {

			if (this.equals(STATUS)) {
				return getPattern(StatusProtocol);
			}

			if (this.equals(EMERGENCY)) {
				return getPattern(EmergencyProtocol);
			}

			if (this.equals(EVENT)) {
				return getPattern(EventProtocol);
			}

			if (this.equals(ALERT)) {
				return getPattern(AlertProtocol);
			}

			if (this.equals(ALIVE)) {
				return getPattern(AliveProtocol);
			}

			return null;
		}

	}

	private static ST210REPORTS getReportType(String msg) {

		if (msg.startsWith(ST210REPORTS.STATUS.getHeader())) {
			return ST210REPORTS.STATUS;
		}

		if (msg.startsWith(ST210REPORTS.EMERGENCY.getHeader())) {
			return ST210REPORTS.EMERGENCY;
		}

		if (msg.startsWith(ST210REPORTS.EVENT.getHeader())) {
			return ST210REPORTS.EVENT;
		}

		if (msg.startsWith(ST210REPORTS.ALERT.getHeader())) {
			return ST210REPORTS.ALERT;
		}

		if (msg.startsWith(ST210REPORTS.ALIVE.getHeader())) {
			return ST210REPORTS.ALIVE;
		}

		return null;
	}

	public static Pattern getPattern(List<ST210FIELDS> protocol) {

		String patternStr = "";

		for (ST210FIELDS field : protocol) {
			patternStr += field.getPattern();
		}

		return Pattern.compile(patternStr);

	}

	@SuppressWarnings("serial")
	static private List<ST210FIELDS> StatusProtocol = new LinkedList<ST210FIELDS>() {

		{
			add(ST210FIELDS.HDR_STATUS);
			add(ST210FIELDS.DEV_ID);
			add(ST210FIELDS.SW_VER);
			add(ST210FIELDS.DATE);
			add(ST210FIELDS.TIME);
			add(ST210FIELDS.CELL);
			add(ST210FIELDS.LAT);
			add(ST210FIELDS.LON);
			add(ST210FIELDS.SPD);
			add(ST210FIELDS.CRS);
			add(ST210FIELDS.SATT);
			add(ST210FIELDS.FIX);
			add(ST210FIELDS.DIST);
			add(ST210FIELDS.PWR_VOLT);
			add(ST210FIELDS.IO);
			add(ST210FIELDS.MODE);
			add(ST210FIELDS.MSG_NUM);
		}

	};

	@SuppressWarnings("serial")
	static private List<ST210FIELDS> EmergencyProtocol = new LinkedList<ST210FIELDS>() {

		{
			add(ST210FIELDS.HDR_EMERGENCY);
			add(ST210FIELDS.DEV_ID);
			add(ST210FIELDS.SW_VER);
			add(ST210FIELDS.DATE);
			add(ST210FIELDS.TIME);
			add(ST210FIELDS.CELL);
			add(ST210FIELDS.LAT);
			add(ST210FIELDS.LON);
			add(ST210FIELDS.SPD);
			add(ST210FIELDS.CRS);
			add(ST210FIELDS.SATT);
			add(ST210FIELDS.FIX);
			add(ST210FIELDS.DIST);
			add(ST210FIELDS.PWR_VOLT);
			add(ST210FIELDS.IO);
			add(ST210FIELDS.EMG_ID);
		}

	};

	@SuppressWarnings("serial")
	static private List<ST210FIELDS> EventProtocol = new LinkedList<ST210FIELDS>() {

		{
			add(ST210FIELDS.HDR_EVENT);
			add(ST210FIELDS.DEV_ID);
			add(ST210FIELDS.SW_VER);
			add(ST210FIELDS.DATE);
			add(ST210FIELDS.TIME);
			add(ST210FIELDS.CELL);
			add(ST210FIELDS.LAT);
			add(ST210FIELDS.LON);
			add(ST210FIELDS.SPD);
			add(ST210FIELDS.CRS);
			add(ST210FIELDS.SATT);
			add(ST210FIELDS.FIX);
			add(ST210FIELDS.DIST);
			add(ST210FIELDS.PWR_VOLT);
			add(ST210FIELDS.IO);
			add(ST210FIELDS.EVT_ID);
		}

	};

	@SuppressWarnings("serial")
	static private List<ST210FIELDS> AlertProtocol = new LinkedList<ST210FIELDS>() {

		{
			add(ST210FIELDS.HDR_ALERT);
			add(ST210FIELDS.DEV_ID);
			add(ST210FIELDS.SW_VER);
			add(ST210FIELDS.DATE);
			add(ST210FIELDS.TIME);
			add(ST210FIELDS.CELL);
			add(ST210FIELDS.LAT);
			add(ST210FIELDS.LON);
			add(ST210FIELDS.SPD);
			add(ST210FIELDS.CRS);
			add(ST210FIELDS.SATT);
			add(ST210FIELDS.FIX);
			add(ST210FIELDS.DIST);
			add(ST210FIELDS.PWR_VOLT);
			add(ST210FIELDS.IO);
			add(ST210FIELDS.ALERT_ID);
		}

	};

	@SuppressWarnings("serial")
	static private List<ST210FIELDS> AliveProtocol = new LinkedList<ST210FIELDS>() {

		{
			add(ST210FIELDS.HDR_ALIVE);
			add(ST210FIELDS.DEV_ID);
		}

	};

	@Override
	public Object decode(ChannelHandlerContext ctx, Channel channel, Object msg)
			throws Exception {
		if (channel != null) {
			channel.write("OI AMIGO do decoder");
		}
		String sentence = (String) msg;
		return decodeMsg(sentence);
	}

	public Position decodeMsg(String msg) throws Exception {

		ST210REPORTS report = getReportType(msg);

		List<ST210FIELDS> protocol = report.getProtocol();

		Pattern protocolPattern = report.getProtocolPattern();

		Log.info("Protocol Pattern: " + protocolPattern.toString());
		Log.info("Msg: " + msg);

		// Parse message
		Matcher parser = protocolPattern.matcher(msg);
		if (!parser.matches()) {
			return null;
		}

		// Create new position
		Position position = new Position();

		position.setAltitude(0D);
		position.setExtendedInfo("");
		position.setValid(true);

		Integer index = 0;
		for (ST210FIELDS field : protocol) {

			String groupValue = parser.group(index++);

			field.populatePosition(position, groupValue, getDataManager());
		}

		return position;
	}
}