# 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 logging
import threading, queue
import platform
import subprocess
import uuid
import pathlib
import os
try:
    import winreg
except ImportError:
    winreg = None 
    WindowsError = Exception # to validate code under Linux

import launcher
import log
import paths

class PreFormUploader:

    FILES_FOLDER_PATH = os.path.join(paths.CURRENT_SETTINGS_FOLDER, "formlabs_tmp_files")
    ALLOWED_EXTENSIONS = (".form", ".stl", ".obj")
    CLEAR_FILES_ON_INIT = True
    OVERWRITE_FILES = True
    FILE_READ_BUFFER = 1024*1024

    PREFORM_PROCESS_TIMEOUT = 120
    PREFORM_ERROR_RETURN_CODE = {
        1: "Process killed",
        101: "Printer connection failed",
        102: "Dashboard login authentication failed",
        200: "Upload to printer failed",
        201: "Bad part. Auto-repair failed",
        202: "Bad part. Outside of build volume",
        203: "Input file does not exist",
        204: "Input file does not fit the printer"
    }

    __instance = None

    @staticmethod
    def getInstance():
        """ Static access method. """
        if not PreFormUploader.__instance:
            PreFormUploader()
        return PreFormUploader.__instance

    def __init__(self):
        self.call_lock = threading.RLock()
        """ Virtually private constructor. """
        if PreFormUploader.__instance != None:
            raise RuntimeError("This class is a singleton! Use only getInstance() call.")
        else:
            PreFormUploader.__instance = self
            self.logger = logging.getLogger(__name__)
            self.running = False
            self.preform_path = ""
            self.preform_running = False
            self.logger.info("FormLabs: PreForm Job Uploader Manager created.")
            self.job_queue = queue.Queue()
            self.job_results = {}
            self.preform_process = None
            self.job_thread = None
            self.init_files_folder()
            self.find_preform_path()
            self.start_job_uploader_loop()

    def is_supported(self):
        return platform.system() == 'Windows' and os.path.exists(self.preform_path)

    def start_job_uploader_loop(self):
        if self.running:
            self.logger.warning("WarningFormLabs job uploader loop already started.")
            return
        elif platform.system() != 'Windows':
            self.logger.warning("You OS doesn't support local printing on FormLabs.")
        elif not self.preform_path:
            self.logger.warning("Please install Preform to print locally.")
        else:
            self.logger.info("Starting FormLabs job uploader loop thread")
            self.running = True
            self.job_thread = threading.Thread(target=self.job_uploader_loop)
            self.job_thread.start()

    def stop_job_uploader_loop(self):
        self.logger.info("Stopping FormLabs job upload loop")
        self.running = False

    def add_job(self, printer_ip, file_path, thickness_um="", material=""):
        job_id = ""
        if self.is_supported():
            if printer_ip and file_path:
                job_id = str(uuid.uuid4())
                job = dict(
                    ID = job_id,
                    IP = printer_ip,
                    PATH = file_path,
                    THICKNESS = thickness_um,
                    MATERIAL = material
                )
                self.job_queue.put(job)
            else:
                self.logger.warning("Error. Can't add job for upload. Please check file and profile")
        return job_id

    def get_job_result(self, job_id):
        result = None
        with self.call_lock:
            if job_id in self.job_results:
                result = self.job_results.pop(job_id)
        return result

    @log.log_exception
    def job_uploader_loop(self):
        self.running = True
        while self.running:
            job = self.job_queue.get()
            self.logger.info(f'Working on {job}')
            job_result = {
                "success": True,
                "message": ""
            }
            processes = launcher.get_win_processes()
            for proc in processes:
                if 'PreForm.exe' in proc:
                    job_result["success"] = False
                    job_result["message"] = "PreForm app already running. Skipping job upload..."
            # Create a copy of the environment
            # Without this, Qt conflict blocks Preform from starting
            qt_plugin_path = "".join(self.preform_path.split(".")[:-1])
            my_env = os.environ.copy()
            my_env["QT_PLUGIN_PATH"] = qt_plugin_path

            # Check extension
            file_path = job["PATH"]
            file_extension = pathlib.Path(file_path).suffix
            if file_extension not in self.ALLOWED_EXTENSIONS:
                job_result["success"] = False
                job_result["message"] = f"Error. Can't send job. {file_extension} files not supported"
            preform_args = [self.preform_path, file_path, '--silentRepair', '--printer', job["IP"], '--autoSetup', '--upload', '--close']
            if job["THICKNESS"]:
                preform_args.append('--thickness_um')
                preform_args.append(job["THICKNESS"])
            if job["MATERIAL"]:
                preform_args.append('--material')
                preform_args.append(job["MATERIAL"])
            if job_result["success"]:
                try:
                    self.preform_process = subprocess.Popen(preform_args, stdout=subprocess.PIPE, env=my_env, shell=True)
                    stdout, stderr = self.preform_process.communicate(timeout=self.PREFORM_PROCESS_TIMEOUT)
                    exit_code = self.preform_process.returncode
                    if exit_code:
                        if exit_code == 0:
                            self.logger.info("Job upload to %s finished with exit code %d" % (job["IP"], exit_code))
                        elif exit_code in self.PREFORM_ERROR_RETURN_CODE.keys():
                            self.logger.warning("Error. Job upload to %s failed. PreForm exits with code %d. Reason: %s." % (
                                job["IP"], exit_code, self.PREFORM_ERROR_RETURN_CODE[exit_code]))
                            job_result["success"] = False
                            job_result["message"] = self.PREFORM_ERROR_RETURN_CODE[exit_code]
                        else:
                            self.logger.warning(
                                "Error. Job upload to %s failed. PreForm exits with code %d. Reason: Unknown." % (job["IP"], exit_code))
                            job_result["success"] = False
                            job_result["message"] = "Unknown. PreForm exit code %d" % exit_code
                except subprocess.TimeoutExpired:
                    self.logger.warning("Error. Failed to upload job. PreForm timeout expired. Skipping...")
                    job_result["success"] = False
                    job_result["message"] = "PreForm timeout expired"
            self.logger.info(f'Finished {job}')
            with self.call_lock:
                self.job_results[job["ID"]] = job_result
            self.job_queue.task_done()
            #time.sleep(self.JOB_MIN_LOOP_TIME)

    def find_preform_path(self):
        if platform.system() == 'Windows':
            # check if PreForm 3.20+ exist in default install path
            self.preform_path = "C:\\Program Files\\FormLabs\\PreForm\\PreForm.exe"
            if os.path.exists(self.preform_path):
                return
            self.preform_path = ""
            # find and check PreForm path from Windows registry
            try:
                key_to_read = r'SOFTWARE\WOW6432Node\PreForm'
                reg = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
                k = winreg.OpenKey(reg, key_to_read)
                value, regtype = winreg.QueryValueEx(k, 'Path')
                self.preform_path = value + r'\PreForm.exe'
                if not os.path.exists(self.preform_path):
                    self.preform_path = ""
            except WindowsError as e: # pylint: disable=used-before-assignment
                self.logger.warning("Warning. PreForm not installed on the machine. Feature 'Send print job' disabled")
        elif platform.system() == 'Darwin':
            self.preform_path = '/Applications/PreForm.app/Contents/MacOS/PreForm'
            if not os.path.exists(self.preform_path):
                self.preform_path = ""
        else:
            self.logger.warning("Warning. PreForm not installed on the machine. Feature 'Send print job' disabled")

    def init_files_folder(self):
        if not os.path.exists(self.FILES_FOLDER_PATH):
            self.logger.info("Creating tmp folder for formlabs...")
            try:
                os.mkdir(self.FILES_FOLDER_PATH)
                self.logger.info("...success")
            except OSError:
                self.logger.error("Can't create files folder")
                raise RuntimeError("Unable to create files folder")
        else:
            if not os.path.isdir(self.FILES_FOLDER_PATH):
                self.logger.info("Removing file on tmp formlabs folder path...")
                try:
                    os.remove(self.FILES_FOLDER_PATH)
                    os.mkdir(self.FILES_FOLDER_PATH)
                    self.logger.info("...success")
                except OSError:
                    self.logger.error("Can't remove file in place of files folder")
                    raise RuntimeError("Unable to create files folder")
        if self.CLEAR_FILES_ON_INIT:
            self.logger.info("Cleaning tmp formlabs folder...")
            for filename in os.listdir(self.FILES_FOLDER_PATH):
                if filename:
                    self.logger.info(f"Removing {filename}...")
                    try:
                        os.remove(os.path.join(self.FILES_FOLDER_PATH, filename))
                    except OSError:
                        self.logger.warning('Error removing file')
            self.logger.info("...success")

    def create_file_copy(self, input_file_path, filename=None):
        if not filename:
            filename = os.path.basename(input_file_path)
        output_file_path = os.path.join(self.FILES_FOLDER_PATH, filename)
        self.logger.info(f'Coping received file to {output_file_path}...')
        try:
            with open(input_file_path, "rb") as input_file:
                with open(output_file_path, "wb") as output_file:
                    data = input_file.read(self.FILE_READ_BUFFER)
                    while data:
                        output_file.write(data)
                        data = input_file.read(self.FILE_READ_BUFFER)
        except OSError:
            self.logger.error('Error while coping file ' + str(filename))
        else:
            self.logger.info("...success")
            return output_file_path


if __name__ == "__main__":
    logging.basicConfig()
    logger = logging.getLogger(__name__)
    #logger.setLevel(logging.WARNING)
    logger.setLevel(logging.INFO)
    #logger.setLevel(logging.DEBUG)
    requests_log = logging.getLogger("requests.packages.urllib3")
    #requests_log.setLevel(logging.WARNING)
    requests_log.setLevel(logging.DEBUG)
    requests_log.propagate = False

    uploader = PreFormUploader.getInstance()
    print(uploader.preform_path)
    while uploader.running:
        time.sleep(5)
