# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import logging
import serial
import subprocess
from os.path import isfile
from os import listdir
from threading import Thread, Lock

from odoo import http

from odoo.addons.hw_proxy.controllers import main as hw_proxy

_logger = logging.getLogger(__name__)

DRIVER_NAME = 'fiscal_data_module'

class Blackbox(Thread):
    def __init__(self):
        Thread.__init__(self)
        self.blackbox_lock = Lock()
        self.set_status('connecting')
        self.device_path = self._find_device_path_by_probing()

    def set_status(self, status, messages=[]):
        self.status = {
            'status': status,
            'messages': messages
        }

    def get_status(self):
        return self.status

    # There is no real way to find a serial device, all you can really
    # find is the name of the serial to usb interface, which in the
    # case of the blackbox is not defined because it doesn't always
    # come with it's own interface (eg. Retail Cleancash SC-B). So, in
    # order to differentiate between other devices like this, what
    # we'll do is probe every serial device with an FDM status
    # request. The first device to give an answer that makes sense
    # wins.
    def _find_device_path_by_probing(self):
        with hw_proxy.rs232_lock:
            path = "/dev/serial/by-path/"
            probe_message = self._wrap_low_level_message_around("S000")

            try:
                devices = listdir(path)
            except OSError:
                _logger.warning(path + " doesn't exist")
                self.set_status("disconnected", ["No RS-232 device (or emulated ones) found"])
            else:
                for device in devices:
                    if device in hw_proxy.rs232_devices:
                        continue
                    path_to_device = path + device
                    _logger.debug("Probing " + device)

                    if self._send_to_blackbox(probe_message, 21, path_to_device, just_wait_for_ack=True):
                        _logger.info(device + " will be used as the blackbox")
                        self.set_status("connected", [device])
                        hw_proxy.rs232_devices[device] = DRIVER_NAME
                        return path_to_device

                _logger.warning("Blackbox could not be found")
                self.set_status("disconnected", ["Couldn't find the Fiscal Data Module"])
                return ""

    def _lrc(self, msg):
        lrc = 0

        for character in msg:
            byte = ord(character)
            lrc = (lrc + byte) & 0xFF

        lrc = ((lrc ^ 0xFF) + 1) & 0xFF

        return lrc

    def _wrap_low_level_message_around(self, high_level_message):
        bcc = self._lrc(high_level_message)
        high_level_message_bytes = (ord(b) for b in high_level_message)

        low_level_message = bytearray()
        low_level_message.append(0x02)
        low_level_message.extend(high_level_message_bytes)
        low_level_message.append(0x03)
        low_level_message.append(bcc)

        return low_level_message

    def _send_and_wait_for_ack(self, packet, serial):
        ack = 0
        MAX_RETRIES = 1 # no more than 9

        while ack != 0x06 and int(chr(packet[4])) < MAX_RETRIES:
            serial.write(packet)
            ack = serial.read(1)

            # This violates the principle that we do high level
            # client-side and low level posbox-side but the retry
            # counter is always in a fixed position in the high level
            # message so it's safe to do it. Also it would be a pain
            # to have to throw this all the way back to js just so it
            # can increment the retry counter and then try again.
            packet[4] += 1

            if ack:
                ack = ord(ack)
            else:
                _logger.warning("did not get ACK, retrying...")
                ack = 0

        if ack == 0x06:
            return True
        else:
            _logger.error("retried " + str(MAX_RETRIES) + " times without receiving ACK, is blackbox properly connected?")
            return False

    def _send_to_blackbox(self, packet, response_size, device_path, just_wait_for_ack=False):
        if not device_path:
            return ""

        ser = serial.Serial(port=device_path,
                            baudrate=19200,
                            timeout=3)
        MAX_NACKS = 1
        got_response = False
        sent_nacks = 0

        if self._send_and_wait_for_ack(packet, ser):
            if just_wait_for_ack:
                return True

            while not got_response and sent_nacks < MAX_NACKS:
                stx = ser.read(1)
                response = ser.read(response_size)
                etx = ser.read(1)
                bcc = ser.read(1)

                if stx == chr(0x02).encode() and etx == chr(0x03).encode() and bcc and self._lrc(response.decode()) == ord(bcc):
                    got_response = True
                    ser.write(chr(0x06).encode())
                else:
                    _logger.warning("received ACK but not a valid response, sending NACK...")
                    sent_nacks += 1
                    ser.write(chr(0x15).encode())

            if not got_response:
                _logger.error("sent " + str(MAX_NACKS) + " NACKS without receiving response, giving up.")
                return ""

            ser.close()
            return response
        else:
            ser.close()
            return ""

if isfile("/home/pi/registered_blackbox_be"):
    blackbox_thread = Blackbox()
    hw_proxy.drivers[DRIVER_NAME] = blackbox_thread

    class BlackboxDriver(hw_proxy.Proxy):
        @http.route('/hw_proxy/request_blackbox/', type='json', auth='none', cors='*')
        def request_blackbox(self, high_level_message, response_size):
            to_send = blackbox_thread._wrap_low_level_message_around(high_level_message)

            with blackbox_thread.blackbox_lock:
                response = blackbox_thread._send_to_blackbox(to_send, response_size, blackbox_thread.device_path)

            return response

        @http.route('/hw_proxy/request_serial/', type='json', auth='none', cors='*')
        def request_serial(self):
            try:
                with open('/sys/class/net/eth0/address', 'rb') as f:
                    return f.read().rstrip().replace(b':', b'')[-7:]
            except IOError as e:
                _logger.warning("eth0 network interface MAC address could not be found")
                return b''
