# packet.py - recognize GPS packet types
# This code is generated by scons.  Do not hand-hack it!
#
# This file is Copyright by the GPSD project
# SPDX-License-Identifier: BSD-2-Clause
#
# This code runs compatibly under Python 2 and 3.x for x >= 2.
# Preserve this property!
#
# -*- coding: utf-8 -*-
"""Recognize GPS packets using the lexer from libgpsd and ctypes.

Directly calling the  Device() class with a file handle,  returns a
new instance with three methods:
    get() returns a tuple consisting of the integer packet type and
string packet value.  At the end of the stream it returns (-1, "").
    reset() resets the packet-lexer to its initial state.
    register_report() accepts a callback for debug message reporting.
The callback will get two arguments: the error level of the message;
and the message itself.

Deprecated APIs:

Lexer() FFI interface is unused, and deprecated October 2025.
It will be removed.

The new() function returns a new packet-lexer instance.  Lexer instances
have two methods:
    get() takes a file handle argument and returns a tuple consisting of
the integer packet type and string packet value.  At the end of the stream
it returns (-1, "").
    reset() resets the packet-lexer to its initial state.
    The module also has a register_report() function that accepts a callback
for debug message reporting.  The callback will get two arguments: the error
level of the message; and the message itself.
"""
from __future__ import absolute_import
import ctypes
import ctypes.util
import os.path
import sys

import gps  # For gps.__path__
import gps.misc


# Packet types and  Logging levels extracted from gpsd.h
MAX_PACKET_LENGTH = 9216
COMMENT_PACKET = 0
NMEA_PACKET = 1
AIVDM_PACKET = 2
GARMINTXT_PACKET = 3
SIRF_PACKET = 4
ZODIAC_PACKET = 5
TSIP_PACKET = 6
EVERMORE_PACKET = 7
ITALK_PACKET = 8
GARMIN_PACKET = 9
NAVCOM_PACKET = 10
UBX_PACKET = 11
SUPERSTAR2_PACKET = 12
ONCORE_PACKET = 13
GEOSTAR_PACKET = 14
NMEA2000_PACKET = 15
GREIS_PACKET = 16
SKY_PACKET = 17
ALLYSTAR_PACKET = 18
CASIC_PACKET = 19
IS_PACKET = 20
MAX_GPSPACKET_TYPE = 20
RTCM2_PACKET = 21
RTCM3_PACKET = 22
JSON_PACKET = 23
SPARTN_PACKET = 24
PACKET_TYPES = 25
LOG_SHOUT = 0
LOG_WARN = 1
LOG_CLIENT = 2
LOG_INF = 3
LOG_PROG = 4
LOG_IO = 5
LOG_DATA = 6
LOG_SPIN = 7
LOG_RAW = 8
LOG_RAW1 = 9
LOG_RAW2 = 10
ISGPS_ERRLEVEL_BASE = LOG_RAW


class PacketLibraryNotFoundError(Exception):
    """Error loading packet library."""

    pass


def importado():
    """
    Load the packet library or throw a PacketLibraryNotFoundError trying.

    See below for the search list.
    find_library() can look in: LD_LIBRARY_PATH, DYLD_LIBRARY_PATH, /lib,
    /usr/lib, /usr/local/lib, and $home/lib returning the library handle.
    """
    # Form full paths to candidates
    packet_paths = [
        os.path.join(os.path.abspath(x), "libgpsdpacket.32.0.0.dylib")
        for x in [
            # First, look in the directory containing this package "gps",
            # possibly following a symlink in the process. Scons places
            # the file there in the build tree.  We expect it to fail
            # when running the installed version rather than say
            # `scons check`.
            os.path.dirname(os.path.realpath(gps.__path__[0])),

            # Next, look in the library install directory. This is the
            # expected location when running the installed version.
            os.path.realpath("/opt/local/lib"),
        ]
    ]

    # Finally, try find_library().

    # find_library() looks for a bare library name, using dlopen()
    # May, or may not, return a full path.  Either way, use it as is.
    #
    # linux dlopen() looks in:
    #  LD_LIBRARY_PATH,
    #  paths in /etc/ld.so.cache,
    #  /lib(64) and /usr/lib(64)
    #
    # macOS dlopen() looks in:
    #  LD_LIBRARY_PATH,
    #  DYLD_LIBRARY_PATH,
    #  current working directory,
    #  DYLD_FALLBACK_LIBRARY_PATH (default: $HOME/lib:/usr/local/lib:/usr/lib)
    # Note that some recent macOS versions have stopped honoring
    #  *_LIBRARY_PATH, for security reasons.
    #
    # Linux:
    #   find_library() does not usually return a full path.
    #   LoadLibrary() can use a full path, or whatever find_library() returned.
    #
    # macOS:
    #   find_library() returns a full path, unless lib is in the current
    #   directory, then it returns a partial path. But LoadLibrary() always
    #   needs a full path
    #
    packet_path = ctypes.util.find_library('gpsdpacket')
    if packet_path:
        packet_paths.append(packet_path)

    for packet_path in packet_paths:
        try:
            if sys.flags.verbose:
                sys.stderr.write('INFO: try_packet_lib: %s\n' % packet_path)
            lib = ctypes.cdll.LoadLibrary(packet_path)
            # Get the library version from the library.
            wrap_version = "3.27.1"
            flib_version = ctypes.c_char_p.in_dll(lib, "gpsd_version")
            flib_version = gps.polystr(flib_version.value)
            if wrap_version != flib_version:
                sys.stderr.write(
                    "ERROR: got library version %s, expected %s from %s\n"
                    % (flib_version, wrap_version, packet_path)
                )
                sys.exit(1)
            return lib
        except OSError:
            pass

    raise PacketLibraryNotFoundError("Can't find packet library")


