"""Parse CAN packet either from hardcoded interface or file."""

import json
import logging
from pathlib import Path
import re
import socket
import struct
import time

can_frame_fmt = "=IB3x8s"
can_frame_size = struct.calcsize(can_frame_fmt)
pstr = lambda b: str(b, encoding="latin-1")
can_busses = []
can_bus_ids = []
CAN_BUS = "vcan0"
PGN_ALL = {
    223: [0, 5, "On Board Diagnostic II"],
    59392: [0, 0, "ISO  Acknowledgment"],
    60928: [0, 0, "ISO  Address Claim"],
    126208: [0, 0, "NMEA Command/Request/Acknowledge"],
    126464: [1, 0, "ISO  Transmit/Receive PGN List"],
    126992: [0, 0, "GNSS System Time"],
    126996: [1, 0, "ISO  Product Information"],
    127245: [0, 4, "NAV Rudder"],
    127250: [0, 4, "NAV Vessel Heading"],
    127258: [0, 0, "GNSS Magnetic Variation"],
    127506: [1, 3, "PWR DC Detailed Status"],
    127508: [0, 3, "PWR Battery Status"],
    127513: [1, 3, "PWR Battery Configuration Status"],
    128259: [0, 4, "NAV Speed"],
    128267: [0, 4, "NAV Water Depth"],
    128275: [1, 4, "NAV Distance Log"],
    129025: [0, 1, "GNSS Position Rapid Update"],
    129026: [0, 1, "GNSS COG and SOG Rapid Update"],
    129029: [1, 1, "GNSS Position Data"],
    129038: [1, 2, "AIS  Class A Position Report"],
    129039: [1, 2, "AIS  Class B Position Report"],
    129040: [1, 2, "AIS  Class B Extended Position Report"],
    129283: [0, 0, "NAV Cross Track Error"],
    129284: [1, 0, "NAV Navigation Data"],
    129285: [1, 0, "NAV Navigation - Route/WP Information"],
    129539: [0, 1, "GNSS DOPs"],
    129540: [1, 1, "GNSS Satellites in View"],
    129793: [1, 2, "AIS  UTC and Date report"],
    129794: [1, 2, "AIS  Class A Static and Voyage Related Data"],
    129798: [1, 2, "AIS  SAR Aircraft Position Report"],
    129802: [1, 2, "AIS  Safety Related Broadcast Message"],
    129809: [1, 2, "AIS  Class B CS Static Data Report, Part A"],
    129810: [1, 2, "AIS  Class B CS Static Data Report, Part B"],
    130306: [0, 4, "NAV Wind Data"],
    130310: [
        0,
        4,
        "NAV Water Temp., Outside Air Temp.,Atmospheric Pressure",
    ],
    130311: [0, 4, "NAV Environmental Parameters"],
}
obdecode = {
    4: ["B4x", [100, 255, 0, "Calculated engine load"]],
    5: ["B4x", [1, 1, -40, "Engine coolant temperature"]],
    6: [
        "B4x",
        [100, 128, -100, "Short term fuel trim (STFT)—Bank 1"],
    ],
    7: [
        "B4x",
        [100, 128, -100, "Long term fuel trim (STFT)—Bank 1"],
    ],
    8: [
        "B4x",
        [100, 128, -100, "Short term fuel trim (STFT)—Bank 2"],
    ],
    9: [
        "B4x",
        [100, 128, -100, "Long term fuel trim (STFT)—Bank 2"],
    ],
    10: ["B4x", [300, 100, 0, "Fuel pressure"]],
    11: ["B4x", [1, 1, 0, "Intake manifold absolute pressure"]],
    12: ["H3x", [1, 1, 0, "Engine speed"]],
    13: ["B4x", [1, 1, 0, "Vehicle speed"]],
    14: ["B4x", [100, 200, -64, "Timing advance"]],
    15: ["B4x", [1, 1, -40, "Intake air temperature"]],
    16: ["H3x", [1, 100, 0, "Mass air flow sensor (MAF) rate"]],
    17: ["B4x", [100, 255, 0, "Throttle position"]],
    20: [
        "BB3x",
        [1, 200, 0, "Oxygen sensor 1 voltage"],
        [100, 128, -100, "Oxygen sensor 1 short term fuel trim"],
    ],
    21: [
        "BB3x",
        [1, 200, 0, "Oxygen sensor 2 voltage"],
        [100, 128, -100, "Oxygen sensor 2 short term fuel trim"],
    ],
    22: [
        "BB3x",
        [1, 200, 0, "Oxygen sensor 3 voltage"],
        [100, 128, -100, "Oxygen sensor 3 short term fuel trim"],
    ],
    23: [
        "BB3x",
        [1, 200, 0, "Oxygen sensor 4 voltage"],
        [100, 128, -100, "Oxygen sensor 4 short term fuel trim"],
    ],
    24: [
        "BB3x",
        [1, 200, 0, "Oxygen sensor 5 voltage"],
        [100, 128, -100, "Oxygen sensor 5 short term fuel trim"],
    ],
    25: [
        "BB3x",
        [1, 200, 0, "Oxygen sensor 6 voltage"],
        [100, 128, -100, "Oxygen sensor 6 short term fuel trim"],
    ],
    26: [
        "BB3x",
        [1, 200, 0, "Oxygen sensor 7 voltage"],
        [100, 128, -100, "Oxygen sensor 7 short term fuel trim"],
    ],
    27: [
        "BB3x",
        [1, 200, 0, "Oxygen sensor 8 voltage"],
        [100, 128, -100, "Oxygen sensor 8 short term fuel trim"],
    ],
    28: ["B4x", [1, 1, 0, "OBD standards"]],
    31: ["H3x", [1, 1, 0, "Run time since engine start"]],
    33: [
        "H3x",
        [
            100,
            100,
            0,
            "Malfunction indicator lamp (MIL) travel distance",
        ],
    ],
    34: ["H3x", [79, 1000, 0, "Fuel Rail Pressure"]],
    35: ["H3x", [1000, 100, 0, "Fuel Rail Pressure"]],
    36: [
        "HHx",
        [2, 65536, 0, "Oxygen sensor 1 air/fuel equivalence ratio"],
        [8, 65536, 0, "Oxygen sensor 1 voltage"],
    ],
    37: [
        "HHx",
        [2, 65536, 0, "Oxygen sensor 2 air/fuel equivalence ratio"],
        [8, 65536, 0, "Oxygen sensor 2 voltage"],
    ],
    38: [
        "HHx",
        [2, 65536, 0, "Oxygen sensor 3 air/fuel equivalence ratio"],
        [8, 65536, 0, "Oxygen sensor 3 voltage"],
    ],
    39: [
        "HHx",
        [2, 65536, 0, "Oxygen sensor 4 air/fuel equivalence ratio"],
        [8, 65536, 0, "Oxygen sensor 4 voltage"],
    ],
    40: [
        "HHx",
        [2, 65536, 0, "Oxygen sensor 5 air/fuel equivalence ratio"],
        [8, 65536, 0, "Oxygen sensor 5 voltage"],
    ],
    41: [
        "HHx",
        [2, 65536, 0, "Oxygen sensor 6 air/fuel equivalence ratio"],
        [8, 65536, 0, "Oxygen sensor 6 voltage"],
    ],
    42: [
        "HHx",
        [2, 65536, 0, "Oxygen sensor 7 air/fuel equivalence ratio"],
        [8, 65536, 0, "Oxygen sensor 7 voltage"],
    ],
    43: [
        "HHx",
        [2, 65536, 0, "Oxygen sensor 8 air/fuel equivalence ratio"],
        [8, 65536, 0, "Oxygen sensor 8 voltage"],
    ],
    44: ["B4x", [100, 255, 0, "Commanded EGR"]],
    45: ["B4x", [100, 128, -100, "EGR Error"]],
    46: ["B4x", [100, 255, 0, "Commanded evaporative purge"]],
    47: ["B4x", [100, 255, 0, "Fuel tank level input"]],
    48: ["B4x", [1, 1, 0, "Warm ups since codes cleared"]],
    49: [
        "H3x",
        [1, 1, 0, "Distance traveled since codes cleared"],
    ],
    50: ["H3x", [100, 400, 0, "Evap. system vapor pressure"]],
    51: ["B4x", [1, 1, 0, "Absolute barometric pressure"]],
    52: [
        "HHx",
        [
            2,
            65536,
            0,
            "Oxygen sensor 1 air/fuel equivalence ratio lambda",
        ],
        [8, 65536, 0, "Oxygen sensor 1 current"],
    ],
    53: [
        "HHx",
        [
            2,
            65536,
            0,
            "Oxygen sensor 2 air/fuel equivalence ratio lambda",
        ],
        [8, 65536, 0, "Oxygen sensor 2 current"],
    ],
    54: [
        "HHx",
        [
            2,
            65536,
            0,
            "Oxygen sensor 3 air/fuel equivalence ratio lambda",
        ],
        [8, 65536, 0, "Oxygen sensor 3 current"],
    ],
    55: [
        "HHx",
        [
            2,
            65536,
            0,
            "Oxygen sensor 4 air/fuel equivalence ratio lambda",
        ],
        [8, 65536, 0, "Oxygen sensor 4 current"],
    ],
    56: [
        "HHx",
        [
            2,
            65536,
            0,
            "Oxygen sensor 5 air/fuel equivalence ratio lambda",
        ],
        [8, 65536, 0, "Oxygen sensor 5 current"],
    ],
    57: [
        "HHx",
        [
            2,
            65536,
            0,
            "Oxygen sensor 6 air/fuel equivalence ratio lambda",
        ],
        [8, 65536, 0, "Oxygen sensor 6 current"],
    ],
    58: [
        "HHx",
        [
            2,
            65536,
            0,
            "Oxygen sensor 7 air/fuel equivalence ratio lambda",
        ],
        [8, 65536, 0, "Oxygen sensor 7 current"],
    ],
    59: [
        "HHx",
        [
            2,
            65536,
            0,
            "Oxygen sensor 8 air/fuel equivalence ratio lambda",
        ],
        [8, 65536, 0, "Oxygen sensor 8 current"],
    ],
    60: [
        "H3x",
        [10, 100, -40, "Catalyst temperature bank 1 sensor 1"],
    ],
    61: [
        "H3x",
        [10, 100, -40, "Catalyst temperature bank 2 sensor 1"],
    ],
    62: [
        "H3x",
        [10, 100, -40, "Catalyst temperature bank 1 sensor 2"],
    ],
    63: [
        "H3x",
        [10, 100, -40, "Catalyst temperature bank 2 sensor 2"],
    ],
    66: ["H3x", [1, 1000, 0, "Control module voltage"]],
    67: ["H3x", [100, 255, 0, "Absolute load value"]],
    68: [
        "H3x",
        [2, 65536, 0, "Commanded air/fuel equivalence ratio"],
    ],
    69: ["B4x", [100, 255, 0, "Relative throttle position"]],
    70: ["B4x", [1, 1, -40, "Ambient air temperature"]],
    71: ["B4x", [100, 255, 0, "Absolute throttle position B"]],
    72: ["B4x", [100, 255, 0, "Absolute throttle position C"]],
    73: ["B4x", [100, 255, 0, "Absolute throttle position D"]],
    74: ["B4x", [100, 255, 0, "Absolute throttle position E"]],
    75: ["B4x", [100, 255, 0, "Absolute throttle position F"]],
    76: ["B4x", [100, 255, 0, "Commanded throttle actuator"]],
    77: ["H3x", [1, 1, 0, "Time  run w/ MIL on"]],
    78: ["H3x", [1, 1, 0, "Time since trouble codes cleared"]],
    79: [
        "BBBBx",
        [1, 1, 0, "Maximum value for fuel/air equivalence ratio"],
        [1, 1, 0, "Oxygen sensor voltage"],
        [1, 1, 0, "Oxygen sensor current"],
        [1, 10, 0, "Intake manifold absolute pressure"],
    ],
    80: [
        "B4x",
        [1000, 100, 0, "Maximum flow rate from MAF sensor"],
    ],
    82: ["B4x", [100, 255, 0, "Ethanol Fuel %"]],
    83: [
        "H3x",
        [1, 200, 0, "Absolute evaporative system vapor pressure"],
    ],
    84: ["H3x", [1, 1, 0, "Evap system vapor pressure"]],
    85: [
        "BB3x",
        [
            100,
            128,
            -100,
            "Short term secondary Oxygen sensor trim bank 1",
        ],
        [
            100,
            128,
            -100,
            "Short term secondary Oxygen sensor trim bank 3",
        ],
    ],
    86: [
        "BB3x",
        [
            100,
            128,
            -100,
            "Long term secondary Oxygen sensor trim bank 1",
        ],
        [
            100,
            128,
            -100,
            "Long term secondary Oxygen sensor trim bank 3",
        ],
    ],
    87: [
        "BB3x",
        [
            100,
            128,
            -100,
            "Short term secondary Oxygen sensor trim bank 2",
        ],
        [
            100,
            128,
            -100,
            "Short term secondary Oxygen sensor trim bank 4",
        ],
    ],
    88: [
        "BB3x",
        [
            100,
            128,
            -100,
            "Long term secondary Oxygen sensor trim bank 2",
        ],
        [
            100,
            128,
            -100,
            "Long term secondary Oxygen sensor trim bank 4",
        ],
    ],
    89: ["H3x", [1000, 100, 0, "Fuel rail absolute pressure"]],
    90: [
        "B4x",
        [100, 255, 0, "Relative accelerator pedal position"],
    ],
    91: [
        "B4x",
        [100, 255, 0, "Hybrid battery pack remaining life"],
    ],
    92: ["B4x", [1, 1, -40, "Engine oil temperature"]],
    93: ["H3x", [100, 128, -210, "Fuel injection timing"]],
    94: ["H3x", [100, 20, 0, "Engine fuel rate"]],
    97: [
        "B4x",
        [1, 1, -125, "Drivers demand engine - percent torque"],
    ],
    98: [
        "B4x",
        [1, 1, -125, "Actual engine - percent torque"],
    ],
    99: [
        "H3x",
        [1, 1, 0, "Engine reference torque"],
    ],
    100: [
        "5B",
        [1, 1, -125, "engine percent torque - idle"],
        [1, 1, -125, "engine percent torque - point 1"],
        [1, 1, -125, "engine percent torque - point 2"],
        [1, 1, -125, "engine percent torque - point 3"],
        [1, 1, -125, "engine percent torque - point 4"],
    ],
    102: [
        "xHH",
        [1, 32, 0, "Mass air flow sensor A"],
        [1, 32, 0, "Mass air flow sensor B"],
    ],
    103: [
        "x2B2x",
        [1, 1, -40, "Engine coolant temperature A"],
        [1, 1, -40, "Engine coolant temperature B"],
    ],
    104: [
        "x2B2x",
        [1, 1, -40, "Intake air temperature A"],
        [1, 1, -40, "Intake air temperature B"],
    ],
    124: [
        "H3x",
        [1, 10, -40, "Diesel particulate filter (DPF) temperature"],
    ],
    133: [
        "4xB",
        [100, 255, 0, "NOx reagent system"],
    ],
    141: ["B4x", [100, 255, 0, "Absolute throttle position F"]],
    142: [
        "B4x",
        [1, 1, -125, "Engine friction - percent torque"],
    ],
    155: [
        "3xBx",
        [100, 255, 0, "Diesel exhaust fluid sensor data"],
    ],
    162: ["H3x", [1, 32, 0, "Cylinder fuel rate"]],
    164: ["2xHx", [1, 1000, 0, "Transmission actual gear ration"]],
    165: [
        "xB2x",
        [100, 200, 0, "Commanded Diesel exhaust fuel dosing"],
    ],
    166: ["Ix", [1, 10, 0, "Odometer"]],
}
log = logging.getLogger("gpscant")
logging.basicConfig(level=logging.DEBUG)


