# 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 time
import os
import signal
import platform
import subprocess
import getpass


import log
import version
import config
import paths
import rights
from plugin_controller import PluginController
from user_login import UserLogin
from updater import Updater
from rights import RightsChecker
from camera_controller import CameraController
from makerware_utils import ConveyorKillWaiter
from usb_detect import USBDetector
from network_detect import NetworkDetector
from client_detector import ClientDetector
from printer_interface import PrinterInterface
from gpio_interface import GPIOInterface


class App(object):

    LOG_TIMESTAMP_PERIOD = 30
    BROWSER_OPENING_DELAY = 2
    QUIT_THREAD_JOIN_TIMEOUT = 30

    def __init__(self):
        self.logger = log.create_logger('', log.LOG_FILE)
        self.logger.info("Starting 3DPrinterOS client. Version %s. Build %s." % (version.version, version.build))
        self.logger.info('Operating system: ' + platform.system() + ' ' + platform.release())
        self.logger.info('Server: ' + config.get_settings()['URL'])
        try:
            self.init()
        except:
            pass
        finally:
            self.quit()

    @log.log_exception
    def init(self):
        config.Config.instance().set_app_pointer(self)
        paths.remove_downloaded_files()
        rights.add_cacert_to_certifi()
        self.printer_interfaces = []
        self.offline_mode = False
        self.poweroff_flag = False
        self.virtual_printer_enabled = False
        self.stop_flag = False
        self.restart_flag = False
        self.closing_status = []
        signal.signal(signal.SIGINT, self.intercept_signal)
        signal.signal(signal.SIGTERM, self.intercept_signal)
        self.plugin_controller = PluginController(self)
        self.rights_checker = RightsChecker(self)
        self.conveyor_kill_waiter = ConveyorKillWaiter(self)
        self.user_login = UserLogin(self)
        self.os_user_name = getpass.getuser()
        self.init_user_interfaces()
        self.wait_and_open_browser()
        self.client_detector = ClientDetector(self)
        if self.user_login.wait() and self.rights_checker.wait() and self.conveyor_kill_waiter.wait():
            #TODO Get mac here too. To do this, we need to move mac address detection code from http_client.
            self.local_ip = self.user_login.http_connection.local_ip
            self.updater = Updater(self)
            config.Config.instance().set_profiles(self.user_login.profiles)
            self.virtual_printer_enabled = config.get_settings()['virtual_printer']['enabled']
            self.virtual_printer_usb_info = dict(config.get_settings()['virtual_printer'])
            del self.virtual_printer_usb_info['enabled']
            self.camera_controller = CameraController(self)
            self.usb_detector = USBDetector()
            self.network_printers_detector = NetworkDetector(self)
            self.main_loop()

    @staticmethod
    def open_browser():
        if config.get_settings()['web_interface']['browser_opening_on_start']:
            import webbrowser
            import logging
            address = "http://%s:%d" % (config.LOCAL_IP, config.get_settings()['web_interface']['port'])
            logger = logging.getLogger()
            logger.info("Opening browser tab %s" % address)
            webbrowser.open(address, 2, True)

    def wait_and_open_browser(self):
        start = time.monotonic()
        self.logger.info("Waiting for timeout or login to open browser...")
        while time.monotonic() < start + self.BROWSER_OPENING_DELAY and not self.user_login.user_token:
            time.sleep(0.01)
        self.logger.info("...done")
        App.open_browser()

    def init_user_interfaces(self):
        self.gpio_interface = GPIOInterface()
        if config.get_settings()['web_interface']['enabled']:
            import web_interface
            self.web_interface = self.init_user_interface(web_interface.WebInterface)
            if config.get_settings()['web_interface']['port_https']:
                self.https_web_interface = self.init_user_interface(web_interface.HTTPSWebInterface)
        if config.get_settings()['qt_interface']['enabled']:
            import qt_interface
            self.qt_interface = self.init_user_interface(qt_interface.QtInterface)

    def init_user_interface(self, interface_class):
        interface = interface_class(self)
        interface.start()
        self.logger.debug("Waiting for %s to start..." % str(interface))
        interface.wait()
        self.logger.debug("...interface %s is up and running." % interface_class.__class__.__name__)
        return interface

    def intercept_signal(self, signal_code, frame):
        self.logger.warning("SIGINT or SIGTERM received. Starting exit sequence")
        if self.stop_flag:
            self.quit()
        self.stop_flag = True

    def toggle_virtual_printer(self):
        self.virtual_printer_enabled = not self.virtual_printer_enabled
        settings = config.get_settings()
        settings['virtual_printer']['enabled'] = self.virtual_printer_enabled
        config.Config.instance().save_settings(settings)

    @log.log_exception
    def main_loop(self):
        last_time = 0
        while not self.stop_flag:
            detected_printers = self.usb_detector.get_printers_list()
            if self.virtual_printer_enabled:
                detected_printers.append(self.virtual_printer_usb_info)
            detected_printers.extend(self.network_printers_detector.get_printers_list())
            self.connect_new_printers(detected_printers)
            for pi in self.printer_interfaces:
                if pi.usb_info not in detected_printers:
                    if not pi.forced_state == "error":
                        pi.register_error(99, "Printer is no longer detected as device", is_blocking=True)
                if not pi.is_alive():
                    self.logger.info('Removing %s from printer list' % pi.usb_info)
                    self.printer_interfaces.remove(pi)
            self.gpio_interface.check_events(self.printer_interfaces)
            if not self.stop_flag:
                time.sleep(config.get_settings()['main_loop_period'])
                now = time.monotonic()
                if last_time + self.LOG_TIMESTAMP_PERIOD < now:
                    last_time = now
                    self.logger.debug(time.strftime("%d %b %Y %H:%M:%S", time.localtime()))

    def connect_new_printers(self, detected_printers):
        currently_connected_usb_info = [pi.usb_info for pi in self.printer_interfaces]
        for usb_info in detected_printers:
            if usb_info not in currently_connected_usb_info:
                pi = PrinterInterface(self, usb_info, config.get_settings()['printer_loop_period'])
                currently_connected_usb_info.append(pi.usb_info)
                pi.start()
                self.printer_interfaces.append(pi)

    def close_module(self, state, closing_function, thread_to_join=None):
        self.logger.info(state)
        self.closing_status.append(state)
        closing_function()
        self.closing_status[-1] += 'done'
        if thread_to_join:
            try:
                thread_to_join.join(self.QUIT_THREAD_JOIN_TIMEOUT)
            except (AttributeError, RuntimeError):
                pass
        else:
            time.sleep(0.5)

    def register_error(self, code, message, is_blocking=False, is_info=False):
        self.logger.warning("Error:N%d in child of app. %s" % (code, message))

    def raise_stop_flag(self):
        self.stop_flag = True

    def set_offline_mode(self, enabled=True):
        self.offline_mode = enabled
        for pi in self.printer_interfaces:
            pi.offline_mode = enabled

    def is_sudo_available(self):
        try:
            return not subprocess.run(['sudo', '-n', 'whoami']).returncode
        except:
            return False

    @log.log_exception
    def quit(self): # TODO refactor all the closing by creating class Closable and subclasses for each case
        self.logger.info("Starting exit sequence...")
        if hasattr(self, 'client_detector'):
            self.close_module('Closing client detector...', self.client_detector.close)
        if hasattr(self, 'camera_controller'):
            self.close_module('Closing camera...', self.camera_controller.stop_camera_process)
        if hasattr(self, 'gpio_interface'):
            self.close_module('Closing GPIO interface...', self.gpio_interface.close)
        printer_interfaces = getattr(self, "printer_interfaces", [])
        for pi in printer_interfaces:
            printer_name = pi.printer_profile.get('name', 'nameless printer')
            state = 'Closing ' + printer_name + '...'
            self.close_module(state, pi.close)
        for pi in printer_interfaces:
            printer_name = pi.printer_profile.get('name', 'nameless printer')
            state = 'Joining ' + printer_name + '...'
            self.close_module(state, pi.join)
        if hasattr(self, "plugin_controller"):
            self.close_module('Closing Plugins...', self.plugin_controller.close)
        if hasattr(self, 'zmq_context'):
            self.close_module('Terminating ZMQ context...', self.zmq_context.term)
        time.sleep(0.2)  # need to reduce logging spam
        if hasattr(self, "web_interface"):
            self.close_module("Closing web server...", self.web_interface.close, self.web_interface)
            if hasattr(self, "https_web_interface"):
                self.close_module("Closing HTTPS web server...", self.https_web_interface.close, self.https_web_interface)
        if hasattr(self, "qt_interface"):
            self.close_module("Closing qt interface...", self.qt_interface.close, self.qt_interface)
        self.logger.info("...everything is correctly closed.")
        self.logger.info("Goodbye ;-)")
        if self.poweroff_flag:
            if self.is_sudo_available():
                subprocess.call('sudo bash -c "echo 1 > /sys/class/backlight/rpi_backlight/bl_power"', shell=True)
                subprocess.call(["sudo", "poweroff"])
                time.sleep(1)
        for handler in self.logger.handlers:
            handler.flush()
