# Copyright 3D Control Systems, Inc. All Rights Reserved 2017-2019.
# Built in San Francisco.

# This software is distributed under a commercial license for personal,
# educational, corporate or any other use.
# The software as a whole or any parts of it is prohibited for distribution or
# use without obtaining a license from 3D Control Systems, Inc.

# All software licenses are subject to the 3DPrinterOS terms of use
# (available at https://www.3dprinteros.com/terms-and-conditions/),
# and privacy policy (available at https://www.3dprinteros.com/privacy-policy/)

import usb.core
import usb.util
import sys
import os
import time
import logging
from typing import Any

import paths
import printrun_sender
import base_sender

READ_TIMEOUT = 1000
DEFAULT_READ_LENGTH = 512


class Sender(printrun_sender.Sender):

    def __init__(self, parent: Any, usb_info: dict, profile: dict):
        base_sender.BaseSender.__init__(self, parent, usb_info, profile)
        self.usb_info = usb_info
        self.printcore = None # for sake of printrun_senders status and percentage reports
        self.logger = logging.getLogger(__name__)
        if not sys.platform.startswith('linux'):
            raise RuntimeError("BeeVeryCreative don't supported by your OS... for now.")
        if usb_info.get('COM'):
            printrun_sender.Sender.__init__(self, parent, usb_info, profile)
        else:
            if self._init_raw_usb_device():
                self._flash_firmware()

    def _find_firmware_file(self) -> str:
        firmware_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "firmware")
        for file_name in os.listdir(firmware_dir):
            if file_name.lower().startswith('beetf'):
                firmware = os.path.join(firmware_dir, file_name)
                return firmware

    def _get_firmware_version(self, firmware_filename: str) -> str:
        version = firmware_filename.lower().replace('beetf-', '')
        return version.replace('.bin', '')

    def _init_raw_usb_device(self):  # TODO: add ret value
        print("Starting flashing initialization")
        # find our device
        int_vid = int(self.usb_info['VID'], 16)
        int_pid = int(self.usb_info['PID'], 16)
        backend_from_our_directory = usb.backend.libusb1.get_backend(find_library=paths.get_libusb_path)
        dev = usb.core.find(idVendor=int_vid, idProduct=int_pid, backend=backend_from_our_directory)
        # set the active configuration. With no arguments, the first
        # configuration will be the active one
        dev.set_configuration()
        # get an endpoint instance
        cfg = dev.get_active_configuration()
        intf = cfg[(0,0)]
        ep_out = usb.util.find_descriptor(
            intf,
            # match the first OUT endpoint
            custom_match = \
            lambda e: \
                usb.util.endpoint_direction(e.bEndpointAddress) == \
                usb.util.ENDPOINT_OUT)
        ep_in = usb.util.find_descriptor(
            intf,
            # match the first in endpoint
            custom_match = \
            lambda e: \
                usb.util.endpoint_direction(e.bEndpointAddress) == \
                usb.util.ENDPOINT_IN)
        # Verify that the end points exist
        self.dev = dev
        self.ep_in = ep_in
        self.ep_out = ep_out
        return ep_in and ep_out

    #used only in raw usb mode, which we use only to flash firmware in printer
    def _dispatch(self, message):
        time.sleep(0.001)
        self.ep_out.write(message)
        time.sleep(0.009)
        try:
            ret = self.ep_in.read(DEFAULT_READ_LENGTH, READ_TIMEOUT)
            sret = ''.join([chr(x) for x in ret])
        except:
            sret = "USB read timeout"
        return sret

    def _flash_firmware(self):
        firmware = self._find_firmware_file()
        if firmware:
            self.logger.info("Prepare to flash device with firmware:{0}.".format(firmware))
            file_size = os.path.getsize(firmware)
            version = "0.0.0"
            ret1 = self._dispatch("M114 A{0}\n".format(version))
            message = "M650 A{0}\n".format(file_size)
            ret2 = self._dispatch(message)
            if 'ok' in ret1 and 'ok' in ret2:
                with open(firmware, 'rb') as f:
                    while True:
                        buf = f.read(64)
                        if not buf: break
                        self.ep_out.write(buf)
                        ret = []
                        while (len(ret) != len(buf)):
                            ret += self.ep_in.read(len(buf), READ_TIMEOUT)
                        assert (''.join([chr(x) for x in ret]) in buf)
                        self.logger.debug(".")
                self.logger.info("Flashing complete.")
                new_version = self._get_firmware_version(os.path.basename(firmware))
                self._dispatch("M114 A{0}\n".format(new_version))
                self._dispatch("M630\n")
                self.dev.reset()
                self.logger.info("Rebooting to new firmware...")
        else:
            self.parent.register_error(601, "Error - no firmware found.", disconnect=True)

    def cancel(self):  # TODO: discuss
        if self.profile.get('COM'):
            self.cancel()
        else:
            self._dispatch("M609\n")