def hnd_generic(can_bus, device, buf, pgn):
    """Handle unsupported CAN PGNs minimally."""
    lenny = len(buf)
    pgn_title = "unhandled" if pgn not in PGN_ALL else PGN_ALL[pgn][2]
    log.debug(
        "%s:%02x PGN%06d got %dB of %s",
        can_bus,
        device,
        pgn,
        lenny,
        pgn_title,
    )
    for row in range(0, lenny, 16):
        log.debug(
            "> %08x  %s",
            row,
            " ".join(["%02x" % x for x in buf[row:][:16]]),
        )


def hnd_223(buf):
    """Handle some On-Board-Diagnostic-II packets."""
    data = struct.unpack("<BBB", buf[:3])
    if data[2] in range(0, 193, 32):
        supported = set()
        inty = struct.unpack("<I", buf[3:7])
        for koff in range(32, 0, -1):
            if 0 != (inty & (1 << (32 - koff))):
                supported += data[2] + koff
    elif data[2] in obdecode:
        frag = buf[3 : 1 + data[0]]
        work = obdecode[data[2]]
        idata = struct.unpack("<" + work[0], frag)
        ret = {}
        for row, member in enumerate(work[1:]):
            if member[0] == member[1]:
                ret[member[3]] = idata[row] + member[2]
            else:
                ret[member[3]] = (
                    float(idata[row]) * member[0] / member[1]
                    + member[2]
                )
        return ret
    return None


