"""Support for APRS device tracking."""

import logging
import threading

import voluptuous as vol

from homeassistant.components.device_tracker import PLATFORM_SCHEMA
from homeassistant.const import (
    ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
    CONF_HOST, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME,
    EVENT_HOMEASSISTANT_STOP)
import homeassistant.helpers.config_validation as cv
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 = 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('b/{0}'.format(cs.upper()) for cs in callsigns)


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

    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 = "APRS position ambiguity must be 0-4, not '{0}'.".format(
            posambiguity)
        raise ValueError(message)

    return accuracy


def setup_scanner(hass, config, see, discovery_info=None):
    """Set up the APRS tracker."""
    callsigns = config.get(CONF_CALLSIGNS)
    server_filter = make_filter(callsigns)

    callsign = config.get(CONF_USERNAME)
    password = config.get(CONF_PASSWORD)
    host = config.get(CONF_HOST)
    timeout = config.get(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

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

    _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__()

        import aprslib

        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)
        from aprslib import ConnectionError as AprsConnectionError
        from aprslib import LoginError

        try:
            _LOGGER.info("Opening connection to %s with callsign %s.",
                         self.host, self.callsign)
            self.ais.connect()
            self.start_complete(
                True,
                "Connected to {0} with callsign {1}.".format(
                    self.host, 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)