package com.lingniu.ingest.protocol.jt808.model;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public final class Jt808LocationAdditionalInfo {

    public static final String DEFAULT_PREFIX = "jt808.extra.";

    private static final String[] VEHICLE_SIGNAL_BITS = {
            "low_beam",
            "high_beam",
            "right_turn",
            "left_turn",
            "brake",
            "reverse",
            "fog_lamp",
            "position_lamp",
            "horn",
            "air_conditioner",
            "neutral",
            "retarder",
            "abs",
            "heater",
            "clutch"
    };

    private static final String[] IO_STATUS_BITS = {
            "deep_sleep",
            "sleep"
    };

    private Jt808LocationAdditionalInfo() {
    }

    public static Map<String, String> decode(Map<Integer, byte[]> extensionItems) {
        return decode(extensionItems, DEFAULT_PREFIX);
    }

    public static Map<String, String> decode(Map<Integer, byte[]> extensionItems, String prefix) {
        if (extensionItems == null || extensionItems.isEmpty()) {
            return Map.of();
        }
        String p = prefix == null ? "" : prefix;
        Map<String, String> out = new LinkedHashMap<>();
        extensionItems.forEach((id, bytes) -> decodeItem(out, p, id == null ? -1 : id, bytes));
        return Map.copyOf(out);
    }

    public static Double mileageKm(Map<Integer, byte[]> extensionItems) {
        if (extensionItems == null) {
            return null;
        }
        byte[] bytes = extensionItems.get(0x01);
        if (bytes == null || bytes.length != 4) {
            return null;
        }
        return u32(bytes, 0) * 0.1;
    }

    private static void decodeItem(Map<String, String> out, String prefix, int id, byte[] bytes) {
        if (bytes == null) {
            return;
        }
        switch (id) {
            case 0x01 -> put(out, prefix + "mileage_km", scaled(bytes, 4, 1));
            case 0x02 -> put(out, prefix + "fuel_l", scaled(bytes, 2, 1));
            case 0x03 -> put(out, prefix + "recorder_speed_kmh", scaled(bytes, 2, 1));
            case 0x04 -> put(out, prefix + "manual_alarm_event_id", unsigned(bytes, 2));
            case 0x05 -> decodeTirePressure(out, prefix, bytes);
            case 0x06 -> put(out, prefix + "compartment_temp_c", signedHighBitWord(bytes));
            case 0x11 -> decodeOverspeed(out, prefix, bytes);
            case 0x12 -> decodeAreaRouteAlarm(out, prefix, bytes);
            case 0x13 -> decodeRouteTime(out, prefix, bytes);
            case 0x25 -> decodeVehicleSignal(out, prefix, bytes);
            case 0x2A -> decodeIoStatus(out, prefix, bytes);
            case 0x2B -> decodeAnalog(out, prefix, bytes);
            case 0x30 -> put(out, prefix + "network_signal_strength", unsigned(bytes, 1));
            case 0x31 -> put(out, prefix + "gnss_satellite_count", unsigned(bytes, 1));
            default -> {
            }
        }
    }

    private static void decodeTirePressure(Map<String, String> out, String prefix, byte[] bytes) {
        if (bytes.length != 30) {
            return;
        }
        List<String> values = new ArrayList<>();
        List<Integer> validIndexes = new ArrayList<>();
        List<Integer> invalidIndexes = new ArrayList<>();
        for (int i = 0; i < bytes.length; i++) {
            int value = u8(bytes, i);
            if (value == 0xFF) {
                invalidIndexes.add(i);
            } else {
                validIndexes.add(i);
                values.add(Integer.toString(value));
            }
        }
        put(out, prefix + "tire_pressure_pa_values", String.join(",", values));
        put(out, prefix + "tire_pressure_valid_indexes", ranges(validIndexes));
        put(out, prefix + "tire_pressure_invalid_indexes", ranges(invalidIndexes));
    }

    private static void decodeOverspeed(Map<String, String> out, String prefix, byte[] bytes) {
        if (bytes.length != 1 && bytes.length != 5) {
            return;
        }
        put(out, prefix + "overspeed.position_type", Integer.toString(u8(bytes, 0)));
        if (bytes.length == 5) {
            put(out, prefix + "overspeed.area_route_id", Long.toString(u32(bytes, 1)));
        }
    }

    private static void decodeAreaRouteAlarm(Map<String, String> out, String prefix, byte[] bytes) {
        if (bytes.length != 6) {
            return;
        }
        put(out, prefix + "area_route_alarm.position_type", Integer.toString(u8(bytes, 0)));
        put(out, prefix + "area_route_alarm.area_route_id", Long.toString(u32(bytes, 1)));
        put(out, prefix + "area_route_alarm.direction", Integer.toString(u8(bytes, 5)));
    }

    private static void decodeRouteTime(Map<String, String> out, String prefix, byte[] bytes) {
        if (bytes.length != 7) {
            return;
        }
        put(out, prefix + "route_time.segment_id", Long.toString(u32(bytes, 0)));
        put(out, prefix + "route_time.duration_seconds", Integer.toString(u16(bytes, 4)));
        put(out, prefix + "route_time.result", Integer.toString(u8(bytes, 6)));
    }

    private static void decodeVehicleSignal(Map<String, String> out, String prefix, byte[] bytes) {
        if (bytes.length != 4) {
            return;
        }
        long raw = u32(bytes, 0);
        put(out, prefix + "vehicle_signal_raw", Long.toString(raw));
        for (int i = 0; i < VEHICLE_SIGNAL_BITS.length; i++) {
            put(out, prefix + "vehicle_signal." + VEHICLE_SIGNAL_BITS[i], Boolean.toString(bit(raw, i)));
        }
    }

    private static void decodeIoStatus(Map<String, String> out, String prefix, byte[] bytes) {
        if (bytes.length != 2) {
            return;
        }
        int raw = u16(bytes, 0);
        put(out, prefix + "io_status_raw", Integer.toString(raw));
        for (int i = 0; i < IO_STATUS_BITS.length; i++) {
            put(out, prefix + "io_status." + IO_STATUS_BITS[i], Boolean.toString(bit(raw, i)));
        }
    }

    private static void decodeAnalog(Map<String, String> out, String prefix, byte[] bytes) {
        if (bytes.length != 4) {
            return;
        }
        long raw = u32(bytes, 0);
        put(out, prefix + "analog_raw", Long.toString(raw));
        put(out, prefix + "analog_ad0", Integer.toString((int) (raw & 0xFFFF)));
        put(out, prefix + "analog_ad1", Integer.toString((int) ((raw >>> 16) & 0xFFFF)));
    }

    private static String scaled(byte[] bytes, int expectedLength, int scale) {
        if (bytes.length != expectedLength) {
            return null;
        }
        long raw = expectedLength == 4 ? u32(bytes, 0) : u16(bytes, 0);
        return BigDecimal.valueOf(raw, scale).stripTrailingZeros().toPlainString();
    }

    private static String unsigned(byte[] bytes, int expectedLength) {
        if (bytes.length != expectedLength) {
            return null;
        }
        return switch (expectedLength) {
            case 1 -> Integer.toString(u8(bytes, 0));
            case 2 -> Integer.toString(u16(bytes, 0));
            case 4 -> Long.toString(u32(bytes, 0));
            default -> null;
        };
    }

    private static String signedHighBitWord(byte[] bytes) {
        if (bytes.length != 2) {
            return null;
        }
        int raw = u16(bytes, 0);
        int value = (raw & 0x8000) == 0 ? raw : -(raw & 0x7FFF);
        return Integer.toString(value);
    }

    private static int u8(byte[] bytes, int offset) {
        return bytes[offset] & 0xFF;
    }

    private static int u16(byte[] bytes, int offset) {
        return (u8(bytes, offset) << 8) | u8(bytes, offset + 1);
    }

    private static long u32(byte[] bytes, int offset) {
        return ((long) u8(bytes, offset) << 24)
                | ((long) u8(bytes, offset + 1) << 16)
                | ((long) u8(bytes, offset + 2) << 8)
                | u8(bytes, offset + 3);
    }

    private static boolean bit(long raw, int bit) {
        return ((raw >>> bit) & 1L) == 1L;
    }

    private static void put(Map<String, String> out, String key, String value) {
        if (value != null) {
            out.put(key, value);
        }
    }

    private static String ranges(List<Integer> indexes) {
        if (indexes.isEmpty()) {
            return "";
        }
        List<String> ranges = new ArrayList<>();
        int start = indexes.getFirst();
        int previous = start;
        for (int i = 1; i < indexes.size(); i++) {
            int current = indexes.get(i);
            if (current == previous + 1) {
                previous = current;
                continue;
            }
            ranges.add(formatRange(start, previous));
            start = current;
            previous = current;
        }
        ranges.add(formatRange(start, previous));
        return String.join(",", ranges);
    }

    private static String formatRange(int start, int end) {
        return start == end ? Integer.toString(start) : start + "-" + end;
    }
}
