#
# Copyright 3D Control Systems, Inc. All Rights Reserved 2017-2019. Built in San Francisco.
#
# This software is distributed under commercial non-GPL license for personal, educational,
# corporate or any other use. The software as a whole or any parts of that are prohibited
# for distribution and/or use without obtaining license from 3D Control Systems, Inc.
#
# If you do not have the license to use this software, please delete all software files
# immediately and contact sales to obtain the license: sales@3dprinteros.com.
# If you are unsure about the licensing please contact directly our sales: sales@3dprinteros.com.

import datetime
import json
import logging
import os
import shutil
import subprocess
import threading
from PySide2 import QtCore

import config
import paths
import rights


def humanize_file_size(size):
    for unit in ['B', 'kB', 'MB', 'GB']:
        if abs(size) < 1024.0:
            return "%.1f %s" % (size, unit)
        size /= 1024.0
    return "%.1f%s" % (size, unit)


class FilesModel(QtCore.QObject):

    ROOT_FILES_DIR = os.path.expanduser("/media")

    SETTINGS_PATCH_NAME = "3dprinteros_settings_patch.json"

    INVALID_PATH_CHARS = ("..", "/", "\\")

    STAT_RESULT_PREFIX = "st_"

    on_fileError = QtCore.Signal()
    on_fileList = QtCore.Signal()
    on_diskSpaceLeft = QtCore.Signal()

    def __init__(self, core_model):
        super().__init__()
        self.logger = logging.getLogger(__name__)
        self._coreModel = core_model
        self._app = core_model._app
        self._printThread = threading.Thread()
        self._initFilesDir()

    def _initFilesDir(self):
        self._files_dir = self.ROOT_FILES_DIR
        if not os.path.isdir(self._files_dir):
            os.mkdir(self._files_dir)
        os.chdir(self.ROOT_FILES_DIR)

    def getAbsPath(self, path):
        return os.path.abspath(os.path.join(self._files_dir, path))

    def checkNameForHacks(self, name):
        for forbidden in self.INVALID_PATH_CHARS:
            if forbidden in name:
                self.on_fileError.emit("Invalid characters in path:" + forbidden)
                return False
        return True
    
    def _printFile(self, path):
        self._coreModel.showProcessing.emit("Loading complete", 0.0)
        #self._coreModel.showProcessing.emit("Loading complete", False)
        try:
            with open(self.getAbsPath(path), "rb") as f:
                gcodes = f.read()
        except Exception:
            message = "Exception while opening file to print"
            self.logger.exception(message)
            self.on_fileError.emit(message)
        else:
            self._app.qt_interface.printer_model.loadGcodes(gcodes)
        finally:
            self._coreModel.hideProcessing.emit()

    @QtCore.Slot(str)
    def makeDir(self, dirname):
        if self.checkNameForHacks(dirname):
            try:
                os.mkdir(self.getAbsPath(dirname))
            except Exception as e:
                self.logger.exception("Exception while making directory")
                self.on_fileError.emit(str(e))

    @QtCore.Slot(str)
    def printFile(self, path):
        if self._printThread and self._printThread.is_alive():
            message = "File loading blocked by other unfinished loading"
            self.logger.error(message)
            self.emit.on_fileError(message)
        else:
            self._printThread = threading.Thread(target=self._printFile, args=[path])
            self._printThread.start()

    @QtCore.Property('QVariantList', notify=on_fileList)
    def changeFilesDirectory(self, path):
        self._files_dir = os.path.join(self._files_dir, path)
        return self.getFilesList()

    @QtCore.Property('QVariantList', notify=on_fileList)
    def filesList(self):
        file_info_list = []
        if self._files_dir != self.ROOT_FILES_DIR:
            file_info_list.append({"filename": "..", "size": None, "atime": "", "mtime": "", "isdir": True})
        try:
            for filename in os.listdir(self._files_dir):
                file_info = {"filename": filename}
                stats = os.stat(filename)
                #file_info["size"] = humanize_file_size(stats.st_size)
                file_info["size"] = stats.st_size
                for attr_name in ("atime", "mtime"):
                    seconds = getattr(stats, self.STAT_RESULT_PREFIX + attr_name)
                    date = datetime.datetime.fromtimestamp(seconds).strftime("%A, %B %d, %Y %I:%M:%S")
                    file_info[attr_name] = date
                file_info["isdir"] = os.path.isdir(filename)
                file_info_list.append(file_info)
        except OSError:
            self.logger.exception("OS error on getting file list:")
        return file_info_list

    @QtCore.Property(str, notify=on_diskSpaceLeft)
    def diskSpaceLeft(self):
        _, _, free_space = shutil.disk_usage("/")
        return humanize_file_size(free_space)

    @QtCore.Slot()
    def copyUpdatePackage(self):
        source_path = self.ROOT_FILES_DIR
        dest_path = paths.UPDATE_FILE_PATH
        for folder in os.listdir(source_path):
            source_path = os.path.join(self.ROOT_FILES_DIR, folder, paths.UPDATE_FILE_NAME)
            if os.path.isfile(source_path):
                break
        if not os.path.isfile(source_path):
            self._coreModel.showMessage.emit(None, f"Error: No {paths.UPDATE_FILE_NAME} file found on the USB drive", None)
        else:
            try:
                subprocess.run(['cp', source_path, dest_path], check=True)
            except subprocess.CalledProcessError:
                self._coreModel.showMessage.emit(None, "Error: update to copy update package", None)
                try:
                    subprocess.run(['sync'])
                except:
                    pass
            else:
                self._coreModel.showMessage.emit(None, "Success. Reboot to apply the update", None)

    @QtCore.Slot()
    def loadSettingsFile(self):
        source_path = self.ROOT_FILES_DIR
        for folder in os.listdir(source_path):
            source_path = os.path.join(self.ROOT_FILES_DIR, folder, self.SETTINGS_PATCH_NAME)
            if os.path.isfile(source_path):
                break
        if not os.path.isfile(source_path):
            self._coreModel.showMessage.emit(None, f"Error: No {self.SETTINGS_PATCH_NAME} file found on the USB drive", None)
        else:
            try:
                with open(source_path) as f:
                    text = f.read()
                    settings_patch = json.loads(text)
            except:
                self._coreModel.showMessage.emit(None, "Error: corrupted setting patch", None)
            else:
                config.Config.instance().update_settings(settings_patch)
                try:
                    subprocess.run(['sync'])
                except:
                    pass
                self._coreModel.showMessage.emit(None, "Settings patching success", None)

    @QtCore.Slot()
    def updateCACerts(self):
        cert_found = False
        dest_path = paths.CUSTOM_CACERT_PATH
        for root, dirs, files in os.walk(self.ROOT_FILES_DIR):
            for f in files:
                source_path = os.path.join(root, f)
                if f.endswith('.crt'):
                    self.logger.info(f"Converting crt {source_path} to pem {dest_path}")
                    try:
                        subprocess.run(['openssl', 'x509', "-in", source_path, '-out', dest_path, '-outform', 'PEM'], check=True)
                    except subprocess.CalledProcessError as e:
                        self.logger.info('Certificate conversion crt->pem error:' + str(e))
                    else:
                        self.logger.info('Certificate conversion crt->pem success')
                        cert_found = True
                        break
                elif f.endswith('.pem'):
                    self.logger.info(f"Copying pem from {source_path} to {dest_path}")
                    try:
                        subprocess.run(['cp', source_path, dest_path], check=True)
                    except subprocess.CalledProcessError:
                        self.logger.warning(f'Unable to copy {source_path} to {dest_path}')
                    else:
                        cert_found = True
                        break
            if cert_found:
                break
        if cert_found:
            self.logger.info('Adding custom CA cert to certifi package')
            error = rights.add_cacert_to_certifi(dest_path)
            if error:
                self.logger.warning(error)
                self._coreModel.showMessage.emit(None, error, None)
            else:
                self.logger.info('Adding custom CA cert to system')
                error = rights.add_cacert_to_trust()
                if error:
                    self.logger.warning(error)
                    self._coreModel.showMessage.emit(None, error, None)
                else:
                    message = "Success. Custom CA certificate installed"
                    self.logger.info(message)
                    self._coreModel.showMessage.emit(None, message, None)
        else:
            message = "Error: No .pem or .crt file found on the USB drive"
            self.logger.warning(message)
            self._coreModel.showMessage.emit(None, message, None)
