# aprslib - Python library for working with APRS
# Copyright (C) 2013-2014 Rossen Georgiev
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

"""
IS class is used for connection to APRS-IS network
"""
import socket
import select
import time
import logging

from aprslib import __version__, string_type, is_py3
from aprslib.parsing import parse
from aprslib.packets.base import APRSPacket
from aprslib.exceptions import (
    GenericError,
    ConnectionDrop,
    ConnectionError,
    LoginError,
    ParseError,
    UnknownFormat,
    )

__all__ = ['IS']

logging.addLevelName(11, "ParseError")
logging.addLevelName(9, "UnknownFormat")


class IS(object):
    """
    The IS class is used to connect to aprs-is network and listen to the stream
    of packets. You can either run them through aprs.parse() or get them in raw
    form.

    Note: sending of packets is not supported yet

    """
    def __init__(self, callsign, passwd="-1", host="rotate.aprs.net", port=10152, skip_login=False):
        """
        callsign        - used when login in
        passwd          - for verification, or "-1" if only listening
        Host & port     - aprs-is server
        """

        self.logger = logging.getLogger("%s.%s" % (__name__, self.__class__.__name__))
        self._parse = parse

        self.set_server(host, port)
        self.set_login(callsign, passwd, skip_login)

        self.sock = None
        self.filter = ""  # default filter, everything

        self._connected = False
        self.buf = b''

    def _sendall(self, text):
        if is_py3:
            text = text.encode('utf-8')
        self.sock.sendall(text)

    def set_filter(self, filter_text):
        """
        Set a specified aprs-is filter for this connection
        """
        self.filter = filter_text

        self.logger.info("Setting filter to: %s", self.filter)

        if self._connected:
            self._sendall("#filter %s\r\n" % self.filter)

    def set_login(self, callsign, passwd="-1", skip_login=False):
        """
        Set callsign and password
        """
        self.__dict__.update(locals())

    def set_server(self, host, port):
        """
        Set server ip/host and port to use
        """
        self.server = (host, port)

    def connect(self, blocking=False, retry=30):
        """
        Initiate connection to APRS server and attempt to login

        blocking = False     - Should we block until connected and logged-in
        retry = 30           - Retry interval in seconds
        """

        if self._connected:
            return

        while True:
            try:
                self._connect()
                if not self.skip_login:
                    self._send_login()
                break
            except (LoginError, ConnectionError):
                if not blocking:
                    raise

            self.logger.info("Retrying connection is %d seconds." % retry)
            time.sleep(retry)

    def close(self):
        """
        Closes the socket
        Called internally when Exceptions are raised
        """

        self._connected = False
        self.buf = b''

        if self.sock is not None:
            self.sock.close()

    def sendall(self, line):
        """
        Send a line, or multiple lines sperapted by '\\r\\n'
        """
        if isinstance(line, APRSPacket):
            line = str(line)
        elif not isinstance(line, string_type):
            raise TypeError("Expected line to be str or APRSPacket, got %s", type(line))
        if not self._connected:
            raise ConnectionError("not connected")

        if line == "":
            return

        line = line.rstrip("\r\n") + "\r\n"

        try:
            self.sock.setblocking(1)
            self.sock.settimeout(5)
            self._sendall(line)
        except socket.error as exp:
            self.close()
            raise ConnectionError(str(exp))

    def consumer(self, callback, blocking=True, immortal=False, raw=False):
        """
        When a position sentence is received, it will be passed to the callback function

        blocking: if true (default), runs forever, otherwise will return after one sentence
                  You can still exit the loop, by raising StopIteration in the callback function

        immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions
                  if false (default), consumer will return

        raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse()
        """

        if not self._connected:
            raise ConnectionError("not connected to a server")

        line = b''

        while True:
            try:
                for line in self._socket_readlines(blocking):
                    if line[0:1] != b'#':
                        if raw:
                            callback(line)
                        else:
                            callback(self._parse(line))
                    else:
                        self.logger.debug("Server: %s", line.decode('utf8'))
            except ParseError as exp:
                self.logger.log(11, "%s\n    Packet: %s", exp.message, exp.packet)
            except UnknownFormat as exp:
                self.logger.log(9, "%s\n    Packet: %s", exp.message, exp.packet)
            except LoginError as exp:
                self.logger.error("%s: %s", exp.__class__.__name__, exp.message)
            except (KeyboardInterrupt, SystemExit):
                raise
            except (ConnectionDrop, ConnectionError):
                self.close()

                if not immortal:
                    raise
                else:
                    self.connect(blocking=blocking)
                    continue
            except GenericError:
                pass
            except StopIteration:
                break
            except:
                self.logger.error("APRS Packet: %s", line)
                raise

            if not blocking:
                break

    def _open_socket(self):
        """
        Creates a socket
        """
        self.sock = socket.create_connection(self.server, 15)

    def _connect(self):
        """
        Attemps connection to the server
        """

        self.logger.info("Attempting connection to %s:%s", self.server[0], self.server[1])

        try:
            self._open_socket()

            peer = self.sock.getpeername()

            self.logger.info("Connected to %s", str(peer))

            # 5 second timeout to receive server banner
            self.sock.setblocking(1)
            self.sock.settimeout(5)

            self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)

            banner = self.sock.recv(512)
            if is_py3:
                banner = banner.decode('latin-1')

            if banner[0] == "#":
                self.logger.debug("Banner: %s", banner.rstrip())
            else:
                raise ConnectionError("invalid banner from server")

        except ConnectionError as e:
            self.logger.error(str(e))
            self.close()
            raise
        except (socket.error, socket.timeout) as e:
            self.close()

            self.logger.error("Socket error: %s" % str(e))
            if str(e) == "timed out":
                raise ConnectionError("no banner from server")
            else:
                raise ConnectionError(e)

        self._connected = True

    def _send_login(self):
        """
        Sends login string to server
        """
        login_str = "user {0} pass {1} vers aprslib {3}{2}\r\n"
        login_str = login_str.format(
            self.callsign,
            self.passwd,
            (" filter " + self.filter) if self.filter != "" else "",
            __version__
            )

        self.logger.info("Sending login information")

        try:
            self._sendall(login_str)
            self.sock.settimeout(5)
            test = self.sock.recv(len(login_str) + 100)
            if is_py3:
                test = test.decode('latin-1')
            test = test.rstrip()

            self.logger.debug("Server: %s", test)

            _, _, callsign, status, _ = test.split(' ', 4)

            if callsign == "":
                raise LoginError("Server responded with empty callsign???")
            if callsign != self.callsign:
                raise LoginError("Server: %s" % test)
            if status != "verified," and self.passwd != "-1":
                raise LoginError("Password is incorrect")

            if self.passwd == "-1":
                self.logger.info("Login successful (receive only)")
            else:
                self.logger.info("Login successful")

        except LoginError as e:
            self.logger.error(str(e))
            self.close()
            raise
        except:
            self.close()
            self.logger.error("Failed to login")
            raise LoginError("Failed to login")

    def _socket_readlines(self, blocking=False):
        """
        Generator for complete lines, received from the server
        """
        try:
            self.sock.setblocking(0)
        except socket.error as e:
            self.logger.error("socket error when setblocking(0): %s" % str(e))
            raise ConnectionDrop("connection dropped")

        while True:
            short_buf = b''
            newline = b'\r\n'

            select.select([self.sock], [], [], None if blocking else 0)

            try:
                short_buf = self.sock.recv(4096)

                # sock.recv returns empty if the connection drops
                if not short_buf:
                    self.logger.error("socket.recv(): returned empty")
                    raise ConnectionDrop("connection dropped")
            except socket.error as e:
                # ignore error when blocking=false, and we attempt to read empty socket
                if ("Resource temporarily unavailable" in str(e)
                   and not blocking
                   and len(self.buf) == 0):
                        break
                else:
                    self.logger.error("socket error on recv(): %s" % str(e))

            self.buf += short_buf

            while newline in self.buf:
                line, self.buf = self.buf.split(newline, 1)

                yield line
