import logging
import os
import time
import threading
import json

import log
import config
import user_login
import usb_detect

from printer_interface import PrinterInterface


class DeviceContainer:

    STATUS_UNKNOWN = "Unknown"
    STATUS_CONNECTING = "Connecting..."
    STATUS_NOT_PRINTER = "Probably not a printer"
    STATUS_CONNECTED = "Connected"
    FINAL_STATUSES = [STATUS_CONNECTED]
    PRINTER_INTERFACE_REQUEST_RERIOD = 0.5
    PRINTER_INTERFACE_JOIN_TIMEOUT = 0.51
    WAIT_STEP = 0.1
    CONNECTION_TIMEOUT = 20

    def __init__(self, parent, usb_info, profiles):
        self.logger = logging.getLogger(self.__class__.__name__ + str(usb_info))
        self.parent = parent
        self.usb_info = usb_info
        self.current_profile_index = 0
        self.printer_interface_lock = threading.RLock()
        self.printer_interface = None
        self.finished = False
        self.is_printer = False
        self.profiles = profiles
        self.status = self.STATUS_UNKNOWN

    def attempt_connection(self):
        self.create_printer_interface(self.profiles[self.current_profile_index])
        self.waiting_thread = threading.Thread(target=self.wait_for_connection_or_timeout)
        self.waiting_thread.start()

    def wait_for_connection_or_timeout(self):
        self.logger.info("Waiting for wizard detector's device container")
        total_waiting_time = 0
        while not (self.parent.app.stop_flag or self.finished):
            time.sleep(self.WAIT_STEP)
            if self.get_status() in self.FINAL_STATUSES:
                self.finished = True
            elif total_waiting_time >= self.CONNECTION_TIMEOUT:
                self.logger.info("Detection wizard device container %s switch to next profile" % self.usb_info)
                is_last_profile = self.switch_to_next_profile()
                if is_last_profile:
                    self.finished = True
                    self.status = self.STATUS_NOT_PRINTER
            else:
                total_waiting_time += self.WAIT_STEP

    def switch_to_next_profile(self):
        return True #TODO reimplement profile switching
        #  self.printer_interface.close()
        #  self.printer_interface.join(self.PRINTER_INTERFACE_JOIN_TIMEOUT)
        #  if len(self.profiles) < self.current_profile_index + 1:
        #      self.current_profile += 1
        #      self.attempt_connection()
        #      return False
        #  else:
        #      return True

    def create_printer_interface(self, profile):
        with self.printer_interface_lock:
            self.logger.info("Creating printer interface for %s..." % self.usb_info)
            pi = PrinterInterface(self.parent.app, self.usb_info,\
                    command_request_period=self.PRINTER_INTERFACE_REQUEST_RERIOD, offline_mode=True,
                    forced_printer_profile=profile)
            pi.start()
            self.printer_interface = pi
            self.status = self.STATUS_CONNECTING

    def get_status(self):
        with self.printer_interface_lock:
            if self.printer_interface:
                report = self.printer_interface.status_report()
                self.logger.info("Report:%s" % report)
                status = report['state']
                if status == "ready":
                    self.status = self.STATUS_CONNECTED
                elif status == "connecting":
                    self.status = self.STATUS_CONNECTING
                elif status == "error":
                    self.status = self.STATUS_NOT_PRINTER
            else:
                self.status = self.STATUS_NOT_PRINTER
            return self.status

    def get_current_profile_alias(self):
        return self.profiles[self.current_profile_index]['alias']

    def join(self):
        with self.printer_interface_lock:
            if self.printer_interface and self.printer_interface.is_alive():
                self.printer_interface.join(self.PRINTER_INTERFACE_JOIN_TIMEOUT)

    def close(self):
        self.logger.info("Closing detection wizard's device container for:%s" % self.usb_info)
        self.finished = True
        with self.printer_interface_lock:
            if self.printer_interface:
                self.printer_interface.close()
                self.printer_interface.join(self.PRINTER_INTERFACE_JOIN_TIMEOUT)


