# -*- coding: utf-8 -*-
"""
If you call sender.py from the command line, it will send 3 test packets.
See argparse for options.

Otherwise, this module is loaded to provide the ServerSender class.
"""

from __future__ import print_function

import socket
import argparse
import time
import numpy as np
from contextlib import closing
from Crypto.Cipher import AES

from auxiliaries import set_verbosity, Config, PublicKey
from globalvalues import DEFAULT_HOSTNAME, DEFAULT_SENDER_MODE
from globalvalues import DEFAULT_UDP_PORT, DEFAULT_TCP_PORT
from globalvalues import TESTING_UDP_PORT, TESTING_TCP_PORT
from globalvalues import DEFAULT_CONFIG, DEFAULT_PUBLICKEY, DEFAULT_AESKEY
from globalvalues import NETWORK_LED_BLINK_PERIOD_S

TCP_TIMEOUT = 5
D3S_PREPEND_STR = '{:05d}'


class ServerSender(object):
    """
    Provides ServerSender.send_cpm() for sending UDP packets to the DoseNet
    server.
    """

    def __init__(self,
                 manager=None,
                 address=DEFAULT_HOSTNAME,
                 port=None,
                 config=None,
                 publickey=None,
                 aes=None,
                 verbosity=1,
                 logfile=None,
                 mode=None,
                 ):
        """
        network_status, config, publickey, aes loaded from manager
          if not provided.
        address and port take system defaults, although without config and
          publickey, address and port will not be used.
        """
        self.v = verbosity
        if manager and logfile is None:
            set_verbosity(self, logfile=manager.logfile)
        else:
            set_verbosity(self, logfile=logfile)

        self.address = address
        self.handle_input(
            manager, mode, port, config, publickey, aes)

    def handle_input(
            self, manager, mode, port, config, publickey, aes):

        # TODO: this stuff is messy. Is there a cleaner way using exceptions?
        if manager is None:
            self.vprint(1, 'ServerSender starting without Manager object')
        self.manager = manager

        try:
            if mode is None:
                self.mode = DEFAULT_SENDER_MODE
            elif mode == 'udp_test':
                self.mode = 'udp_test'
            elif mode == 'tcp_test':
                self.mode = 'tcp_test'
            elif mode.lower() == 'udp':
                self.mode = 'udp'
            elif mode.lower() == 'tcp':
                self.mode = 'tcp'
            else:
                raise RuntimeError(
                    'Invalid ServerSender mode (choose TCP or UDP)')
        except AttributeError:
            # if mode is not a string or None, then mode.lower() raises this
            raise RuntimeError('Invalid ServerSender mode (choose TCP or UDP)')

        if self.mode == 'udp':
            if port is None:
                self.port = DEFAULT_UDP_PORT
            else:
                self.port = port
            self.vprint(3, 'ServerSender using UDP for {}:{}'.format(
                self.address, self.port))
        elif self.mode == 'tcp':
            if port is None:
                self.port = DEFAULT_TCP_PORT
            else:
                self.port = port
            self.vprint(3, 'ServerSender using TCP for {}:{}'.format(
                self.address, self.port))
        elif self.mode == 'udp_test':
            if port is None:
                self.port = TESTING_UDP_PORT
            else:
                self.port = port
            self.vprint(3, 'ServerSender using UDP for {}:{}'.format(
                self.address, self.port))
        elif self.mode == 'tcp_test':
            if port is None:
                self.port = TESTING_TCP_PORT
            else:
                self.port = port
            self.vprint(3, 'ServerSender using TCP for {}:{}'.format(
                self.address, self.port))

        if config is None:
            if manager is None:
                self.vprint(1, 'ServerSender starting without config file')
                self.config = None
            else:
                self.config = manager.config
        else:
            self.config = config

        if publickey is None:
            if manager is None:
                self.vprint(1, 'ServerSender starting without publickey file')
                self.encrypter = None
            elif manager.publickey is None:
                self.vprint(1, 'ServerSender starting without publickey file')
                self.encrypter = None
            else:
                self.encrypter = manager.publickey.encrypter
        else:
            self.encrypter = publickey.encrypter

        if aes is None:
            if manager is None:
                self.vprint(2, 'ServerSender starting without AES key')
                self.aes = None
            elif manager.aes is None:
                self.vprint(2, 'ServerSender starting without AES key')
                self.aes = None
            else:
                self.aes = manager.aes
        else:
            self.aes = aes

    def construct_packet(self, sensor_type, cpm, cpm_error, error_code=0):
        """
        Construct the raw packet string. (basically copied from old code)

        hash,ID,cpm,cpm_error,error_code
        """

        try:
            raw_packet = ','.join(
                [str(self.config.hash),
                 str(self.config.ID),
                 str(sensor_type),
                 str(cpm),
                 str(cpm_error),
                 str(error_code)]
            )
        except AttributeError:      # on self.config.hash
            raise MissingFile('Missing or broken Config object')
        else:
            self.vprint(3, 'Constructed packet')
            return raw_packet

    def construct_packet_new(self, sensor_type, timestamp, cpm, cpm_error, error_code=0):
        """
        New protocol version of construct packet.

        hash,ID,timestamp,cpm,cpm_error,error_code
        """

        try:
            raw_packet = ','.join(
                [str(self.config.hash),
                 str(self.config.ID),
                 str(sensor_type),
                 str(timestamp),
                 str(cpm),
                 str(cpm_error),
                 str(error_code)]
            )
        except AttributeError:      # on self.config.hash
            raise MissingFile('Missing or broken Config object')
        else:
            self.vprint(3, 'Constructed packet')
            return raw_packet

    def construct_packet_new_D3S(self, sensor_type, timestamp, spectra, error_code=0):
        """
        TCP version of construct packet.
        """

        # convert spectra to a string representation that won't interfere with
        #   injector's parsing (no commas)
        spectra_str = str(spectra).replace(',', ';')
        try:
            raw_packet = ','.join(
                [str(self.config.hash),
                 str(self.config.ID),
                 str(sensor_type),
                 str(timestamp),
                 spectra_str,
                 str(error_code)]
            )
        except AttributeError:      # on self.config.hash
            raise MissingFile('Missing or broken Config object')
        else:
            self.vprint(3, 'Constructed packet')
            return raw_packet

    def construct_packet_new_AQ(self, sensor_type, timestamp, average_data, error_code=0):
        avgdata_str = str(average_data).replace(',', ';')
        try:
            raw_packet = ','.join(
                [str(self.config.hash),
                 str(self.config.ID),
                 str(sensor_type),
                 str(timestamp),
                 avgdata_str,
                 str(error_code)]
            )
        except AttributeError:      # on self.config.hash
            raise MissingFile('Missing or broken Config object')
        else:
            self.vprint(3, 'Constructed packet')
            return raw_packet

    def construct_packet_new_CO2(self, sensor_type, timestamp, average_data, error_code=0):
        avgdata_str = str(average_data).replace(',', ';')
        try:
            raw_packet = ','.join(
                [str(self.config.hash),
                 str(self.config.ID),
                 str(sensor_type),
                 str(timestamp),
                 avgdata_str,
                 str(error_code)]
            )
        except AttributeError:
            raise MissingFile('Missing or broken Config object')
        else:
            self.vprint(3, 'Constructed packet')
            return raw_packet

    def construct_packet_new_weather(self, sensor_type, timestamp, average_data, error_code=0):
        avgdata_str = str(average_data).replace(',', ';')
        try:
            raw_packet = ','.join(
                [str(self.config.hash),
                 str(self.config.ID),
                 str(sensor_type),
                 str(timestamp),
                 avgdata_str,
                 str(error_code)]
            )
        except AttributeError:
            raise MissingFile('Missing or broken Config object')
        else:
            self.vprint(3, 'Constructed packet')
            return raw_packet

    def construct_log_packet(self, msg_code, msg_text):
        """
        Send a message to be recorded in the server log database.

        hash,ID,"LOG",msg_code,msg_text
        """

        if not isinstance(msg_code, int):
            raise TypeError('msg_code should be an int')
        try:
            raw_packet = ','.join(
                [str(self.config.hash),
                 str(self.config.ID),
                 'LOG',
                 str(msg_code),
                 str(msg_text)]
            )
        except AttributeError:      # on self.config.hash
            raise MissingFile('Missing or broken Config object')
        else:
            self.vprint(3, 'Constructed log packet')
            return raw_packet

    def encrypt_packet(self, raw_packet):
        """Encrypt the raw packet"""

        self.vprint(3, 'Encrypting packet: {}'.format(raw_packet))
        try:
            encrypted = self.encrypter.encrypt_message(raw_packet)[0]
        except AttributeError:
            raise MissingFile('Missing or broken PublicKey object')
        else: