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 {
    

    public ST210ProtocolDecoder(DataManager dataManager) {
        super(dataManager);
    }

    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("(\\w+);","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+.\\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 MODE:
                //position.setMode(Integer.parseInt(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))-1));
                time.set(Calendar.DAY_OF_MONTH, Integer.valueOf(Integer.valueOf(groupValue.substring(6, 8))));
                
                /*Calendar ret = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
                
                ret.setTimeInMillis(time.getTimeInMillis() + 
                                    TimeZone.getTimeZone("UTC").getOffset(time.getTimeInMillis()) -
                					TimeZone.getDefault().getOffset(time.getTimeInMillis()));*/

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

            case TIME: {

                Calendar time = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
                time.clear();
                time.setTime(position.getTime());
                
                time.set(Calendar.HOUR_OF_DAY, 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))));

                /*Calendar ret = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
                
                ret.setTimeInMillis(time.getTimeInMillis() + 
                                    TimeZone.getTimeZone("UTC").getOffset(time.getTimeInMillis()) -
                					TimeZone.getDefault().getOffset(time.getTimeInMillis()));*/

                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("Index not defined");
            }
        }
    }

    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("Index not defined");
            }
        }
    }

    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("Index not defined");
            }
        }
    }

    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("Index not defined");
            }
        }
    }

    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("Index not defined");
            }
        }
    }

    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();
        }

        if(patternStr.endsWith(";")){
            patternStr = patternStr.substring(0, patternStr.length()-1);
        }
        
        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) {
        String sentence = (String) msg;
        Log.info("Msg: " + msg);
        
        Position position = null;
        
        try{
            position = decodeMsg(sentence);
            Log.info("MESSAGE DECODED WITH SUCCESS!");
        }
        catch(Exception e){
            Log.severe("ERROR WHILE DECODING MESSAGE: " + e.getMessage());
        }
        
        return position;
    }

    public Position decodeMsg(String msg) throws Exception {

        ST210REPORTS report = getReportType(msg);

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

        Pattern protocolPattern = report.getProtocolPattern();

        // Parse message
        Matcher parser = protocolPattern.matcher(msg);
        if (!parser.matches()) {
            throw new Exception("Pattern no match: " + protocolPattern.toString());
        }

        if(report.equals(ST210REPORTS.ALIVE)){
            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;
    }

}