# 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 json
import random
import ssl
import threading
import time
import typing

import paho.mqtt.client as mqtt

import config
import base_sender

class BaseMQTTSender(base_sender.BaseSender):
    MQTT_ACTION_QOS = 1
    MQTT_PORT = 1883
    LOOP_SLEEP_SEC = 5
    CONNECT_TIMEOUT = 6

    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"
    }

    def __init__(self, parent: typing.Any, usb_info: dict, profile: dict = {}, client_id: str = ""):
        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(client_id=client_id if client_id else None)
        self.client.on_connect = self._on_connect
        self.client.on_message = self._on_message
        self.client.on_disconnect = self._on_disconnect
        self.client.on_log = self._on_log
        self.last_message = ""
        self.current_sequence_id = 0
        self.publish_lock = threading.RLock()
        self.sequence_lock = threading.RLock()
        self.last_err_lock = threading.RLock()
        self.verbose = config.get_settings().get('verbose', False)
        self.status_thread = None

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

    def _on_connect(self, client, userdata, flags, rc) -> None:
        raise NotImplementedError

    def _on_disconnect(self, client, userdata, rc):
        raise NotImplementedError

    def _on_message(self, client, userdata, message):
        raise NotImplementedError

    def _on_log(self, client, userdata, level, buf):
        if self.verbose:
            self.logger.info(f"[LOG] level={level} msg={buf}")

    def _status_loop(self) -> None:
        raise NotImplementedError

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

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

    def close(self):
        self.stop_flag = True
        if self.status_thread:
            try:
                self.status_thread.join(10)
            except RuntimeError:
                pass
        try:
            self.client.disconnect()
        except:
            pass
        time.sleep(0.5)
        try:
            self.client.loop_stop()
        except:
            pass
        time.sleep(0.5)

    def save_current_job_filename(self, job_filename: str):
        self.logger.info(f'Saving current printer({self.parent}) job filename {job_filename}')
        self.update_printer_settings({'printer_job_filename': job_filename})

    def _wait_for_connect(self, error_message: str):
        start_time = time.time()
        while not self.operational_flag:
            if time.time() - start_time > self.CONNECT_TIMEOUT:
                self.close()
                raise RuntimeError(error_message)
            time.sleep(1)