def hnd_126992(buf):
    """Handle PGN 126992."""
    if 8 != len(buf):
        return None
    data = struct.unpack("<BxHL", buf)
    ret = {
        "SID": data[0],
        "time": time.strftime(
            "%Y-%m-%dT%H:%M:%SZ",
            time.gmtime(68400 * data[1] + data[2] / 1e4),
        ),
    }
    return ret


def hnd_127506(buf):
    """Handle PGN 127506."""
    ret = {}
    data = struct.unpack("<BBBBBHHH", buf[:11])
    fields = [
        "SID",
        "Instance",
        "DC Type",
        "State of Charge",
        "State of Heath",
        "Time Remaining",
        "Ripple Voltage",
        "Remaining Capacity",
    ]
    for key, value in zip(fields, data):
        ret[key] = value
    return ret


def hnd_127508(buf):
    """Handle PGN 127508."""
    data = struct.unpack("<BHhHB", buf)
    return {
        "SID": data[4],
        "Instance": data[0],
        "Voltage": data[1] / 1e2,
        "Current": data[2] / 1e1,
        "Temperature": data[3] / 1e2,
    }


def hnd_129025(buf):
    """Handle PGN 129025."""
    data = struct.unpack("<ii", buf)
    return {
        "lat": data[0] / 1e7,
        "lon": data[1] / 1e7,
    }