class DetectionWizard:

    DETECTION_TIMEOUT = 12
    #PROFILES_TO_TRY = ['Marlin', 'DUET3D', 'R2', "FF_DREAMER"]
    PROFILES_TO_TRY = ['Marlin'] # TODO Implement reset and multi-profile detecting. For now its only Marlin
    SAVE_TEXT_SUCCESS = "Detected printer profile was saved locally"
    FINAL_TEXT_SUCCESS = "Printers detection success"
    FINAL_TEXT_FAIL = "Sorry, no devices were identified as printers"
    SEND_TEXT = "Printer integration request is sent"
    WARNING_TEXT = "Welcome to a new printer detection wizard.\n\
        If your printer model is known to 3DPrinterOS Client, than you DON'T NEED this.\n\
        The wizard will walk through your USB devices and try to a identify 3D printers.\n\
        Detection process can take up to several minutes, but usually much less.\n\
        This is in most cases safe - hardware damage is very unlikely, but still theoretically possible.\n\
        Please note that 3D Control Systems is not responsible for any potential damage.\n\
        \n\
        WARNING: PROCEED AT YOUR OWN RISK!"

    def __init__(self, parent):
        self.logger = logging.getLogger(self.__class__.__name__)
        self.parent = parent
        self.usb_detector = usb_detect.USBDetector(None)
        self.all_profiles = config.get_profiles()
        self.try_profiles = [profile for profile in self.all_profiles if profile['alias'] in self.PROFILES_TO_TRY]
        self.device_containers = []
        self.active = False
        self.reset_params()

    def reset_params(self):
        if self.device_containers:
            for container in self.device_containers:
                container.close()
        for container in self.device_containers:
            container.join()
        self.device_containers = []
        self.full_devices_list = []
        self.devices_list = []
        self.active = False

    def stop(self):
        self.reset_params()

    def get_warning_text(self):
        return self.WARNING_TEXT

    def get_final_text(self, success):
        if success:
            return self.FINAL_TEXT_SUCCESS
        else:
            return self.FINAL_TEXT_FAIL

    def get_devices_list(self):
        return self.devices_list

    def get_full_devices_list(self):
        return self.full_devices_list

    def start_scan(self):
        # start only if we have user token to send a report/request to the cloud
        error = None
        if not self.parent.user_login.user_token:
            error = "Error: you have to be log in as user to use Detection Wizard"
        elif self.active:
            error = "Detection wizard error: already scanning"
        else:
            self.logger.error("Detection wizard scan start")
            self.active = True
            self.scan_thread = threading.Thread(target=self.scan, daemon=True)
            self.scan_thread.start()
            time.sleep(0.1)
        self.logger.error(error)
        return error

    def scan(self):
        self.logger.info("Waiting for all printer interfaces to close")
        timeleft = self.DETECTION_TIMEOUT
        while self.parent.printer_interfaces:
            if self.parent.stop_flag or timeleft < 0:
                return
            time.sleep(DeviceContainer.WAIT_STEP)
            timeleft -= DeviceContainer.WAIT_STEP
        self.logger.info("Full devices list: %s" % self.full_devices_list)
        self.full_devices_list = self.usb_detector.get_printers_list(ignore_profiles=True, add_descriptions=True)
        self.devices_list = [x for x in self.full_devices_list if x.get('COM')]
        self.logger.info("Detection wizard's devices list: %s" % self.devices_list)
        for device in self.devices_list:
            duplicate = False
            for dc in self.device_containers:
                if dc.usb_info == device:
                    self.logger.error("Detection wizard error - duplicate device detected:%s" % device)
                    duplicate = True
                    break
            if not duplicate:
                dc = DeviceContainer(self, device, self.try_profiles)
                dc.attempt_connection()
                self.device_containers.append(dc)
        self.logger.info("Detection wizard scan thread is waiting for active flag to go down")
        while self.active and not self.parent.stop_flag:
            time.sleep(0.1)
        self.reset_params()

    #  def sortout_known_printers(self):
    #      sorted_devices = []
    #      for profile in self.all_profiles:
    #          for vid_pid in profile.get('vids_pids', []):
    #              vid, pid = vid_pid
    #              for device in self.devices_list:
    #                  if vid != device['VID'] or pid == device['PID']:
    #                      sorted_devices.append(device)
    #      self.devices_list = sorted_devices

    def device_status_lines_list_by_id(self):
        devices = []
        usb_info_str_list = [json.dumps(dc.usb_info).strip('"').replace('"', "'") for dc in self.device_containers]
        device_lines_list = [self.device_line(dc) for dc in self.device_containers]
        statuses = [dc.status for dc in self.device_containers]
        if len(usb_info_str_list) != len(device_lines_list) != len(statuses):
            self.logger.error("Error: device status list and id list have different length")
        else:
            for index in range(0,len(usb_info_str_list)):
                devices.append((usb_info_str_list[index],\
                                device_lines_list[index],\
                                statuses[index]))
        return devices

    def device_line(self, device_container):
        line = ""
        usb_info = device_container.usb_info
        line += "%s:%s" % (usb_info['VID'], usb_info['PID'])
        if usb_info['COM']:
            line += " " + usb_info['COM']
        if usb_info['manufacturer']:
            line += "\t" + usb_info['manufacturer']
            line += " " + usb_info['product']
        try:
            profile_name = device_container.get_current_profile_alias()
        except (KeyError, IndexError):
            self.logger.debug("Error getting profile name of device container")
        else:
            line += "\t" + profile_name
        self.logger.info("Detection wizard status line: %s" % line)
        return line

    def get_device_container_by_usb_info(self, usb_info):
        for dc in self.device_containers:
            if dc.usb_info['VID'] == usb_info['VID'] and\
               dc.usb_info['PID'] == usb_info['PID'] and\
               dc.usb_info['SNR'] == usb_info['SNR']:
                return dc
        self.logger.error("Detection wizard is unable to find device container for %s" % usb_info)

    def save_profile_locally(self, usb_info):
        if type(usb_info) == str:
            try:
                if "'" in usb_info:
                    usb_info = usb_info.replace("'",'"')
                usb_info = json.loads(usb_info)
            except (ValueError, TypeError):
                self.logger.info("Detection wizard usb_info decode error - not valid json: %s" % usb_info)
        dc = self.get_device_container_by_usb_info(usb_info)
        if dc:
            profile_alias = dc.get_current_profile_alias()
        else:
            error = "Detection wizard error: no device container for usb_info:%s" % usb_info
            self.logger.error(error)
            return error
        profiles = config.get_profiles()
        for profile in profiles:
            if profile['alias'] == profile_alias:
                break
        else:
            profile = None
        if not profile:
            error = "Can't find profile with name: %s" % profile
            self.logger.error(error)
            return error
        vid, pid = usb_info['VID'], usb_info['PID']
        vid_pid = [vid, pid] # should be list not tuple, so this conversion is necessary
        vids_pids = profile["vids_pids"]
        if vid_pid in vids_pids:
            self.logger.error("Error: VID and PID already in profile")
        else:
            vids_pids.append(vid_pid)
            user_login.UserLogin.save_profiles(profiles)
            self.logger.info("Profiles updated")

    def scan_finised(self):
        # if there is no device list, that means that scan was not even started
        if not self.full_devices_list:
            time.sleep(0.1)
            return False
        # wait a second if device container list is empty - they could initialize shortly
        if not self.device_containers:
            time.sleep(1)
        for dc in self.device_containers:
            if not dc.finished:
                return False
        return True

    def create_report(self, form_text):
        report =  "Detection wizard report from %s\n\n"
        report += "Full device list:\n%s\n\n"
        report += "Serial port devices list:\n%s\n\n"
        report += "Full connection attempts list:\n%s\n\n"
        report = report % (self.parent.user_login.login,\
                "\n".join(map(str, self.full_devices_list)),\
                "\n".join(map(str, self.devices_list)),\
                "\n".join([self.device_line(dc) for dc in self.device_containers]))
        report += "\n\nIntegration request form:\n" + form_text
        try:
            with open(log.DETECTION_REPORT_FILE, "w") as f:
                f.write(report)
        except (IOError, OSError):
            error = "Error writing report file"
            self.logger.error(error)
            return error

    def send_report(self):
        error = log.send_logs(force=True)
        if error:
            return error
        if os.path.isfile(log.DETECTION_REPORT_FILE):
            os.remove(log.DETECTION_REPORT_FILE)

    def create_and_send_report(self, form_text):
        error = self.create_report(form_text)
        if error:
            return error
        error = self.send_report()
        if error:
            return error
