# 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 ftplib
import json
import io
import random
import os
import ssl
import subprocess
import threading
import time
import uuid
import zipfile
import paths
import typing
import bambulab_lan_cam
import paho.mqtt.client as mqtt
import multiprocessing
import sys
import config
import base_sender
import platforms
import bambulab_errors
import tempfile

class P1P_FTP_TLS(ftplib.FTP_TLS):
    """FTP_TLS subclass that automatically wraps sockets in SSL to support FTPS."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._sock = None

    @property
    def sock(self):
        """Return the socket."""
        return self._sock

    @sock.setter
    def sock(self, value):
        """When modifying the socket, ensure that it is ssl wrapped."""
        if value is not None and not isinstance(value, ssl.SSLSocket):
            value = self.context.wrap_socket(value)
        self._sock = value


class X1_FTP_TLS(P1P_FTP_TLS):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    """Force FTPS passive mode"""
    def makepasv(self):
        _, port = super().makepasv()
        return self.host, port

    """Explicit FTPS, with shared TLS session"""
    def ntransfercmd(self, cmd, rest=None):
        conn, size = ftplib.FTP.ntransfercmd(self, cmd, rest)
        if self._prot_p:
            conn = self.context.wrap_socket(conn,
                                            server_hostname=self.host,
                                            session=self.sock.session)  # this is the fix
        return conn, size


class Sender(base_sender.BaseSender):
    MQTT_PORT = 8883
    FTP_PORT = 990
    CAMERA_PORT = 322
    USERNAME = "bblp"
    LOOP_SLEEP_SEC = 5
    FTP_TIMEOUT = 15
    PROJECT_PRINT_PATH = "Metadata/plate_1.gcode"
    CAMERA_CLOSE_TIMEOUT = 12
    JOB_START_WAIT_TIMEOUT = 120
    SUPPORT_JOBS = True
    CONNECT_TIMEOUT = 6
    CHUNK_SIZE = 1024 * 1024

    CONNECT_RC_DESC = {
        0: "Connection successful",
        1: "Connection refused - incorrect protocol version",
        2: "Connection refused - invalid client identifier",
        3: "Connection refused - server unavailable",
        4: "Connection refused - bad username or password",
        5: "Connection refused - not authorised"
    }

    DEFAULT_SETTINGS = {
        "use_ams" : True,
        "timelapse" : False,
        "bed_leveling" : True,
        "flow_cali" : False,
        "vibration_cali" : False,
        "layer_inspect" : True
    }

    AUTO_BED_TYPE = "auto"

    BED_TYPES = {
        "Textured PEI Plate": "textured_plate",
        "High Temp Plate": "hot_plate",
        "Engineering Plate": "eng_plate",
        "Cool Plate": "cool_plate"
    }

    CURRENT_BED_ARG = "curr_bed_type"

    def __init__(self, parent: typing.Any, usb_info: dict, profile: dict = {}):
        super().__init__(parent, usb_info, profile)
        self.serial_number = usb_info.get("SNR", "").upper()
        self.host = usb_info.get("IP", "")
        self.run_once = usb_info.get('RUNONCE')
        self.password = usb_info.get('PASS')
        self.client = mqtt.Client()
        self.client.on_connect = self._on_connect
        self.client.on_message = self._on_message
        self.client.on_disconnect = self._on_disconnect
        self.last_message = ""
        self.user_id = '3DPrinterOS_Cloud_Client' + str(random.randint(1000, 9999))
        self.logger.info(f'UserID for BambuLab requests for {str(self.host)}: {self.user_id}')
        self.current_sequence_id = 0
        self.printer_has_sdcard = False
        # Set the TLS/SSL options for the client
        self.client.tls_set(tls_version=ssl.PROTOCOL_TLS, cert_reqs=ssl.CERT_NONE)
        self.client.tls_insecure_set(True)
        self.client.username_pw_set(self.USERNAME, self.password)
        self.status_thread = threading.Thread(target=self._status_loop, name="BambuLabStatusThread_" + str(self.serial_number))
        self.publish_lock = threading.RLock()
        self.sequence_lock = threading.RLock()
        self.material_names = None
        self.material_desc = None
        self.material_colors = None
        self.next_print_options = None
        self.has_ams = False
        self.mqtt_device_prefix = "device/%s/" % self.serial_number
        self.settings.update(self.DEFAULT_SETTINGS)
        self.load_printer_settings()
        self.camera_number = 1
        self.camera_url = None
        self.bed_not_clear = False
        self.last_start_print_time = None
        self.camera_process = None
        self.custom_camera_token = None
        self.wait_print_start = False
        self.last_err_code = "0"
        if self.profile.get("camera_url_template"):
            self.camera_url = self.profile.get("camera_url_template") % (self.USERNAME, self.password, self.host, self.CAMERA_PORT, self.camera_number)
        if not self.run_once:
            self._connect_loop()
        # Wait for connection
        start_time = time.time()
        while not self.operational_flag:
            if time.time() - start_time > self.CONNECT_TIMEOUT:
                self.close()
                raise RuntimeError("Max wait time to connect printer reached")
            time.sleep(1)

    def _get_number_by_ip(self):
        numbers = [i.zfill(3) for i in self.host.split(".")]
        try:
            number = int(''.join(numbers))
        except ValueError:
            number = 0
        return number

    def _start_camera_capture_process(self):
        if self.parent:
            camera_settings = config.get_settings()['camera']
            if not camera_settings['enabled'] or \
                not camera_settings['from_sender']['bambulab_lan_cam'] or \
                self.parent.current_camera == "Disable camera":
                    self.logger.info('Bambulab camera start prevented - disabled in user settings')
            else:
                if self.custom_camera_token:
                    token = self.custom_camera_token
                else:
                    token = self.parent.app.user_login.user_token
                self._stop_camera_capture_process()
                module_name = "bambulab_lan_cam.py"
                module_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), module_name)
                start_command = [sys.executable, module_path, token, self.parent.app.host_id, self.host,
                                 self.password, str(self._get_number_by_ip())]
                try:
                    if platforms.get_platform() == 'win':
                        self.camera_process = subprocess.Popen(start_command, close_fds=True,
                                                             creationflags=subprocess.CREATE_NO_WINDOW)
                    else:
                        self.camera_process = subprocess.Popen(start_command, close_fds=True)
                except Exception as e:
                    self.logger.warning('Could not launch Bambulab camera due to error: ' + str(e))

    def _stop_camera_capture_process(self):
        self.logger.info('Terminating BambuLab camera subprocess...')
        counter = self.CAMERA_CLOSE_TIMEOUT
        while counter and self.camera_process:
            try:
                self.camera_process.terminate()
                time.sleep(0.1)
            except:
                self.logger.error("Error terminating camera process")
            try:
                if self.camera_process.poll() != None:
                    self.camera_process = None
                    break
            except (OSError, AttributeError):
                self.camera_process = None
                break
            counter -= 1
            time.sleep(1)
        if self.camera_process:
            self.logger.info('Sending kill signal to BambuLab camera process...')
            try:
                self.camera_process.kill()
            except:
                pass
            time.sleep(1)  # give subprocess a time to go down
        self.logger.info('...camera subprocess terminated.')
        self.camera_process = None

    def _update_camera_url(self, url_to_add: str, retries_left: int = 2) -> None:
        existing_urls = ''
        try:
            existing_urls = open(paths.CAMERA_URLS_FILE, 'r').read()
        except FileNotFoundError:
            existing_urls = ''
        except OSError:
            self.logger.error('Unable to read camera urls file')
            if retries_left:
                retries_left -= 1
                self._update_camera_url(url_to_add, retries_left)
        if url_to_add not in existing_urls:
            try:
                with open(paths.CAMERA_URLS_FILE, 'a') as file:
                    if existing_urls and not existing_urls.endswith('\n'):
                        url_to_add = '\n' + url_to_add
                    file.write(url_to_add)
            except OSError:
                self.logger.error('Unable to write camera urls file')
                if retries_left:
                    retries_left -= 1
                    self._update_camera_url(url_to_add, retries_left)

    def _on_connect(self, client, userdata, flags, rc) -> None:
        self.logger.info(f"Bambulab: Connected to {self.host} with result code {rc}")
        if rc != 0:
            if rc in self.CONNECT_RC_DESC:
                desc = self.CONNECT_RC_DESC[rc]
            else:
                desc = "Return code " + str(rc)
            message = f"Error connecting Bambu Lab Printer on {self.host}. Description: {desc}"
            self.logger.error(message)
            self.client.disconnect()
            self.register_error(1100, message, is_blocking=True)
            return
        client.subscribe(self.mqtt_device_prefix + 'report')
        self._send_get_version()
        if not self.status_thread.is_alive():
            self.status_thread.start()
        else:
            self.logger.warning("Status thread already running. Skip")
        if self.camera_url:
            self._update_camera_url(self.camera_url)
            if self.parent:
                self.parent.restart_camera()
        else:
            self._start_camera_capture_process()

    def _on_disconnect(self, client, userdata, rc):
        self._stop_camera_capture_process()
        if not self.stop_flag:
            self.operational_flag = False
            self.stop_flag = True
            self.parent.register_error(11199, "BambuLab had disconnected. Joining status thread", is_blocking=False, is_critical=True)
            if self.status_thread and self.status_thread.is_alive():
                self.status_thread.join(10)
            self.logger.info(f"BambuLab: Disconnected from {str(self.host)}. with return code : {rc}")

    def is_bed_not_clear(self):
        return self.bed_not_clear

    def _get_sequence_id(self):
        with self.sequence_lock:
            self.current_sequence_id += 1
            return self.current_sequence_id

    def check_preconnect_printer_job(self):
        if self.preconnect_printer_job_id:
            if self.printers_job_id != self.preconnect_printer_job_id:
                self.save_current_printer_job_id(None)
                self.register_error(999,
                                    'After reconnection to the printer, it is not running cloud print job. Assuming that job had failed, when print was offline.',
                                    is_blocking=True)
            self.preconnect_printer_job_id = None

    def _on_message(self, client, userdata, message):
        try:
            response = json.loads(message.payload.decode())
            if response:
                self.logger.debug(f"Received from {self.host}. Desc: {json.dumps(response)}")
                print_sect = response.get("print")
                if print_sect and isinstance(print_sect, dict):
                    print_err_code = print_sect.get("print_error", 0)
                    if print_err_code in bambulab_errors.ALL_ERRORS:
                        if self.parent and print_err_code == bambulab_errors.LOCAL_CANCEL_CODE:
                            self.parent.cancel_locally()
                        else:
                            abort_job = False
                            if print_err_code in bambulab_errors.ERROR_CODES_JOB_FAILED:
                                abort_job = True
                            self.register_error(int(print_err_code), bambulab_errors.ALL_ERRORS[print_err_code],
                                                is_blocking=abort_job, is_info=not abort_job)
                    elif print_err_code != 0:
                        self.logger.warning(f"Received undefined print error code: {print_err_code}")
                    if print_sect.get("command") == "get_version":
                        self.logger.info(f"Printer version: {response}")
                    if print_sect.get("command") == "project_file":
                        self.logger.info(f"Received from {self.host}. Desc: {response}")
                        self.last_start_print_time = time.monotonic()
                    if print_sect.get("command") == "push_status":
                        self.operational_flag = True

                        # TODO: Discuss: in index 0 always must be external spool information (mostly user did not set it and it's undefined don't know even will we use it somehow in he future)
                        # external_spool = print_sect.get("vt_tray", {})
                        # self.material_names.append(external_spool.get("tray_type", ""))
                        # self.material_colors.append(external_spool.get("tray_color", ""))
                        # self.material_desc.append(external_spool.get("tray_sub_brands", ""))
                        if "ams" in print_sect:
                            # parse AMS materials names
                            ams = print_sect.get("ams", {}).get("ams", [])
                            self.has_ams = isinstance(ams, (list, dict)) and ams # can remove dict if it will always be list
                            material_names = []
                            material_colors = []
                            material_desc = []
                            if self.has_ams:
                                for item in ams:
                                    ams_trays = item.get("tray")
                                    if isinstance(ams_trays, (list, dict)): # can remove dict if it will always be list
                                        for tray in ams_trays:
                                            if isinstance(tray, dict):
                                                material_names.append(tray.get("tray_type", ""))
                                                material_colors.append(tray.get("tray_color", ""))
                                                material_desc.append(tray.get("tray_sub_brands", ""))
                            self.material_names = material_names
                            self.material_colors = material_colors
                            self.material_desc = material_desc
                        self.temps[0] = round(response.get("print").get("bed_temper", self.temps[0]), 2)
                        self.temps[1] = round(response.get("print").get("nozzle_temper", self.temps[1]), 2)
                        self.target_temps[0] = round(response.get("print").get("bed_target_temper", self.target_temps[0]), 2)
                        self.target_temps[1] = round(response.get("print").get("nozzle_target_temper", self.target_temps[1]), 2)
                        self.printer_has_sdcard = response.get("print").get("sdcard", self.printer_has_sdcard)
                        if "gcode_state" in response.get("print", {}): # need to check if key exist first for P1P model
                            gcode_state = response.get("print").get("gcode_state")
                            self.printing_flag = gcode_state == "RUNNING" or gcode_state == "PREPARE"
                            self.pause_flag = gcode_state == "PAUSE"
                            # self.bed_not_clear = gcode_state == "FINISH" # TODO: discuss, on FINISH state user can run a new job
                            if self.last_start_print_time and time.monotonic() - self.last_start_print_time > self.JOB_START_WAIT_TIMEOUT:
                                self.logger.warning("Wait time for job start elapsed. Clean flags ...")
                                self.wait_print_start = False
                                self.cancel_upload_to_printer_flag = False
                                self.last_start_print_time = None
                            if self.printing_flag or self.pause_flag:
                                self.wait_print_start = False
                                self.printers_job_id = print_sect.get("task_id", self.printers_job_id)
                                if (self.last_start_print_time and self.printers_job_id
                                        and time.monotonic() - self.last_start_print_time < self.JOB_START_WAIT_TIMEOUT):
                                    self.last_start_print_time = None
                                    if self.cancel_upload_to_printer_flag:
                                        self.cancel_upload_to_printer_flag = False
                                        # Little wait to not damage the printer with sudden cancellation (scary sound on P1S)
                                        time.sleep(self.LOOP_SLEEP_SEC)
                                        self.logger.info("Job was canceled during job uploading or starting. Canceling ...")
                                        self.cancel()
                                    else:
                                        self.save_current_printer_job_id(self.printers_job_id)
                                self.estimated_time = response.get("print").get("mc_remaining_time")
                                self.percent = response.get("print").get("mc_percent")
                                self.current_line_number = response.get("print").get("layer_num")
                            else:
                                self.percent = 0
                                self.current_line_number = 0
                                self.printers_job_id = None
                            self.check_preconnect_printer_job()
                else:
                    self.logger.debug(f"Can't parse response from {self.host}. Desc: {response}")

        except Exception as e:
            self.logger.error(f"Error updating printer info. Desc: {e}")

    def get_material_names(self) -> typing.List[str]:
        return self.material_names

    def get_material_colors_hex(self) -> typing.List[str]:
        return self.material_colors

    def get_material_desc(self) -> typing.List[str]:
        return self.material_desc

    def is_printing(self) -> bool:
        return self.printing_flag

    def _status_loop(self) -> None:
        while not self.stop_flag and self.client.is_connected():
            self._send_pushall()
            time.sleep(self.LOOP_SLEEP_SEC)
        self.stop_flag = True
        self.client.disconnect()
        self.client.loop_stop()
        self.logger.info('Disconnected and stopped status loop')
        if not self.stop_flag:
            self.register_error(1102, f"Status loop exit for BambuLab", is_blocking=True, is_info=True)

    def _connect_loop(self) -> None:
        self.logger.info(f"Connecting Bambu Lab printer on {self.host}")
        try:
            self.client.connect(self.host, self.MQTT_PORT)
            self.client.loop_start()
        except OSError as e:
            msg = f"Error connecting Bambu Lab printer on {self.host}. Description: {e}"
            self.logger.error(msg)
            if self.parent:
                self.parent.register_error(1002, msg)

    def _publish(self, msg: dict) -> bool:
        with self.publish_lock:
            self.logger.debug(f"publish msg: {json.dumps(msg)}")
            try:
                result = self.client.publish(self.mqtt_device_prefix + "request", json.dumps(msg))
                status = result[0]
            except Exception as e:
                self.logger.error(f"Publish with {msg} failed. Desc {str(e)}")
                return False
            if status == 0:
                self.logger.debug(f"Sent {msg} to topic {self.mqtt_device_prefix} request")
                return True
            msg = f"Failed to send message {msg} to topic {self.mqtt_device_prefix} request on {self.host}."
            self.logger.warning(msg)
            if self.parent:
                self.parent.register_error(1003, msg)
            return False

    def _send_print_command(self, command: str, param: typing.Union[str, dict] = None) -> bool:
        request = {"print": {"sequence_id": str(self._get_sequence_id()), "command": command}, "user_id": self.user_id}
        if param:
            request["print"]["param"] = param
        return self._publish(request)

    def _send_pushall(self) -> bool:
        request = {"pushing": {"sequence_id": str(self._get_sequence_id()), "command": "pushall"}, "user_id": self.user_id}
        return self._publish(request)

    def _send_get_version(self) -> bool:
        request = {"info": {"sequence_id": str(self._get_sequence_id()), "command": "get_version"}, "user_id": self.user_id}
        return self._publish(request)

    def pause(self) -> bool:
        return self._send_print_command("pause")

    def unpause(self) -> bool:
        return self._send_print_command("resume")

    def cancel(self) -> bool:
        if self.wait_print_start:
            self.cancel_upload_to_printer_flag = True
            return False
        success = self._send_print_command("stop")
        if success:
            self.save_current_printer_job_id(None)
            self.client.disconnect()
            self.register_error(1101, "Reconnecting after job cancel", is_blocking=True, is_info=True)
        return success

    def _create_project(self, filename) -> str:
        tmp_file = os.path.join(paths.DOWNLOAD_FOLDER, str(uuid.uuid4()) + '.3mf')
        try:
            with zipfile.ZipFile(tmp_file, 'w', zipfile.ZIP_DEFLATED) as zipf:
                zipf.write(filename, self.PROJECT_PRINT_PATH)
        except Exception as e:
            self.logger.error(f"3MF creation failed. Desc: f{str(e)}")
        else:
            return tmp_file

    def _create_ftp_tls(self) -> ftplib.FTP_TLS:
        if self.usb_info.get("PID") == "0P1P":
            return P1P_FTP_TLS()
        else:
            return X1_FTP_TLS()

    def _check_ftp_file(self, local_size: int, ftp_file: str) -> bool:
        try:
            ftp_session = self._create_ftp_tls()
            ftp_session.encoding = "utf-8"
            ftp_session.timeout = self.FTP_TIMEOUT
            ftp_session.connect(host=self.host, port=self.FTP_PORT, timeout=self.FTP_TIMEOUT)
            ftp_session.login(self.USERNAME, self.password)
            ftp_session.prot_p()
            ftp_session.cwd('/')
            for f in ftp_session.nlst():
                if ftp_file in f:
                    file_exists = True
                    break
            else:
                file_exists = False
            if not file_exists:
                self.logger.info(f"File {ftp_file} not found on FTP server")
                return False
            ftp_size = ftp_session.size(ftp_file)
            if ftp_size == 0 and ftp_size != local_size:
                self.logger.warning(f"File {ftp_file} has zero or different size on FTP server. ftp: {ftp_size} / local: {local_size}")
                return False
            self.logger.warning(f"File {ftp_file} found on FTP server with the same size, ftp: {ftp_size} / local: {local_size}")
            return True
        except ftplib.all_errors as err:
            msg = f"Check file on {self.host} failed. Description: {str(err)}"
            self.logger.warning(msg)
            if self.parent:
                self.parent.register_error(1006, msg)
        finally:
            try:
                ftp_session.quit()
            except:
                pass
        return False

    def _upload(self, project_src, file_name: str) -> bool:
        if self.printer_has_sdcard:
            tmp_size = os.path.getsize(project_src)
            self.logger.info(f"Start uploading {file_name} to {self.host}")
            try:
                with open(project_src, "rb") as f:
                    ftp_session = self._create_ftp_tls()
                    ftp_session.encoding = "utf-8"
                    ftp_session.timeout = self.FTP_TIMEOUT
                    ftp_session.connect(host=self.host, port=self.FTP_PORT, timeout=self.FTP_TIMEOUT)
                    ftp_session.login(self.USERNAME, self.password)
                    ftp_session.prot_p()
                    ftp_session.storbinary(f"STOR {file_name}", f)
                    self.logger.info(f"Successful uploaded {file_name} on {self.host}")
                    return True
            except ftplib.all_errors as err:
                self.logger.warning(f"Upload to printer finished with errors. Description: {str(err)}")
            except OSError as e:
                self.logger.error(f"Could not open/read file to upload. Description: {str(e)}")
            finally:
                try:
                    ftp_session.quit()
                except:
                    pass
            if self._check_ftp_file(tmp_size, file_name):
                return True
            msg = f"Upload to {self.host} failed."
            self.logger.warning(msg)
            if self.parent:
                self.parent.register_error(1005, msg, is_blocking=True)
        else:
            msg = f"Can't upload file on {self.host}. Description: no sdcard"
            self.logger.warning(msg)
            if self.parent:
                self.parent.register_error(1004, msg, is_blocking=True)
        return False

    # NOTE: command from Bambulab Studio API Source. Not working with files for print (Tested on X1C Carbon)
    def _run_gcode_file(self, sdcard_filepath):
        return self._send_print_command("gcode_file", sdcard_filepath)

    def unbuffered_gcodes(self, gcodes: str) -> bool:
        self.logger.info(f"Enter unbuffered_gcodes: {gcodes}")
        try:
            gcodes = self.preprocess_gcodes(gcodes)
        except Exception as e:
            self.logger.exception(e)
            return False
        success = True
        for command in gcodes:
            request = {"print": {"command": "gcode_line","param": command.decode('utf-8', errors="ignore") + "\n"}, "user_id": self.user_id}
            success &= self._publish(request)
        return success

    def detect_gcode_bed_type(self, gcode_file_path):
        bed_type_snake = self.AUTO_BED_TYPE
        try:
            with open(gcode_file_path, 'r') as file:
                for line in file:
                    if self.CURRENT_BED_ARG in line:
                        try:
                            bed_type = line.split('=')[1].strip()
                            bed_type_snake = self.BED_TYPES.get(bed_type)
                            if not bed_type_snake:
                                self.logger.warning('Unknown bed type: ' + str(bed_type))
                        except IndexError:
                            pass
                        break
        except Exception as e:
            self.logger.error('Error while getting bed type from a gcode file: ' + str(e))
        return bed_type_snake

    def gcodes(self, filepath: str, keep_file: bool = False) -> bool:
        # self.register_error(1366613, f"TEST PRINT OPTIONS PARAMS: {self.next_print_options}", is_blocking=True) # For Tests
        # self.next_print_options = None
        self.wait_print_start = True
        # need to catch errors of bad build plate type for X1/A1 series if build plate detect options is enabled
        bed_type = self.detect_gcode_bed_type(filepath)
        project_src = self._create_project(filepath)
        project_filename = self.get_filename() + '.3mf'
        if self._upload(project_src, project_filename):
            if not keep_file or self.keep_print_files:
                try:
                    os.remove(filepath)
                    os.remove(project_src)
                except:
                    pass
            task_id = self.get_clouds_job_id()
            if not task_id:
                task_id = str(random.randint(1000, 99999))
            time.sleep(self.LOOP_SLEEP_SEC)  # WORKAROUND: need little wait for P1S to start print
            request = {
                "print": {
                    "sequence_id": "50000", # WORKAROUND: this magic number should be used after update to 01.06.00.00 on P1S
                    "command": "project_file",
                    "bed_type": bed_type,
                    "profile_id": "0",
                    "project_id": "0",
                    "subtask_id": "0",
                    "task_id": task_id,
                    "file": project_filename,
                    "param": self.PROJECT_PRINT_PATH,
                    "subtask_name": self.get_filename(),
                    "url": f"ftp://{project_filename}",
                    "use_ams": self.has_ams and self.settings['use_ams']
                }
            }
            # FIXME: Temporary disable load settings from file before AMS support not released
            # if self.settings != self.DEFAULT_SETTINGS:
            #     request["print"].update(self.settings)
            if self.next_print_options:
                request["print"].update(self.next_print_options)
                self.next_print_options = None
            self.logger.info("Sending print start: " + json.dumps(request))
            return self._publish(request)
        if not keep_file or self.keep_print_files:
            try:
                os.remove(filepath)
                os.remove(project_src)
            except:
                pass
        self.next_print_options = None
        self.wait_print_start = False
        return False

    def camera_enable_hook(self, token=None):
        self.custom_camera_token = token
        if not self.camera_url:
            self._start_camera_capture_process()

    def camera_disable_hook(self):
        self._stop_camera_capture_process()

    def set_next_print_options(self, options: dict):
        self.next_print_options = options

    def close(self):
        self.stop_flag = True
        if self.camera_process:
            try:
                self.camera_process.terminate()
            except:
                pass
        if self.status_thread:
            try:
                self.status_thread.join(10)
            except RuntimeError:
                pass
        try:
            self.client.disconnect()
        except:
           pass
        time.sleep(0.5) # give some for _on_disconnect to run properly
        try:
            self.client.loop_stop()
        except:
           pass
        time.sleep(0.5) # give some for _on_disconnect to run properly
        self._stop_camera_capture_process()