def hnd_129026(buf):
    """Handle PGN 129026."""
    data = struct.unpack("<BBHH2x", buf)
    ret = {
        "SID": data[0],
        "speed": data[3] / 1e2,
    }
    cogr = data[1] & 3
    tv = data[2] / 1e4
    if 0 == cogr:
        ret["track"] = tv
    if 1 == cogr:
        ret["magtrack"] = tv
    return ret


def hnd_129029(buf):
    """Handle PGN 129029."""
    data = struct.unpack("<BHHlllxxxhhlb", buf[:29])
    ret = {
        "SID": data[0],
        "time": time.strftime(
            "%Y-%m-%dT%H:%M:%SZ",
            time.gmtime(68400 * data[1] + data[2] / 1e4),
        ),
        "lat": data[3] / 1e16,
        "lon": data[3] / 1e16,
        "alt": data[3] / 1e6,
        "hdop": data[4] / 1e2,
        "pdop": data[5] / 1e2,
        "geoidSep": data[6] / 1e2,
    }
    return ret


def hnd_129038(buf):
    """Handle PGN 129038."""
    data = struct.unpack("<BIiiBHHBBBHhBB", buf[:27])
    tri = data[7] << 16 | data[8] << 8 | data[9]
    ret = {
        "mid": data[0] // 4,
        "repeat": data[0] % 4,
        "mmsi": data[1],
        "lon": data[2] / 1e7,
        "lat": data[3] / 1e7,
        "accp": 0 != (data[4] & 0x80),
        "raim": 0 != (data[4] & 0x40),
        "sec": data[4] & 0x3F,
        "cog": data[5] / 1e4,
        "sog": data[6] / 1e2,
        "commstate": tri & 0xFFFFE0,
        "aistransceiverinfo": tri & 0x00001F,
        "navstatus": (data[10] >> 4) & 0xF,
        "specialmaneuver": (data[10] >> 2) & 0x3,
        "reserved": data[10] & 0x3,
        #   "sequence": data[11],
    }
    return ret


