#!/usr/bin/env python3
"""
ZeroMQ lazy pirate server for I2C sensors.
Binds REQ socket to tcp://*:5555
"""

import argparse
import hashlib
import logging
import sys
import time

from RPi import GPIO
import zmq

import mmcb.configure_environment as ce
from mmcb.peltier import Peltier


##############################################################################
# data structures
##############################################################################


class PeltierGroup:
    peltier_config = {
        1: [1, (1, 2), 12],
        2: [1, (3, 4), 20],
        3: [2, (1, 2), 16],
        4: [2, (3, 4),  6],
    }
    enabled_peltier_devices = {1, 2, 3, 4}
    peltier_devices = set()

    def __init__(self):
        for device in self.enabled_peltier_devices:
            board, (relay_0, relay_1), gpio = self.peltier_config[device]
            GPIO.setup(gpio, GPIO.OUT)
            self.peltier_devices.add(Peltier(device, board, relay_0, relay_1, gpio))

    def __str__(self):
        """
        print all devices
        """
        return '\n'.join(
            str(device)
            for device in sorted(self.peltier_devices)
        )

    def status(self, device_selection):
        """
        print selected, or all devices
        """
        try:
            parts = device_selection.split(',')
        except ValueError:
            # print status of all devices
            whole_group = True
            device_selection = []
        else:
            try:
                device_selection = [int(d) for d in parts]
            except ValueError:
                # print status of all devices
                whole_group = True
                device_selection = []
            else:
                # print selected devices
                whole_group = False

        return '\n'.join(
            str(pdev)
            for pdev in sorted(self.peltier_devices)
            if whole_group or pdev.device in device_selection
        )


##############################################################################
# command line option handler
##############################################################################


def check_arguments():
    """
    Handle command line options.

    --------------------------------------------------------------------------
    args : none
    --------------------------------------------------------------------------
    returns : none
    --------------------------------------------------------------------------
    """
    parser = argparse.ArgumentParser(
        description=(
            'Server that offers access to I2C sensors and Peltier devices '
            '(via GPIO pins) attached to a Raspberry Pi via ZeroMQ '
            '(port 5555.)'
        )
    )
    parser.parse_args()


##############################################################################
# communications
##############################################################################


def format_response(readings):
    """Format the sensor readings into a response string."""
    losr = len(readings)
    if losr == 1:
        return str(next(iter(readings.values())))
    if losr > 1:
        return ','.join(f'{k}={v}' for k, v in readings.items())

    return ''


def package_response(txt):
    """
    Adds checksum to text and returns string as bytes.

    ---------------------------------------------------------------------------
    args
        txt : str
    ---------------------------------------------------------------------------
    returns : bytes
    ---------------------------------------------------------------------------
    """
    checksum = hashlib.sha1(txt.encode()).hexdigest()
    return f'{txt}:{checksum}'.encode()


def verify_message(message):
    """
    Verify the checksum of the server response.
    """
    sender_message_b, _, sender_checksum_b = message.rpartition(b':')
    sender_checksum = sender_checksum_b.decode('utf-8')
    local_checksum = hashlib.sha1(sender_message_b).hexdigest()
    return local_checksum == sender_checksum, sender_message_b.decode('utf-8')


def process_sensor_read(testenv, client_sensors):
    """
    """
    user_sensor_parameter_pairs = [
        sensor_param.strip()
        for sensor_param in client_sensors.split(',')
    ]
    logging.info('sensor-parameter pairs requested %s', user_sensor_parameter_pairs)

    try:
        measurements = {
            k: v
            for k, v in testenv.read_all_sensors(*user_sensor_parameter_pairs).items()
            if k in user_sensor_parameter_pairs
        }
    except IndexError:
        # no command line options given, so get everything
        ordered_sensor_readings = testenv.read_all_sensors()
    else:
        # maintain user-supplied argument order
        ordered_sensor_readings = dict(
            sorted(
                measurements.items(),
                key=lambda k: user_sensor_parameter_pairs.index(k[0])
            )
        )

    if not ordered_sensor_readings:
        logging.warning('no reading')

    logging.info(ordered_sensor_readings)

    txt = format_response(ordered_sensor_readings)
    response = package_response(txt)
    logging.info('response %s', response)

    return response


def process_peltier_read(peltier_group, data):
    """
    Get status information about Peltier devices

    ---------------------------------------------------------------------------
    args
        peltier_group : class PeltierGroup
        data : str
    ---------------------------------------------------------------------------
    returns
    ---------------------------------------------------------------------------
    """
    txt = peltier_group.status(data)
    response = package_response(txt)
    logging.info('response %s', response)

    return response


##############################################################################
# main
##############################################################################


def main():
    """
    ZeroMQ lazy pirate server for I2C sensors.
    """
    check_arguments()

    testenv = ce.TestEnvironment('~/config.txt')

    GPIO.setwarnings(False)
    GPIO.setmode(GPIO.BCM)

    peltier_group = PeltierGroup()

    logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.INFO)
    logging.Formatter.converter = time.gmtime

    context = zmq.Context()
    server = context.socket(zmq.REP)
    server.bind("tcp://*:5555")

    while True:
        request = server.recv()
        is_valid, message = verify_message(request)

        if not is_valid:
            logging.warning("checksum mismatch (%s)", request)
            server.send('fail'.encode())
            continue

        try:
            resource, instruction, data = message.split(':')
        except ValueError:
            server.send('fail'.encode())
            continue

        if resource == 'i2c' and instruction == 'read':
            response = process_sensor_read(testenv, data)
        elif resource == 'gpio' and instruction == 'read':
            response = process_peltier_read(peltier_group, data)
        else:
            response = package_response('not implemented')

        server.send(response)


##############################################################################
if __name__ == '__main__':
    sys.exit(main())