_lexer_global = None
_packet = importado()

_buffer_size = ctypes.c_size_t.in_dll(_packet, "fvi_size_buffer").value

REPORTER = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_char_p)


class GpsdErrOutT(ctypes.Structure):
    # pylint: disable-msg=R0903
    """Hold data used to set the logging callback, etc."""

    _fields_ = [('debug', ctypes.c_int),
                ('report', REPORTER),
                ('label', ctypes.c_char_p)]


class lexer_t(ctypes.Structure):
    # pylint: disable-msg=R0903
    """Hold data and help transfer data to/from a Lexer instance.

Careful: must match gps_lexer_r in include/gpsd.h

Held data notably includes packet type,
length, the out buffer, and the counter.
"""

    _fields_ = [
        ('packet_type', ctypes.c_int),
        ('type_mask', ctypes.c_long),
        ('state', ctypes.c_uint),
        ('length', ctypes.c_size_t),
        ('inbuffer', ctypes.c_ubyte * _buffer_size),
        ('inbuflen', ctypes.c_size_t),
        ('inbufptr', ctypes.c_char_p),
        ('outbuffer', ctypes.c_ubyte * _buffer_size),
        ('outbuflen', ctypes.c_size_t),
        ('char_counter', ctypes.c_ulong),
        ('retry_counter', ctypes.c_ulong),
        ('counter', ctypes.c_uint),
        ('errout', GpsdErrOutT),
    ]


C_DEVICE_P = ctypes.c_void_p
C_LEXER_P = ctypes.POINTER(lexer_t)
_packet.packet_reset.argtypes = [C_LEXER_P]


def new():
    """Create new Lexer instance."""
    global _lexer_global
    ret = Lexer()
    if not _lexer_global:  # Always happens on pass 1
        _lexer_global = ret
    return ret


def register_report(reporter):
    """Set logging callback for the Lexer instance.

    The callback must be a callable object expecting
    an int log level and a str message.
    """
    global _lexer_global
    if not _lexer_global:  # Always happens before pass 1
        raise NameError('new() not called.')
    _lexer_global.register_report(reporter)


class Lexer():
    """Fetch sentences from a file handle.

    (May potentially change devices mid-stream.)
    Fetch a single packet from a file handle.
    """

    pointer = None

    def __init__(self):
        """Allocate and configure a Device instance."""
        _packet.ffi_Lexer_init.restype = C_LEXER_P
        _packet.packet_get.restype = ctypes.c_int
        _packet.packet_get.argtypes = [ctypes.c_int, C_LEXER_P]
        self.pointer = _packet.ffi_Lexer_init()
        if not self.pointer:
            raise ValueError

    def __del__(self):
        """Link into the tear down."""
        _packet.ffi_Lexer_fini.restype = None
        _packet.ffi_Lexer_fini.argtypes = [C_LEXER_P]
        _packet.ffi_Lexer_fini(self.pointer)

    def get(self, file_handle):
        """Get a packet from the device.

        Returns a tuple of the packets length,
        type, contents and position in stream.

        Deprecated in 2023; use packet_get1() instead.
        """
        length = _packet.packet_get(file_handle, self.pointer)
        lexer = self.pointer.contents
        packet = bytearray(lexer.outbuffer[:lexer.outbuflen])
        return (length,
                lexer.packet_type,
                bytes(packet),
                lexer.char_counter)

    def register_report(self, reporter):
        """Set logging callback for Device instance.

        The callback must be a callable object expecting
        an int log level and a str message.
        """
        if callable(reporter):
            lexer__instance = self.pointer.contents
            lexer__instance.errout.report = REPORTER(reporter)

    def reset(self):
        """Reset the lexer to ground state."""
        _packet.packet_reset(self.pointer)


class Device():
    """Fetch sentences from a file handle."""

    pointer = None
    lexer_pointer = None

    def __init__(self, file_handle):
        """Allocate and configure a Device instance."""
        _packet.ffi_Device_init.restype = C_DEVICE_P
        _packet.ffi_Device_init.argtypes = [ctypes.c_int]
        _packet.ffi_Device_fini.restype = None
        _packet.ffi_Device_fini.argtypes = [C_DEVICE_P]
        _packet.ffi_Device_Lexer.restype = C_LEXER_P
        _packet.ffi_Device_Lexer.argtypes = [C_DEVICE_P]
        _packet.packet_get1.restype = ctypes.c_int
        _packet.packet_get1.argtypes = [C_DEVICE_P]

        self.pointer = _packet.ffi_Device_init(file_handle)
        if not self.pointer:
            raise ValueError
        self.lexer_pointer = _packet.ffi_Device_Lexer(self.pointer)
        if not self.lexer_pointer:
            raise ValueError

    def __del__(self):
        """Link into the tear down."""
        _packet.ffi_Device_fini(self.pointer)

    def get(self):
        """Get a packet from the device.

        Returns a tuple of the packets length,
        type, contents and position in stream.
        """
        length = _packet.packet_get1(self.pointer)
        lexer_instance = self.lexer_pointer.contents
        packet = bytearray(lexer_instance.outbuffer[:lexer_instance.outbuflen])
        return (length,
                lexer_instance.packet_type,
                bytes(packet),
                lexer_instance.char_counter)

    def register_report(self, reporter):
        """Set logging callback for Device instance.

        The callback must be a callable object expecting
        an int log level and a str message.
        """
        if callable(reporter):
            lexer_instance = self.lexer_pointer.contents
            lexer_instance.errout.report = REPORTER(reporter)

    def reset(self):
        """Reset the lexer to ground state."""
        _packet.packet_reset(self.lexer_pointer)

# vim: set expandtab shiftwidth=4