def hnd_129039(buf):
    """Handle PGN 129039."""
    data = struct.unpack("<BIiiBHHBBBHBBH", buf[:27])
    tri = data[7] << 16 | data[8] << 8 | data[9]
    ret = {
        "mid": data[0] // 4,
        "repeat": data[0] % 4,
        "mmsi": data[1],
        "lon": data[2] / 1e7,
        "lat": data[3] / 1e7,
        "accp": 0 != (data[4] & 0x80),
        "raim": 0 != (data[4] & 0x40),
        "timestamp": data[4] & 0x3F,
        "cog": data[5] / 1e4,
        "sog": data[6] / 1e2,
        "commstate": tri & 0xFFFFE0,
        "navstatus": tri & 0x00001F,
        "heading": data[10] / 1e4,
        "rgnapp": data[11],
        "rgnappb": (data[12] >> 6) & 3,
        "unittype": 0 != (data[12] & 0x20),
        "integrated": 0 != (data[12] & 0x10),
        "dsc": 0 != (data[12] & 0x08),
        "band": 0 != (data[12] & 0x04),
        "msg22": 0 != (data[12] & 0x02),
        "aismode": 0 != (data[12] & 0x01),
        "aiscommstate": 0 != (data[13] & 0x80),
        "reserved": data[13] & 0x7F,
    }
    return ret


