# 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 os
import re
import time
import logging
import threading
import time
import subprocess
from PySide6 import QtCore
from PySide6.QtWidgets import QApplication
from PySide6.QtGui import QFont

import config
import log
import rights
import version
import user_login
import ffplay_audio


class CoreModel(QtCore.QObject):

    PYTHON_QML_CONSTANTS_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), 'qml/constants.js'))

    DEFAULT_RESOLUTION = [800, 480]
    DEFAULT_FULLSCREEN = False

    stateChanged = QtCore.Signal()
    on_show_hello = QtCore.Signal()
    on_offlineModeChanged = QtCore.Signal()
    on_cameraState = QtCore.Signal()
    on_virtualPrinter = QtCore.Signal()
    on_sshState = QtCore.Signal()
    on_configToEdit = QtCore.Signal()

    # Header(Info, Warning, Error, etc), Message, Button text
    showMessage = QtCore.Signal(str, str, str)
    # Header(Info, Warning, Error, etc), Message, Confirm button text, callback name(in qml format)
    showConfirm = QtCore.Signal(str, str, str, str, str)
    showProcessing = QtCore.Signal(str, float)
    hideProcessing = QtCore.Signal()
    hideSettings = QtCore.Signal()
    osUpdateLog = QtCore.Signal(str)

    on_updateCheck = QtCore.Signal(str)
    on_updateError = QtCore.Signal(str)

    def __init__(self, app):
        self.logger = logging.getLogger(__name__)
        self._app = app
        self._cameraEnabled = False
        self._loadConstants()
        self._state = self.STATE_CONNECTING
        self._previous_state = self.STATE_CONNECTING
        self._is_sudo_available = rights.is_sudo_available()
        self._state_lock = threading.Lock()
        self._show_hello = config.get_settings().get('wizard_on_start', False)
        self.resolution = config.get_settings().get('qt_interface').get('resolution', self.DEFAULT_RESOLUTION)
        self.fullscreen = config.get_settings().get('qt_interface').get('fullscreen', self.DEFAULT_FULLSCREEN)
        super().__init__()

    def _constantNameToClassPrefix(self, name):
        new_name = ""
        previous_letter = ""
        for letter in name:
            if letter.isupper() and previous_letter.islower():
                new_name += "_"
            new_name += letter
            previous_letter = letter
        new_name = new_name.upper()
        new_name = new_name.rstrip("S")
        new_name = new_name + "_" 
        return new_name

    def _loadConstants(self):
        with open(self.PYTHON_QML_CONSTANTS_FILE) as f:
            current_var = None
            text = f.read()
            for line in text.split("\n"):
                var_search = re.match(r"\s*var(.*) = ", line)
                if not line:
                    continue
                if var_search:
                    current_var = var_search.group(1).strip()
                if current_var:
                    if "}" in line:
                        current_var = None
                    elif ":" in line:
                        line = line.replace(",", "")
                        attr_name, attr_value = line.split(":")
                        prefix = self._constantNameToClassPrefix(current_var)
                        attr_name = prefix + attr_name.strip()
                        self.logger.debug("QML constant %s set to attribute %s" % (attr_value, attr_name))
                        setattr(self, attr_name, attr_value.strip().strip('"'))

    def setState(self, state):
        with self._state_lock:
            self._previous_state = self._state
            self._state = state
            if self._previous_state != state:
                self.logger.debug("Model state changed from %s to %s " % (self._previous_state, state))
                self.stateChanged.emit()

    @log.log_exception
    def _checkForUpdates(self):
        updater = self._app.updater
        update_download_url = updater.request_update_download_url()
        self.on_updateCheck.emit(update_download_url)

    def _updateCameraState(self):
        cameraName = self._app.camera_controller.get_current_camera_name()
        self._cameraEnabled = cameraName != "Disable camera"

    def _sshEnabled(self):
        try:
            output = subprocess.check_output(["systemctl", "is-active", "sshd"])
        except subprocess.CalledProcessError:
            return False
        return not b"inactive" in output

    @QtCore.Slot(str)
    def setAppFont(self, font_name):
        defaultFont = QFont()
        defaultFont.setFamily(font_name)
        QApplication.instance().setFont(defaultFont)

    @QtCore.Slot(result=bool)
    def checkOSUpdate(self):
        # TODO:
        result = True
        return result

    @QtCore.Slot(result=bool)
    def startOSUpdate(self):
        self.osUpdateLog.emit("OS update not implemented. Should be run in another thread to prevent UI blocking. self.osUpdateLog.emit('finished') when updates done")
        self.osUpdateLog.emit("finished")

    @QtCore.Slot()
    def shutdown(self):
        self.logger.warning("Shutting down RPi...")
        self._app.poweroff_flag = True
        self._app.stop_flag = True

    @QtCore.Slot()
    def unbind(self):
        self.logger.warning("Unbinding account(erasing token)")
        user_login.UserLogin.logout()
        self._app.user_login.forget_auth_tokens()
        self._app.qt_interface.printer_model.disconnect_printer()

    @QtCore.Slot()
    def toggleOfflineMode(self):
        self.logger.info("Offline mode toggled")
        offline_mode = not self._app.offline_mode
        if not offline_mode and not self._app.qt_interface.printer_model._authToken:
            self._app.qt_interface.printer_model.disconnect_printer()
        self._app.set_offline_mode(offline_mode)
        self.on_offlineModeChanged.emit()

    @QtCore.Slot()
    def switchCloud(self):
        settings = config.get_settings()
        url = settings["URL"].replace("cli-", "")
        if url == "cloud.3dprinteros.com":
            config.Config.instance().update_settings({"URL": "acorn.3dprinteros.com"})
        else:
            config.Config.instance().update_settings({"URL": "cli-cloud.3dprinteros.com"})
        self.logger.warning("Restarting...")
        self._app.stop_flag = True

    @QtCore.Slot()
    def toggleSSH(self):
        if not self._is_sudo_available:
            self.logger.info("No passwordless sudo to toggle sshd(checked by username).")
        else:
            try:
                if self._sshEnabled():
                    self.logger.info("Stopping and disabling ssh...")
                    subprocess.call(["sudo", "systemctl", "stop", "sshd"])
                    subprocess.call(["sudo", "systemctl", "disable", "sshd"])
                    self.logger.info("ssh disabled")
                else:
                    self.logger.info("Starting and enabling ssh...")
                    subprocess.call(["sudo", "systemctl", "start", "sshd"])
                    subprocess.call(["sudo", "systemctl", "enable", "sshd"])
                    self.logger.info("ssh enabled")
            except (subprocess.CalledProcessError, OSError):
                self.logger.exception("Exception while toggling sshd")
            self.on_sshState.emit()

    @QtCore.Slot()
    def checkForUpdates(self):
        thread = threading.Thread(target=self._checkForUpdates)
        thread.start()

    def _perform_update(self):
        self.logger.info('Perform update slot is called')
        self.showProcessing.emit('Updating...', 0.0)
        error = self._app.updater.download_update()
        if error:
            self.logger.info('Update download error: ' + error)
            self.on_updateError.emit(error)
            self.hideProcessing.emit()
        else:
            self.logger.info('Update downloaded restarting')
            self._app.restart_flag = True
            self._app.stop_flag = True
            self.hideProcessing.emit()

    @QtCore.Slot()
    def performUpdate(self):
        thread = threading.Thread(target=self._perform_update)
        thread.start()

    @QtCore.Slot()
    def enableCamera(self, newCameraTypeName="Pi camera"):
        configObject = config.Config.instance()
        cameraSettingsDict = config.get_settings()["camera"]
        cameraSettingsDict["default"] = newCameraTypeName
        configObject.update_settings({"camera": cameraSettingsDict})
        self._app.camera_controller.start_camera_process(newCameraTypeName)
        self._updateCameraState()
        self.on_cameraState.emit()

    @QtCore.Slot()
    def disableCamera(self):
        configObject = config.Config.instance()
        cameraSettingsDict = config.get_settings()["camera"]
        cameraSettingsDict["default"] = "Disable camera"
        configObject.update_settings({"camera": cameraSettingsDict})
        self._app.camera_controller.stop_camera_process()
        self._updateCameraState()
        self.on_cameraState.emit()

    @QtCore.Slot()
    def toggleVirtualPrinter(self):
        configObject = config.Config.instance()
        if self._app.virtual_printer_enabled:
            self._app.virtual_printer_enabled = False
            configObject.update_settings({"virtual_printer": { "enabled": False }})
        else:
            self._app.virtual_printer_enabled = True
            configObject.update_settings({"virtual_printer": { "enabled": True }})
        self.on_virtualPrinter.emit()

    @QtCore.Slot()
    def uploadLogs(self):
        log.report_problem("Logs from %s" % config.get_settings().get('vendor', "Unknown 3DPrinterOS QtUI printer"))

    @QtCore.Slot(str)
    def writeLog(self, text):
        self.logger.info("QML:" + text)

    @QtCore.Slot(str)
    def validateAndSaveConfig(self, config_text):
        error = config.Config.instance().set_settings_as_text(config_text)
        if error:
            self.showMessage.emit(None, "Error: " + str(error), None)
        else:
            self.showMessage.emit(None, "Saved", None)

    @QtCore.Slot(str, float)
    def playAudio(self, audio_filename, volume_mult):
        ffplay_audio.play(audio_filename, blocking=False, volume_mult=volume_mult)

    @QtCore.Property(str, notify=stateChanged)
    def state(self):
        return self._state

    @QtCore.Property(bool, notify=on_show_hello)
    def show_hello(self):
        return self._show_hello

    @QtCore.Property(bool, notify=on_offlineModeChanged)
    def offlineMode(self):
        return self._app.offline_mode

    # WORKAROUND: PYSIDE-1426 TypeError bug for const decorator
    def version(self):
        return version.version + " " + version.build
    version = QtCore.Property(str, fget=version, constant=True)

    # WORKAROUND: PYSIDE-1426 TypeError bug for const decorator
    def branch(self):
        return version.branch
    branch = QtCore.Property(str, fget=branch, constant=True)

    def cloudURL(self):
        return config.get_settings()["URL"].replace("cli-", "")
    cloudURL = QtCore.Property(str, fget=cloudURL, constant=True)

    def theme(self):
        return config.get_settings().get('qt_interface').get('theme', 'default')
    theme = QtCore.Property(str, fget=theme, constant=True)

    @QtCore.Property(bool, notify=on_cameraState)
    def cameraState(self):
        self._updateCameraState()
        return self._cameraEnabled

    @QtCore.Property(bool, notify=on_virtualPrinter)
    def virtualPrinterState(self):
        return self._app.virtual_printer_enabled

    @QtCore.Property(bool, notify=on_sshState)
    def sshState(self):
        return self._sshEnabled()

    @QtCore.Property(str, notify=on_configToEdit)
    def configToEdit(self):
        return config.Config.instance().get_settings_as_text()
