"""Support for APRS device tracking."""
from __future__ import annotations

from collections.abc import Callable
import logging
import threading

import aprslib
from aprslib import ConnectionError as AprsConnectionError, LoginError
import geopy.distance
import voluptuous as vol

from homeassistant.components.device_tracker import (
    PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
)
from homeassistant.const import (
    ATTR_GPS_ACCURACY,
    ATTR_LATITUDE,
    ATTR_LONGITUDE,
    CONF_HOST,
    CONF_PASSWORD,
    CONF_TIMEOUT,
    CONF_USERNAME,
    EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import slugify

DOMAIN = "aprs"

_LOGGER = logging.getLogger(__name__)

ATTR_ALTITUDE = "altitude"
ATTR_COURSE = "course"
ATTR_COMMENT = "comment"
ATTR_FROM = "from"
ATTR_FORMAT = "format"
ATTR_POS_AMBIGUITY = "posambiguity"
ATTR_SPEED = "speed"

CONF_CALLSIGNS = "callsigns"

DEFAULT_HOST = "rotate.aprs2.net"
DEFAULT_PASSWORD = "-1"
DEFAULT_TIMEOUT = 30.0

FILTER_PORT = 14580

MSG_FORMATS = ["compressed", "uncompressed", "mic-e"]

PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(
    {
        vol.Required(CONF_CALLSIGNS): cv.ensure_list,
        vol.Required(CONF_USERNAME): cv.string,
        vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
        vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
        vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(float),
    }
)


def make_filter(callsigns: list) -> str:
    """Make a server-side filter from a list of callsigns."""
    return " ".join(f"b/{sign.upper()}" for sign in callsigns)


def gps_accuracy(gps, posambiguity: int) -> int:
    """Calculate the GPS accuracy based on APRS posambiguity."""

    pos_a_map = {0: 0, 1: 1 / 600, 2: 1 / 60, 3: 1 / 6, 4: 1}
    if posambiguity in pos_a_map:
        degrees = pos_a_map[posambiguity]

        gps2 = (gps[0], gps[1] + degrees)
        dist_m = geopy.distance.distance(gps, gps2).m

        accuracy = round(dist_m)
    else:
        message = f"APRS position ambiguity must be 0-4, not '{posambiguity}'."
        raise ValueError(message)

    return accuracy


def setup_scanner(
    hass: HomeAssistant,
    config: ConfigType,
    see: Callable[..., None],
    discovery_info: DiscoveryInfoType | None = None,
) -> bool:
    """Set up the APRS tracker."""
    callsigns = config[CONF_CALLSIGNS]
    server_filter = make_filter(callsigns)

    callsign = config[CONF_USERNAME]
    password = config[CONF_PASSWORD]
    host = config[CONF_HOST]
    timeout = config[CONF_TIMEOUT]
    aprs_listener = AprsListenerThread(callsign, password, host, server_filter, see)

    def aprs_disconnect(event):
        """Stop the APRS connection."""
        aprs_listener.stop()

    aprs_listener.start()
    hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, aprs_disconnect)

    if not aprs_listener.start_event.wait(timeout):
        _LOGGER.error("Timeout waiting for APRS to connect")
        return False

    if not aprs_listener.start_success:
        _LOGGER.error(aprs_listener.start_message)
        return False

    _LOGGER.debug(aprs_listener.start_message)
    return True


class AprsListenerThread(threading.Thread):
    """APRS message listener."""

    def __init__(
        self, callsign: str, password: str, host: str, server_filter: str, see
    ):
        """Initialize the class."""
        super().__init__()

        self.callsign = callsign
        self.host = host
        self.start_event = threading.Event()
        self.see = see
        self.server_filter = server_filter
        self.start_message = ""
        self.start_success = False

        self.ais = aprslib.IS(
            self.callsign, passwd=password, host=self.host, port=FILTER_PORT
        )

    def start_complete(self, success: bool, message: str):
        """Complete startup process."""
        self.start_message = message
        self.start_success = success
        self.start_event.set()

    def run(self):
        """Connect to APRS and listen for data."""
        self.ais.set_filter(self.server_filter)

        try:
            _LOGGER.info(
                "Opening connection to %s with callsign %s", self.host, self.callsign
            )
            self.ais.connect()
            self.start_complete(
                True, f"Connected to {self.host} with callsign {self.callsign}."
            )
            self.ais.consumer(callback=self.rx_msg, immortal=True)
        except (AprsConnectionError, LoginError) as err:
            self.start_complete(False, str(err))
        except OSError:
            _LOGGER.info(
                "Closing connection to %s with callsign %s", self.host, self.callsign
            )

    def stop(self):
        """Close the connection to the APRS network."""
        self.ais.close()

    def rx_msg(self, msg: dict):
        """Receive message and process if position."""
        _LOGGER.debug("APRS message received: %s", str(msg))
        if msg[ATTR_FORMAT] in MSG_FORMATS:
            dev_id = slugify(msg[ATTR_FROM])
            lat = msg[ATTR_LATITUDE]
            lon = msg[ATTR_LONGITUDE]

            attrs = {}
            if ATTR_POS_AMBIGUITY in msg:
                pos_amb = msg[ATTR_POS_AMBIGUITY]
                try:
                    attrs[ATTR_GPS_ACCURACY] = gps_accuracy((lat, lon), pos_amb)
                except ValueError:
                    _LOGGER.warning(
                        "APRS message contained invalid posambiguity: %s", str(pos_amb)
                    )
            for attr in (ATTR_ALTITUDE, ATTR_COMMENT, ATTR_COURSE, ATTR_SPEED):
                if attr in msg:
                    attrs[attr] = msg[attr]

            self.see(dev_id=dev_id, gps=(lat, lon), attributes=attrs)