def hnd_128267(buf):
    """Handle NAV Water Depth (PGN 128267)."""
    if 8 != len(buf):
        return None
    data = struct.unpack("<BIhB", buf)
    return {
        "SID": data[0],
        "depth": data[1] / 1e2,
        # "offset": data[2] / 1e3,
        # "range": data[3],
    }


def hnd_129539(buf):
    """Handle PGN 129539."""
    if 8 != len(buf):
        return None
    data = struct.unpack("<BBhhh", buf)
    ret = {
        "SID": data[0],
        "hdop": data[2] / 1e2,
        "vdop": data[3] / 1e2,
        "tdop": data[4] / 1e2,
    }
    return ret


def hnd_129540(buf):
    """Handle PGN 129540."""
    data = struct.unpack("<BBB", buf[:3])
    ret = {"SID": data[0], "nSat": data[2]}
    sats = []
    for sat in range(data[2]):
        data = struct.unpack("<BhHHiB", buf[3 + 4 * sat :][:12])
        sats.append(
            {
                "PRN": data[0],
                "az": data[2] / 1e4,
                "el": data[1] / 1e4,
                "ss": data[3] / 1e2,
                "prRes": data[4],
                "status": (data[5] >> 4) & 0x0F,
            }
        )
    ret["satellites"] = sats
    return ret


def hnd_129794(buf):
    """Handle PGN 129794."""
    data = struct.unpack("<BII7s20sBHHHHHIH20sBB", buf[:75])
    return {
        "messageid": (data[0] >> 2) & 0x3F,
        "repeat": data[0] & 0x3F,
        "mmsi": data[1],
        "imonumber": data[2],
        "callsign": pstr(data[3]),
        "name": pstr(data[4]),
        "shiptype": data[5],
        "length": data[6],
        "beam": data[7],
        "offset starboard": data[8],
        "offset bow": data[9],
        "eta": time.strftime(
            "%Y-%m-%dT%H:%M:%SZ",
            time.gmtime(68400 * data[10] + data[11] / 1e4),
        ),
        "draft": data[12],
        "destination": pstr(data[13]),
        "aisver": (data[14] >> 6) & 0x3,
        "gnsstype": (data[15] >> 2) & 0xF,
        "dte": (data[15] >> 1) & 0x1,
        "reserved1": (data[15] >> 0) & 0x1,
        "aistransceiverinfo": (data[15] >> 3) & 0x1F,
        "reserved2": (data[15] >> 0) & 0x7,
    }


def hnd_129809(buf):
    """Handle PGN 129809."""
    data = struct.unpack("<BI20sBB", buf[:27])
    return {
        "messageid": (data[0] >> 2) & 0x3F,
        "repeat": data[0] & 0x3F,
        "mmsi": data[1],
        "vendor": pstr(data[2]),
        "callsign": pstr(data[2]),
        "aistransceiverinfo": (data[3] >> 3) & 0x1F,
        "reserved2": (data[3] >> 0) & 0x7,
        "SID": data[4],
    }


def hnd_129810(buf):
    """Handle PGN 129810."""
    data = struct.unpack("<BIB7s7sHHHHIBB", buf[:34])
    return {
        "messageid": (data[0] >> 2) & 0x3F,
        "repeat": data[0] & 0x3F,
        "userid": data[1],
        "shiptype": data[2],
        "vendorID": pstr(data[3]),
        "callsign": pstr(data[4]),
        "length": data[5],
        "beam": data[6],
        "offset starboard": data[7],
        "offset bow": data[8],
        "mothershipuserid": data[9],
        "reserved": (data[10] >> 6) & 0x3,
        "aistransceiverinfo": (data[11] >> 3) & 0x1F,
        "reserved2": (data[11] >> 0) & 0x7,
        # "SID": data[12],
    }


