package com.radio.codec2talkie.protocol.aprs;

import com.radio.codec2talkie.protocol.aprs.tools.AprsTools;
import com.radio.codec2talkie.protocol.message.TextMessage;
import com.radio.codec2talkie.protocol.position.Position;
import com.radio.codec2talkie.tools.TextTools;
import com.radio.codec2talkie.tools.UnitTools;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public class AprsDataPositionReportMicE implements AprsData {

    private String _dstCallsign;
    private Position _position;
    private byte[] _binary;
    private boolean _isValid;

    private final Map<String, Integer> _miceMessageTypeMap = new HashMap<String, Integer>() {{
        // standard
        put("off_duty", 0b111);
        put("en_route", 0b110);
        put("in_service", 0b101);
        put("returning", 0b100);
        put("committed", 0b011);
        put("special", 0b010);
        put("priority", 0b001);
        // custom
        put("custom_0", 0b111);
        put("custom_1", 0b110);
        put("custom_2", 0b101);
        put("custom_3", 0b100);
        put("custom_4", 0b011);
        put("custom_5", 0b010);
        put("custom_6", 0b001);
        // emergency
        put("emergency", 0b000);
    }};

    private final Map<Integer, String> _miceMessageReverseTypeMapStd = new HashMap<Integer, String>() {{
        put(0b000, "emergency");
        put(0b111, "off_duty");
        put(0b110, "en_route");
        put(0b101, "in_service");
        put(0b100, "returning");
        put(0b011, "committed");
        put(0b010, "special");
        put(0b001, "priority");
    }};

    private final Map<Integer, String> _miceMessageReverseTypeMapCustom = new HashMap<Integer, String>() {{
        put(0b000, "emergency");
        put(0b111, "custom_0");
        put(0b110, "custom_1");
        put(0b101, "custom_2");
        put(0b100, "custom_3");
        put(0b011, "custom_4");
        put(0b010, "custom_5");
        put(0b001, "custom_6");
    }};

    @Override
    public boolean isPositionReport() {
        return true;
    }

    @Override
    public boolean isTextMessage() {
        return false;
    }

    @Override
    public void fromPosition(Position position) {
        _isValid = false;
        _position = position;

        // latitude into the callsign
        _dstCallsign = generateDestinationCallsign(position);
        position.dstCallsign = _dstCallsign;

        ByteBuffer buffer = ByteBuffer.allocate(256);

        // identifier
        buffer.put((byte)'`');

        // longitude, speed/course into the information field
        buffer.put(generateInfo(position));

        // symbol code + symbol table id
        byte[] symbol = position.symbolCode.getBytes();
        buffer.put(symbol[1]);
        buffer.put(symbol[0]);

        // encode altitude if enabled
        if (position.isAltitudeEnabled && position.hasAltitude) {
            int datumAltitude = (int) (10000 + position.altitudeMeters);
            int a1 = 33 + (datumAltitude / (91 * 91));
            buffer.put((byte)a1);
            int a2 = 33 + ((datumAltitude % (91 * 91)) / 91);
            buffer.put((byte)a2);
            int a3 = 33 + ((datumAltitude % (91 * 91)) % 91);
            buffer.put((byte)a3);
            buffer.put((byte)'}');
        }

        // comment
        buffer.put(position.comment.getBytes());

        // return
        buffer.flip();
        _binary = new byte[buffer.remaining()];
        buffer.get(_binary);
        _isValid = true;
    }

    @Override
    public void fromTextMessage(TextMessage textMessage) {
        _isValid = false;
    }

    @Override
    public Position toPosition() {
        return _position;
    }

    @Override
    public TextMessage toTextMessage() {
        return null;
    }

    @Override
    public void fromBinary(String srcCallsign, String dstCallsign, String digipath, byte[] infoData) {
        _isValid = false;
        _position = new Position();
        _dstCallsign = dstCallsign;
        _position.srcCallsign = srcCallsign;
        _position.dstCallsign = dstCallsign;
        _position.digipath = digipath;
        _position.privacyLevel = 0;
        _position.status = "";
        _position.comment = "";

        if (srcCallsign == null || dstCallsign == null) return;
        if (dstCallsign.length() < 6 || infoData.length < 8) return;

        // read latitude
        boolean isCustom = false;
        char ns = 'S';
        char we = 'E';
        int longOffset = 0;
        int messageId = 0;
        byte[] dstCallsignBuf = dstCallsign.getBytes();
        StringBuilder latitude = new StringBuilder();

        for (int i = 0; i < 6; i++) {
            char c = (char) dstCallsignBuf[i];
            if (c >= 'A' && c <= 'L') {
                if (i < 3) {
                    isCustom = true;
                    if (c != 'L') messageId |= 1;
                }
                if (c == 'K' || c == 'L') {
                    // NOTE, using 0 instead of ' ' for position ambiguity
                    c = '0';
                    _position.privacyLevel += 1;
                } else
                    c = (char) (c - 17);
            } else if (c >= 'P' && c <= 'Z') {
                if (i < 3) {
                    messageId |= 1;
                } else if (i == 3) {
                    ns = 'N';
                } else if (i == 4) {
                    longOffset = 100;
                } else {
                    we = 'W';
                }
                if (c == 'Z') {
                    // NOTE, using 0 instead of ' ' for position ambiguity
                    c = '0';
                    _position.privacyLevel += 1;
                } else
                    c = (char) (c - 32);
            }
            if (i < 2) messageId <<= 1;
            if (i == 4) latitude.append('.');
            latitude.append(c);
        }

        _position.latitude = UnitTools.nmeaToDecimal(latitude.toString(), Character.toString(ns));
        _position.status = isCustom
                ? _miceMessageReverseTypeMapCustom.get(messageId)
                : _miceMessageReverseTypeMapStd.get(messageId);

        // read longitude
        int d = ((int)infoData[0] - 28) + longOffset;
        if (d >= 180 && d <= 189) d -= 80;
        else if (d >= 190) d -= 190;

        int m = ((int)infoData[1] - 28);
        if (m >= 60) m -= 60;

        int h = ((int)infoData[2] - 28);

        String longitude = String.format(Locale.US, "%03d%02d.%02d", d, m, h);
        _position.longitude = UnitTools.nmeaToDecimal(longitude, Character.toString(we));

        // read course/speed
        int sp = 10 * ((int)infoData[3] - 28);
        int dcSp = ((int)infoData[4] - 28) / 10;
        int dcSe = (((int)infoData[4] - 28) % 10) * 100;
        int se = (int)infoData[5] - 28;

        int speed = sp + dcSp;
        if (speed >= 800) speed -= 800;

        int course = dcSe + se;
        if (course >= 400) course -= 400;

        _position.hasBearing = true;
        _position.bearingDegrees = course;

        _position.hasSpeed = true;
        _position.speedMetersPerSecond = UnitTools.knotsToMetersPerSecond(speed);

        // read symbol table + symbol code
        _position.symbolCode = String.format(Locale.US, "%c%c", infoData[7], infoData[6]);

        if (infoData.length > 11 && infoData[11] == '}') {
            _position.hasAltitude = true;
            _position.altitudeMeters = ((infoData[8] - 33) * 91 * 91 + (infoData[9] - 33) * 91 + (infoData[10] - 33)) - 10000;
            _position.comment = TextTools.stripNulls(new String(Arrays.copyOfRange(infoData, 12, infoData.length), StandardCharsets.UTF_8));
        } else {
            _position.comment = TextTools.stripNulls(new String(Arrays.copyOfRange(infoData, 8, infoData.length), StandardCharsets.UTF_8));
        }

        _position.maidenHead = UnitTools.decimalToMaidenhead(_position.latitude, _position.longitude);
        _isValid = true;
    }

    @Override
    public byte[] toBinary() {
        return _binary;
    }

    @Override
    public boolean isValid() {
        return _isValid;
    }

    private String generateDestinationCallsign(Position position) {

        byte[] latitude = AprsTools.applyPrivacyOnUncompressedNmeaCoordinate(
                UnitTools.decimalToNmea(position.latitude, true),
                position.privacyLevel).getBytes();
        byte[] longitude = UnitTools.decimalToNmea(position.longitude, false).getBytes();

        // get Mic-E status bit mapping
        boolean isCustom = position.status.startsWith("custom");
        Integer miceMessageTypeEncoding = _miceMessageTypeMap.get(position.status);
        miceMessageTypeEncoding = miceMessageTypeEncoding == null
                ? 0b111
                : miceMessageTypeEncoding;

        // decide if Mic-E "longitude offset" must be added
        byte lonDeg = (byte)(Integer.parseInt(new String(longitude).substring(0, 3)));
        int longOffset = (lonDeg >= 10 && lonDeg < 100) ? 0 : 1;

        // generate Mic-E position and flags into the destination callsign, add N, W indicators
        ByteBuffer buffer = ByteBuffer.allocate(16);
        // 0, mic-e status
        buffer.put((byte)(latitude[0] + (isCustom ? 17 : 32) * ((miceMessageTypeEncoding >> 2) & 1)));
        // 1, mic-e status
        buffer.put((byte)(latitude[1] + (isCustom ? 17 : 32) * ((miceMessageTypeEncoding >> 1) & 1)));
        // 2, mic-e status
        if (latitude[2] == (byte)' ')
            buffer.put((miceMessageTypeEncoding & 1) == 1 ? isCustom ? (byte)'K' : (byte)'Z' : (byte)'L');
        else
            buffer.put((byte)(latitude[2] + (isCustom ? 17 : 32) * ((miceMessageTypeEncoding) & 1)));
        // 3, north/south
        if (latitude[3] == (byte)' ')
            buffer.put(latitude[6] == 'N' ? (byte)'Z' : (byte)'L');
        else
            buffer.put((byte)(latitude[3] + 32 * (latitude[6] == 'N' ? 1 : 0)));
        // 4, long offset
        if (latitude[4] == (byte)' ')
            buffer.put((longOffset & 1) == 1 ? (byte)'Z' : (byte)'L');
        else
            buffer.put((byte)(latitude[4] + 32 * (longOffset & 1)));
        // 5, west/east
        if (latitude[5] == (byte)' ')
            buffer.put(longitude[7] == 'W' ? (byte)'Z' : (byte)'L');
        else
            buffer.put((byte) (latitude[5] + 32 * (longitude[7] == 'W' ? 1 : 0)));

        // include E-Mic digipath if specified (!= 0)
        if (position.extDigipathSsid > 0) {
            buffer.put((byte)'-');
            buffer.put(Integer.toString(position.extDigipathSsid).getBytes());
        }

        // return
        buffer.flip();
        byte[] callsign = new byte[buffer.remaining()];
        buffer.get(callsign);
        return new String(callsign);
    }

    private byte[] generateInfo(Position position) {
        byte[] longitude = UnitTools.decimalToNmea(position.longitude, false).getBytes();

        ByteBuffer buffer = ByteBuffer.allocate(16);
        String longStr = new String(longitude);

        // encode Mic-E longitude into the information field
        byte lonDeg = (byte)(Integer.parseInt(longStr.substring(0, 3)));
        if (lonDeg >= 0 && lonDeg <= 9) lonDeg += (90 + 28);
        else if (lonDeg >= 10 && lonDeg <= 99) lonDeg += 28;
        else if (lonDeg >= 100 && lonDeg <= 109) lonDeg += 8;
        else lonDeg -= (44 + 28);
        buffer.put(lonDeg);

        byte lonMin = (byte)(Integer.parseInt(longStr.substring(3, 5)));
        if (lonMin >= 0 && lonMin <= 9) lonMin += (60 + 28);
        else lonMin += 28;
        buffer.put(lonMin);

        byte lonMinHun = (byte)(Integer.parseInt(longStr.substring(5, 7)));
        lonMinHun += 28;
        buffer.put(lonMinHun);

        // encode speed/course
        long speed = UnitTools.metersPerSecondToKnots(position.speedMetersPerSecond);
        byte speedHun = (byte)((speed / 10) + 28);
        buffer.put(speedHun);

        byte speedTen = (byte)(speed % 10);
        byte courseHun = (byte)(position.bearingDegrees / 100.0);
        buffer.put((byte)(speedTen * 10 + courseHun + 28));

        // encode bearing
        buffer.put((byte)(position.bearingDegrees % 100.0 + 28));

        // return
        buffer.flip();
        byte[] info = new byte[buffer.remaining()];
        buffer.get(info);
        return info;
    }
}
