/*
 * Copyright 2013 - 2022 Anton Tananaev (anton@traccar.org)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.traccar.protocol;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import org.traccar.BaseProtocolDecoder;
import org.traccar.config.Keys;
import org.traccar.helper.BufferUtil;
import org.traccar.helper.model.AttributeUtil;
import org.traccar.session.DeviceSession;
import org.traccar.Protocol;
import org.traccar.helper.BitUtil;
import org.traccar.helper.DateBuilder;
import org.traccar.helper.UnitsConverter;
import org.traccar.model.CellTower;
import org.traccar.model.Network;
import org.traccar.model.Position;

import java.net.SocketAddress;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.TimeZone;
import java.util.stream.Collectors;

public class SuntechProtocolDecoder extends BaseProtocolDecoder {

    private boolean universal;
    private String prefix;

    private int protocolType;
    private boolean hbm;
    private boolean includeAdc;
    private boolean includeRpm;
    private boolean includeTemp;

    private ByteBuf crash;

    public SuntechProtocolDecoder(Protocol protocol) {
        super(protocol);
    }

    public boolean getUniversal() {
        return universal;
    }

    public String getPrefix() {
        return prefix;
    }

    public void setProtocolType(int protocolType) {
        this.protocolType = protocolType;
    }

    public int getProtocolType(long deviceId) {
        Integer value = AttributeUtil.lookup(getCacheManager(), Keys.PROTOCOL_TYPE, deviceId);
        return value != null ? value : protocolType;
    }

    public void setHbm(boolean hbm) {
        this.hbm = hbm;
    }

    public boolean isHbm(long deviceId) {
        Boolean value = AttributeUtil.lookup(getCacheManager(), Keys.PROTOCOL_HBM, deviceId);
        return value != null ? value : hbm;
    }

    public void setIncludeAdc(boolean includeAdc) {
        this.includeAdc = includeAdc;
    }

    public boolean isIncludeAdc(long deviceId) {
        Boolean value = AttributeUtil.lookup(
                getCacheManager(), Keys.PROTOCOL_INCLUDE_ADC.withPrefix(getProtocolName()), deviceId);
        return value != null ? value : includeAdc;
    }

    public void setIncludeRpm(boolean includeRpm) {
        this.includeRpm = includeRpm;
    }

    public boolean isIncludeRpm(long deviceId) {
        Boolean value = AttributeUtil.lookup(
                getCacheManager(), Keys.PROTOCOL_INCLUDE_RPM.withPrefix(getProtocolName()), deviceId);
        return value != null ? value : includeRpm;
    }

    public void setIncludeTemp(boolean includeTemp) {
        this.includeTemp = includeTemp;
    }

    public boolean isIncludeTemp(long deviceId) {
        Boolean value = AttributeUtil.lookup(
                getCacheManager(), Keys.PROTOCOL_INCLUDE_TEMPERATURE.withPrefix(getProtocolName()), deviceId);
        return value != null ? value : includeTemp;
    }

    private Position decode9(
            Channel channel, SocketAddress remoteAddress, String[] values) throws ParseException {
        int index = 1;

        String type = values[index++];

        if (!type.equals("Location") && !type.equals("Emergency") && !type.equals("Alert")) {
            return null;
        }

        DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, values[index++]);
        if (deviceSession == null) {
            return null;
        }

        Position position = new Position(getProtocolName());
        position.setDeviceId(deviceSession.getDeviceId());

        if (type.equals("Emergency") || type.equals("Alert")) {
            position.set(Position.KEY_ALARM, Position.ALARM_GENERAL);
        }

        if (!type.equals("Alert") || getProtocolType(deviceSession.getDeviceId()) == 0) {
            position.set(Position.KEY_VERSION_FW, values[index++]);
        }

        DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHH:mm:ss");
        dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
        position.setTime(dateFormat.parse(values[index++] + values[index++]));

        if (getProtocolType(deviceSession.getDeviceId()) == 1) {
            index += 1; // cell
        }

        position.setLatitude(Double.parseDouble(values[index++]));
        position.setLongitude(Double.parseDouble(values[index++]));
        position.setSpeed(UnitsConverter.knotsFromKph(Double.parseDouble(values[index++])));
        position.setCourse(Double.parseDouble(values[index++]));

        position.setValid(values[index++].equals("1"));

        if (getProtocolType(deviceSession.getDeviceId()) == 1) {
            position.set(Position.KEY_ODOMETER, Integer.parseInt(values[index++]));
        }

        return position;
    }

    private String decodeEmergency(int value) {
        switch (value) {
            case 1:
                return Position.ALARM_SOS;
            case 2:
                return Position.ALARM_PARKING;
            case 3:
                return Position.ALARM_POWER_CUT;
            case 5:
            case 6:
                return Position.ALARM_DOOR;
            case 7:
                return Position.ALARM_MOVEMENT;
            case 8:
                return Position.ALARM_VIBRATION;
            default:
                return null;
        }
    }

    private String decodeAlert(int value) {
        switch (value) {
            case 1:
                return Position.ALARM_OVERSPEED;
            case 5:
                return Position.ALARM_GEOFENCE_EXIT;
            case 6:
                return Position.ALARM_GEOFENCE_ENTER;
            case 14:
                return Position.ALARM_LOW_BATTERY;
            case 15:
                return Position.ALARM_VIBRATION;
            case 16:
                return Position.ALARM_ACCIDENT;
            case 40:
                return Position.ALARM_POWER_RESTORED;
            case 41:
                return Position.ALARM_POWER_CUT;
            case 42:
                return Position.ALARM_SOS;
            case 46:
                return Position.ALARM_ACCELERATION;
            case 47:
                return Position.ALARM_BRAKING;
            case 50:
                return Position.ALARM_JAMMING;
            case 132:
                return Position.ALARM_DOOR;
            default:
                return null;
        }
    }
    private Position decode4(
            Channel channel, SocketAddress remoteAddress, String[] values) throws ParseException {
        int index = 0;

        String type = values[index++].substring(5);

        if (!type.equals("STT") && !type.equals("ALT")) {
            return null;
        }

        DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, values[index++]);
        if (deviceSession == null) {
            return null;
        }

        Position position = new Position(getProtocolName());
        position.setDeviceId(deviceSession.getDeviceId());
        position.set(Position.KEY_TYPE, type);

        position.set(Position.KEY_VERSION_FW, values[index++]);
        index += 1; // model

        Network network = new Network();

        for (int i = 0; i < 7; i++) {
            int cid = Integer.parseInt(values[index++]);
            int mcc = Integer.parseInt(values[index++]);
            int mnc = Integer.parseInt(values[index++]);
            int lac, rssi;
            if (i == 0) {
                rssi = Integer.parseInt(values[index++]);
                lac = Integer.parseInt(values[index++]);
            } else {
                lac = Integer.parseInt(values[index++]);
                rssi = Integer.parseInt(values[index++]);
            }
            index += 1; // timing advance
            if (cid > 0) {
                network.addCellTower(CellTower.from(mcc, mnc, lac, cid, rssi));
            }
        }

        position.setNetwork(network);

        position.set(Position.KEY_BATTERY, Double.parseDouble(values[index++]));
        position.set(Position.KEY_ARCHIVE, values[index++].equals("0") ? true : null);
        position.set(Position.KEY_INDEX, Integer.parseInt(values[index++]));
        position.set(Position.KEY_STATUS, Integer.parseInt(values[index++]));

        if (values[index].length() == 3) {
            index += 1; // collaborative network
        }

        if (values[index].isEmpty()) {

            getLastLocation(position, null);

        } else {

            DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHH:mm:ss");
            dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
            position.setTime(dateFormat.parse(values[index++] + values[index++]));

            position.setLatitude(Double.parseDouble(values[index++]));
            position.setLongitude(Double.parseDouble(values[index++]));
            position.setSpeed(UnitsConverter.knotsFromKph(Double.parseDouble(values[index++])));
            position.setCourse(Double.parseDouble(values[index++]));

            position.set(Position.KEY_SATELLITES, Integer.parseInt(values[index++]));

            position.setValid(values[index++].equals("1"));

        }

        return position;
    }

    private Position decode2356(
            Channel channel, SocketAddress remoteAddress, String protocol, String[] values) throws ParseException {
        int index = 0;

        String type = values[index++].substring(5);

        if (!type.equals("STT") && !type.equals("EMG") && !type.equals("EVT")
                && !type.equals("ALT") && !type.equals("UEX")) {
            return null;
        }

        DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, values[index++]);
        if (deviceSession == null) {
            return null;
        }

        Position position = new Position(getProtocolName());
        position.setDeviceId(deviceSession.getDeviceId());
        position.set(Position.KEY_TYPE, type);

        if (protocol.startsWith("ST3") || protocol.equals("ST500") || protocol.equals("ST600")) {
            index += 1; // model
        }

        position.set(Position.KEY_VERSION_FW, values[index++]);

        DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHH:mm:ss");
        dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
        position.setTime(dateFormat.parse(values[index++] + values[index++]));

        if (!protocol.equals("ST500")) {
            long cid = Long.parseLong(values[index++], 16);
            if (protocol.equals("ST600")) {
                position.setNetwork(new Network(CellTower.from(
                        Integer.parseInt(values[index++]), Integer.parseInt(values[index++]),
                        Integer.parseInt(values[index++], 16), cid, Integer.parseInt(values[index++]))));
            }
        }

        position.setLatitude(Double.parseDouble(values[index++]));
        position.setLongitude(Double.parseDouble(values[index++]));
        position.setSpeed(UnitsConverter.knotsFromKph(Double.parseDouble(values[index++])));
        position.setCourse(Double.parseDouble(values[index++]));

        position.set(Position.KEY_SATELLITES, Integer.parseInt(values[index++]));

        position.setValid(values[index++].equals("1"));

        position.set(Position.KEY_ODOMETER, Integer.parseInt(values[index++]));
        position.set(Position.KEY_POWER, Double.parseDouble(values[index++]));

        String io = values[index++];
        if (io.length() >= 6) {
            position.set(Position.KEY_IGNITION, io.charAt(0) == '1');
            position.set(Position.PREFIX_IN + 1, io.charAt(1) == '1');
            position.set(Position.PREFIX_IN + 2, io.charAt(2) == '1');
            position.set(Position.PREFIX_IN + 3, io.charAt(3) == '1');
            position.set(Position.PREFIX_OUT + 1, io.charAt(4) == '1');
            position.set(Position.PREFIX_OUT + 2, io.charAt(5) == '1');
        }

        switch (type) {
            case "STT":
                position.set(Position.KEY_STATUS, Integer.parseInt(values[index++]));
                position.set(Position.KEY_INDEX, Integer.parseInt(values[index++]));
                break;
            case "EMG":
                position.set(Position.KEY_ALARM, decodeEmergency(Integer.parseInt(values[index++])));
                break;
            case "EVT":
                position.set(Position.KEY_EVENT, Integer.parseInt(values[index++]));
                break;
            case "ALT":
                position.set(Position.KEY_ALARM, decodeAlert(Integer.parseInt(values[index++])));
                break;
            case "UEX":
                int remaining = Integer.parseInt(values[index++]);
                double totalFuel = 0;
                while (remaining > 0) {
                    String attribute = values[index++];
                    if (attribute.startsWith("CabAVL")) {
                        String[] data = attribute.split(",");
                        double fuel1 = Double.parseDouble(data[2]);
                        if (fuel1 > 0) {
                            totalFuel += fuel1;
                            position.set("fuel1", fuel1);
                        }
                        double fuel2 = Double.parseDouble(data[3]);
                        if (fuel2 > 0) {
                            totalFuel += fuel2;
                            position.set("fuel2", fuel2);
                        }
                    } else if (attribute.startsWith("GTSL")) {
                        position.set(Position.KEY_DRIVER_UNIQUE_ID, attribute.split("\\|")[4]);
                    } else if (attribute.contains("=")) {
                        String[] pair = attribute.split("=");
                        if (pair.length >= 2) {
                            String value = pair[1].trim();
                            if (value.contains(".")) {
                                value = value.substring(0, value.indexOf('.'));
                            }
                            switch (pair[0].charAt(0)) {
                                case 't':
                                    position.set(Position.PREFIX_TEMP + pair[0].charAt(2), Integer.parseInt(value, 16));
                                    break;
                                case 'N':
                                    int fuel = Integer.parseInt(value, 16);
                                    totalFuel += fuel;
                                    position.set("fuel" + pair[0].charAt(2), fuel);
                                    break;
                                case 'Q':
                                    position.set("drivingQuality", Integer.parseInt(value, 16));
                                    break;
                                default:
                                    break;
                            }
                        }
                    } else {
                        position.set("serial", attribute.trim());
                    }
                    remaining -= attribute.length() + 1;
                }
                if (totalFuel > 0) {
                    position.set(Position.KEY_FUEL_LEVEL, totalFuel);
                }
                index += 1; // checksum
                break;
            default:
                break;
        }

        if (isHbm(deviceSession.getDeviceId())) {

            if (index < values.length) {
                position.set(Position.KEY_HOURS, UnitsConverter.msFromMinutes(Integer.parseInt(values[index++])));
            }

            if (index < values.length) {
                position.set(Position.KEY_BATTERY, Double.parseDouble(values[index++]));
            }

            if (index < values.length && values[index++].equals("0")) {
                position.set(Position.KEY_ARCHIVE, true);
            }

            if (isIncludeAdc(deviceSession.getDeviceId())) {
                for (int i = 1; i <= 3; i++) {
                    if (index < values.length && !values[index++].isEmpty()) {
                        position.set(Position.PREFIX_ADC + i, Double.parseDouble(values[index - 1]));
                    }
                }
            }

            if (isIncludeRpm(deviceSession.getDeviceId()) && index < values.length) {
                position.set(Position.KEY_RPM, Integer.parseInt(values[index++]));
            }

            if (values.length - index >= 2) {
                String driverUniqueId = values[index++];
                if (values[index++].equals("1") && !driverUniqueId.isEmpty()) {
                    position.set(Position.KEY_DRIVER_UNIQUE_ID, driverUniqueId);
                }
            }

            if (isIncludeTemp(deviceSession.getDeviceId())) {
                for (int i = 1; i <= 3; i++) {
                    String temperature = values[index++];
                    String value = temperature.substring(temperature.indexOf(':') + 1);
                    if (!value.isEmpty()) {
                        position.set(Position.PREFIX_TEMP + i, Double.parseDouble(value));
                    }
                }

            }

        }

        return position;
    }

    private Position decodeUniversal(
            Channel channel, SocketAddress remoteAddress, String[] values) throws ParseException {
        int index = 0;

        String type = values[index++];

        if (!type.equals("STT") && !type.equals("ALT") && !type.equals("BLE") && !type.equals("RES")) {
            return null;
        }

        DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, values[index++]);
        if (deviceSession == null) {
            return null;
        }

        Position position = new Position(getProtocolName());
        position.setDeviceId(deviceSession.getDeviceId());
        position.set(Position.KEY_TYPE, type);

        if (type.equals("RES")) {
            getLastLocation(position, null);
            position.set(
                    Position.KEY_RESULT,
                    Arrays.stream(values, index, values.length).collect(Collectors.joining(";")));
            return position;
        }

        int mask;
        if (type.equals("BLE")) {
            mask = 0b1100000110110;
        } else {
            mask = Integer.parseInt(values[index++], 16);
        }

        if (BitUtil.check(mask, 1)) {
            index += 1; // model
        }

        if (BitUtil.check(mask, 2)) {
            position.set(Position.KEY_VERSION_FW, values[index++]);
        }

        if (BitUtil.check(mask, 3) && values[index++].equals("0")) {
            position.set(Position.KEY_ARCHIVE, true);
        }

        if (BitUtil.check(mask, 4) && BitUtil.check(mask, 5)) {
            DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHH:mm:ss");
            dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
            position.setTime(dateFormat.parse(values[index++] + values[index++]));
        }

        CellTower cellTower = new CellTower();
        if (BitUtil.check(mask, 6)) {
            cellTower.setCellId(Long.parseLong(values[index++], 16));
        }
        if (BitUtil.check(mask, 7)) {
            cellTower.setMobileCountryCode(Integer.parseInt(values[index++]));
        }
        if (BitUtil.check(mask, 8)) {
            cellTower.setMobileNetworkCode(Integer.parseInt(values[index++]));
        }
        if (BitUtil.check(mask, 9)) {
            cellTower.setLocationAreaCode(Integer.parseInt(values[index++], 16));
        }
        if (cellTower.getCellId() != null) {
            position.setNetwork(new Network(cellTower));
        }

        if (BitUtil.check(mask, 10)) {
            position.set(Position.KEY_RSSI, Integer.parseInt(values[index++]));
        }

        if (BitUtil.check(mask, 11)) {
            position.setLatitude(Double.parseDouble(values[index++]));
        }

        if (BitUtil.check(mask, 12)) {
            position.setLongitude(Double.parseDouble(values[index++]));
        }

        if (type.equals("BLE")) {

            position.setValid(true);

            int count = Integer.parseInt(values[index++]);

            for (int i = 1; i <= count; i++) {
                position.set("tag" + i + "Rssi", Integer.parseInt(values[index++]));
                index += 1; // rssi min
                index += 1; // rssi max
                position.set("tag" + i + "Id", values[index++]);
                position.set("tag" + i + "Samples", Integer.parseInt(values[index++]));
                position.set("tag" + i + "Major", Integer.parseInt(values[index++]));
                position.set("tag" + i + "Minor", Integer.parseInt(values[index++]));
            }

        } else {

            if (BitUtil.check(mask, 13)) {
                position.setSpeed(UnitsConverter.knotsFromKph(Double.parseDouble(values[index++])));
            }

            if (BitUtil.check(mask, 14)) {
                position.setCourse(Double.parseDouble(values[index++]));
            }

            if (BitUtil.check(mask, 15)) {
                position.set(Position.KEY_SATELLITES, Integer.parseInt(values[index++]));
            }

            if (BitUtil.check(mask, 16)) {
                position.setValid(values[index++].equals("1"));
            }

            if (BitUtil.check(mask, 17)) {
                int input = Integer.parseInt(values[index++]);
                position.set(Position.KEY_IGNITION, BitUtil.check(input, 0));
                position.set(Position.KEY_INPUT, input);
            }

            if (BitUtil.check(mask, 18)) {
                position.set(Position.KEY_OUTPUT, Integer.parseInt(values[index++]));
            }

            if (type.equals("ALT")) {
                if (BitUtil.check(mask, 19)) {
                    int alertId = Integer.parseInt(values[index++]);
                    position.set(Position.KEY_ALARM, decodeAlert(alertId));
                }
                if (BitUtil.check(mask, 20)) {
                    position.set("alertModifier", values[index++]);
                }
                if (BitUtil.check(mask, 21)) {
                    position.set("alertData", values[index++]);
                }
            } else {
                if (BitUtil.check(mask, 19)) {
                    position.set("mode", Integer.parseInt(values[index++]));
                }
                if (BitUtil.check(mask, 20)) {
                    position.set("reason", Integer.parseInt(values[index++]));
                }
                if (BitUtil.check(mask, 21)) {
                    position.set(Position.KEY_INDEX, Integer.parseInt(values[index++]));
                }
            }

            if (BitUtil.check(mask, 22)) {
                index += 1; // reserved
            }

            if (BitUtil.check(mask, 23)) {
                int assignMask = Integer.parseInt(values[index++], 16);
                for (int i = 0; i <= 30; i++) {
                    if (BitUtil.check(assignMask, i)) {
                        position.set(Position.PREFIX_IO + (i + 1), values[index++]);
                    }
                }
            }

        }

        return position;
    }

    private Position decodeBinary(
            Channel channel, SocketAddress remoteAddress, ByteBuf buf) {

        int type = buf.readUnsignedByte();
        buf.readUnsignedShort(); // length

        DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, ByteBufUtil.hexDump(buf.readSlice(5)));
        if (deviceSession == null) {
            return null;
        }

        Position position = new Position(getProtocolName());
        position.setDeviceId(deviceSession.getDeviceId());

        int mask = buf.readUnsignedMedium();

        if (BitUtil.check(mask, 1)) {
            buf.readUnsignedByte(); // model
        }

        if (BitUtil.check(mask, 2)) {
            position.set(Position.KEY_VERSION_FW, String.format("%d.%d.%d",
                    buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte()));
        }

        if (BitUtil.check(mask, 3) && buf.readUnsignedByte() == 0) {
            position.set(Position.KEY_ARCHIVE, true);
        }

        if (BitUtil.check(mask, 4) && BitUtil.check(mask, 5)) {
            position.setTime(new DateBuilder()
                    .setDate(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
                    .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
                    .getDate());
        }

        if (BitUtil.check(mask, 6)) {
            buf.readUnsignedInt(); // cell
        }

        if (BitUtil.check(mask, 7)) {
            buf.readUnsignedShort(); // mcc
        }

        if (BitUtil.check(mask, 8)) {
            buf.readUnsignedShort(); // mnc
        }

        if (BitUtil.check(mask, 9)) {
            buf.readUnsignedShort(); // lac
        }

        if (BitUtil.check(mask, 10)) {
            position.set(Position.KEY_RSSI, buf.readUnsignedByte());
        }

        if (BitUtil.check(mask, 11)) {
            position.setLatitude(BufferUtil.readSignedMagnitudeInt(buf) / 1000000.0);
        }

        if (BitUtil.check(mask, 12)) {
            position.setLongitude(BufferUtil.readSignedMagnitudeInt(buf) / 1000000.0);
        }

        if (BitUtil.check(mask, 13)) {
            position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedShort() / 100.0));
        }

        if (BitUtil.check(mask, 14)) {
            position.setCourse(buf.readUnsignedShort() / 100.0);
        }

        if (BitUtil.check(mask, 15)) {
            position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
        }

        if (BitUtil.check(mask, 16)) {
            position.setValid(buf.readUnsignedByte() > 0);
        }

        if (BitUtil.check(mask, 17)) {
            int input = buf.readUnsignedByte();
            position.set(Position.KEY_IGNITION, BitUtil.check(input, 0));
            position.set(Position.KEY_INPUT, input);
        }

        if (BitUtil.check(mask, 18)) {
            position.set(Position.KEY_OUTPUT, buf.readUnsignedByte());
        }

        int alertId = 0;
        if (BitUtil.check(mask, 19)) {
            alertId = buf.readUnsignedByte();
            if (type == 0x82) {
                position.set(Position.KEY_ALARM, decodeAlert(alertId));
            }
        }

        if (BitUtil.check(mask, 20)) {
            buf.readUnsignedShort(); // alert modifier
        }

        if (BitUtil.check(mask, 21) && alertId == 59) {
            position.set(Position.KEY_DRIVER_UNIQUE_ID, ByteBufUtil.hexDump(buf.readSlice(8)));
        }

        return position;
    }

    private Position decodeTravelReport(Channel channel, SocketAddress remoteAddress, String[] values) {
        int index = 1;

        DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, values[index++]);
        if (deviceSession == null) {
            return null;
        }

        Position position = new Position(getProtocolName());
        position.setDeviceId(deviceSession.getDeviceId());

        getLastLocation(position, null);

        position.set(Position.KEY_DRIVER_UNIQUE_ID, values[values.length - 1]);

        return position;
    }

    private Collection<Position> decodeCrashReport(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {

        if (buf.getByte(buf.readerIndex() + 3) != ';') {
            return null;
        }

        String[] values = buf.readCharSequence(23, StandardCharsets.US_ASCII).toString().split(";");

        DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, values[1]);
        if (deviceSession == null) {
            return null;
        }

        int currentIndex = Integer.parseInt(values[2]);
        int totalIndex = Integer.parseInt(values[3]);

        if (crash == null) {
            crash = Unpooled.buffer();
        }

        crash.writeBytes(buf.readSlice(buf.readableBytes() - 3));

        if (currentIndex == totalIndex) {

            LinkedList<Position> positions = new LinkedList<>();

            Date crashTime = new DateBuilder()
                    .setDate(crash.readUnsignedByte(), crash.readUnsignedByte(), crash.readUnsignedByte())
                    .setTime(crash.readUnsignedByte(), crash.readUnsignedByte(), crash.readUnsignedByte())
                    .getDate();

            List<Date> times = Arrays.asList(
                    new Date(crashTime.getTime() - 3000),
                    new Date(crashTime.getTime() - 2000),
                    new Date(crashTime.getTime() - 1000),
                    new Date(crashTime.getTime() + 1000));

            for (Date time : times) {

                Position position = new Position(getProtocolName());
                position.setDeviceId(deviceSession.getDeviceId());

                position.setValid(true);
                position.setTime(time);
                position.setLatitude(crash.readIntLE() * 0.0000001);
                position.setLongitude(crash.readIntLE() * 0.0000001);
                position.setSpeed(UnitsConverter.knotsFromKph(crash.readUnsignedShort() * 0.01));
                position.setCourse(crash.readUnsignedShort() * 0.01);

                StringBuilder value = new StringBuilder("[");
                for (int i = 0; i < 100; i++) {
                    if (value.length() > 1) {
                        value.append(",");
                    }
                    value.append("[");
                    value.append(crash.readShortLE());
                    value.append(",");
                    value.append(crash.readShortLE());
                    value.append(",");
                    value.append(crash.readShortLE());
                    value.append("]");
                }
                value.append("]");

                position.set(Position.KEY_G_SENSOR, value.toString());

                positions.add(position);

            }

            crash.release();
            crash = null;

            return positions;

        } else {

            return null;

        }

    }

    @Override
    protected Object decode(
            Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {

        ByteBuf buf = (ByteBuf) msg;

        if (buf.getByte(buf.readerIndex() + 1) == 0) {

            universal = true;
            return decodeBinary(channel, remoteAddress, buf);

        } else {

            String[] values = buf.toString(StandardCharsets.US_ASCII).split(";", -1);
            prefix = values[0];

            if (prefix.equals("CRR")) {
                return decodeCrashReport(channel, remoteAddress, buf);
            } else if (prefix.length() < 5) {
                universal = true;
                return decodeUniversal(channel, remoteAddress, values);
            } else if (prefix.endsWith("HTE")) {
                return decodeTravelReport(channel, remoteAddress, values);
            } else if (prefix.startsWith("ST9")) {
                return decode9(channel, remoteAddress, values);
            } else if (prefix.startsWith("ST4")) {
                return decode4(channel, remoteAddress, values);
            } else {
                return decode2356(channel, remoteAddress, prefix.substring(0, 5), values);
            }
        }
    }

}