class FastPGN:
    """A class for handling a PGN on a CAN device."""

    def __del__(self):
        if False:
            return
        for pkt in self.pktbuf:
            xdump = " ".join(["%02x" % x for x in pkt])
            log.debug(
                "%s:%02x PGN %d remnant %s",
                self.can_bus,
                self.device,
                self.pgn,
                xdump,
            )
        # object.__del__(self)  # No __del__ in object

    def __init__(self, pgn, can_bus, device):
        """Initialize the CAN PGN handling class."""
        self.pgn = pgn
        self.can_bus = can_bus
        self.device = device
        self.when = None
        self.pktbuf = []
        self.pktlen = -1
        self.sequence = -1
        self.mask = 0
        hstr = "hnd_%d" % pgn
        if hstr not in globals():
            # hnd_generic(data, pgn)
            log.warning(
                "Unimplemented fast PGN %d '%s'", pgn, PGN_ALL[pgn][2]
            )
            self.hnd = self.nop
        else:
            self.hnd = globals()[hstr]

    def nop(self, data):
        """Do Nothing when fast PGN not handled."""

    def reset(self):
        """Reset if changing sequence number."""
        self.when = None
        self.pktbuf = []
        self.pktlen = -1
        self.sequence = -1
        self.mask = 0

    def add(self, data, when):
        """Add a can packet to its sequence resetting as needed."""
        # If changing sentence number reset, set and inject
        pktnum = data[0] & 0x1F
        sequence = (data[0] & 0xE0) / 32
        log.debug(
            "Fast packet %s:%02x pgn%6d seq%d pkt %2d",
            self.can_bus,
            self.device,
            self.pgn,
            sequence,
            pktnum,
        )
        if self.sequence != sequence:
            if self.pktbuf:
                log.warning(
                    "%s:%02x PGN %d fast discarding %d",
                    self.can_bus,
                    self.device,
                    self.pgn,
                    len(self.pktbuf),
                )
                if 0 < self.pktlen:
                    log.debug(
                        "%s:%02x PGN %d missing pieces %s",
                        self.can_bus,
                        self.device,
                        self.pgn,
                        " ".join(
                            str(x) if 0 != (self.mask & (1 << x)) else ""
                            for x in range(self.pktlen + 1)
                        ).strip(),
                    )
                for pkt in self.pktbuf:
                    xdump = " ".join(["%02x" % x for x in pkt])
                    log.debug(
                        "%s:%02x PGN %d reject seq%02d#%02d %s",
                        self.can_bus,
                        self.device,
                        self.pgn,
                        (pkt[0] & 0xE0) >> 5,
                        pkt[0] & 0x1F,
                        xdump,
                    )
            self.reset()
            self.when = when
            self.sequence = sequence

        self.pktbuf.append(data)  # append additional packet in sentence
        self.mask |= 1 << pktnum
        if 0 == pktnum:
            self.pktlen = (data[1] + 6) // 7  # shim this
        if -1 == self.pktlen or 1 != (1 << self.pktlen) - self.mask:
            return None
        log.info(
            "%s:%02x PGN %d complete in %d",
            self.can_bus,
            self.device,
            self.pgn,
            len(self.pktbuf),
        )
        out = [0 for x in range(7 * self.pktlen)]
        for pkt in self.pktbuf:
            pktnum = pkt[0] & 0x1F
            offset = 7 * pktnum
            xdump = " ".join(["%02x" % x for x in pkt])
            if 0 != (self.mask & (1 << pktnum)):
                out[offset : offset + 7] = pkt[1:]
                self.mask ^= 1 << pktnum
                log.debug(
                    "%s:%02x PGN %d accept %s",
                    self.can_bus,
                    self.device,
                    self.pgn,
                    xdump,
                )
            else:
                log.warning(
                    "%s:%02x PGN %d excess %s",
                    self.can_bus,
                    self.device,
                    self.pgn,
                    xdump,
                )
        # hnd_generic(self.can_bus, self.device, out, self.pgn)
        length = out[0]
        out = bytes(out[1:])
        if length >= len(out):
            log.warning(
                "%s:%02x PGN%6d short buffer %d/%d",
                self.can_bus,
                self.device,
                self.pgn,
                len(out),
                length,
            )
            return None
        log.debug(
            "%s:%02x PGN%6d parse",
            self.can_bus,
            self.device,
            self.pgn,
        )
        ret = self.hnd(out)
        if not isinstance(ret, dict):
            return None
        ret["device"] = "%s:%02x" % (self.can_bus, self.device)
        # ret["ftime"] = self.when
        self.reset()
        return ret


class CanDevice:
    """Class to handle and store data for a CAN device."""

    def __init__(self, can_bus, device):
        """Initialize the class."""
        self.pgns = []
        self.bufs = []
        self.can_bus = can_bus
        self.device = device
        self.sid = None
        self.jsob = {}

    def add_frame(self, can_id, _dlen, data):
        """Process singleton CAN packets and stage sets."""
        when = time.time()
        pgn = pgn_from_can_id(can_id)

        # Bucket sort out PGNs we do not support
        if pgn not in PGN_ALL:
            log.warning(
                "%s:%02x PGN %d Unknown/unimplemented ",
                self.can_bus,
                self.device,
                pgn,
            )
            hnd_generic(self.can_bus, self.device, data, pgn)
            return None

        # Bucket sort out singleton CAN datagrams
        if 0 == PGN_ALL[pgn][0]:
            temp = self.singleton(pgn, data, when)
        elif 1 == PGN_ALL[pgn][0]:
            temp = self.fast(pgn, data, when)
        else:
            log.warning(
                "%s:%02x PGN %06d unhandled transport %d",
                self.can_bus,
                self.device,
                pgn,
                PGN_ALL[pgn][0],
            )
            return None
        if not isinstance(temp, dict):
            return None
        temp["device"] = "CAN://%s:%02x" % (self.can_bus, self.device)
        return temp

    def singleton(self, pgn, data, _when):
        """Handle singleton CAN packets."""
        log.info(
            "%s:%02x pgn %6d Single packet",
            self.can_bus,
            self.device,
            pgn,
        )
        hstr = "hnd_%d" % pgn
        if hstr not in globals():
            hnd_generic(self.can_bus, self.device, data, pgn)
            log.warning(
                "Unimplemented PGN %d '%s'", pgn, PGN_ALL[pgn][2]
            )
            return None
        return globals()[hstr](data)

    def fast(self, pgn, data, when):
        """Handle 'fast' PGN CAN packet."""
        if pgn not in self.pgns:
            log.info(
                "%s:%02x New PGN %06d", self.can_bus, self.device, pgn
            )
            self.bufs.append(FastPGN(pgn, self.can_bus, self.device))
            self.pgns.append(pgn)

        return self.bufs[self.pgns.index(pgn)].add(data, when)


def dissect_can_frame(frame):
    """Given a socket can frame return the CAN ID and data."""
    can_id, can_dlc, data = struct.unpack(can_frame_fmt, frame)
    return (can_id, can_dlc, data[:can_dlc])


def pgn_from_can_id(can_id):
    """Gete the PGN from the CAN_ID."""
    ret = can_id >> 8
    return ret & (0x1F00 if 0xF000 == (can_id & 0xF000) else 0x1FFFF)


def saddr_from_can_id(can_id):
    """Get the source address from the CAN ID."""
    return can_id & 0xFF


def data_ingress(busses, _when, bus_name, can_id, data):
    """Handle the packets from file_cook and/or socket_glom."""
    if bus_name not in busses:
        busses[bus_name] = [None for c in range(256)]
        log.info("%s: new can bus", bus_name)
    can_bus = busses[bus_name]
    device = saddr_from_can_id(can_id)
    if can_bus[device] is None:
        can_bus[device] = CanDevice(bus_name, device)
        log.info("%s:%02x new CAN device", bus_name, device)
    out = can_bus[device].add_frame(can_id, len(data), data)
    if isinstance(out, dict):
        print(json.dumps(out))


def file_cook(busses, file_name):
    """Open the named file and process the packets within."""
    try:
        with open(file_name, encoding="latin-1") as fp:
            regex = re.compile(
                r".(\d{10})\.(\d{6}). (.*) (.{8})#(.{16})\n"
            )
            for line in fp:
                if line.startswith("#"):
                    continue
                fragment = regex.match(line).groups()
                when = [int(x) for x in fragment[:2]]
                when[1] /= 1e6
                bus_name = fragment[2]
                can_id = int(fragment[3], 16)
                dlen = len(fragment[4]) // 2
                data = [
                    int(fragment[4][i : i + 2], 16)
                    for i in range(0, len(fragment[4]), 2)
                ]
                data = bytes(data)
                data_ingress(
                    busses, when, bus_name, can_id, data[:dlen]
                )
    except KeyboardInterrupt:
        print()


def socket_glom(busses, bus_name):
    """Attache to namedd CAN bus and handle the received packets."""
    try:
        s = socket.socket(
            socket.AF_CAN, socket.SOCK_RAW, socket.CAN_RAW
        )
        s.bind((bus_name,))
        while True:
            cf, _addr = s.recvfrom(can_frame_size)  # pylint:
            can_id, _len, data = dissect_can_frame(cf)
            when = time.time()
            when = [when // 1, when % 1]
            data_ingress(busses, when, bus_name, can_id, data)
    except KeyboardInterrupt:
        print()


if "__main__" == __name__:
    busses = {}
    if 1:
        file_cook(
            busses,
            Path("/home/jamesb/gpsd/test/nmea2000")
            / "logfile_20140914_365495765_can.log",
            # / "can_bad.log"
        )
    else:
        socket_glom(busses, CAN_BUS)
