# 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 json
import logging
import threading

from PySide6 import QtCore

import config


class WizardModel(QtCore.QObject):

    FILAMENT_PROFILE_FILE = "filament_profiles/%s.json" % config.get_settings()['vendor']['name']
    FILAMENT_FEEDRATE = 10
    FILAMENT_STEP = 1
    FILAMENT_EXTRUSION_TIMEOUT = 300

    HEATING_MONITORING_PERIOD = 2
    HEATING_HYSTERESIS = 3 # +- degrees C
    HEATING_TIMEOUT = 300
    HEATING_GCODE = "M104"

    EXTRUDE_FILAMENT_GCODE = "G0 E%f F%d" % (FILAMENT_STEP, FILAMENT_FEEDRATE)
    PULL_FILAMENT_GCODE = "G0 E-%f F%d" % (FILAMENT_STEP, FILAMENT_FEEDRATE)

    GRID_CALIBRATION_STAGE1_GCODES = "G28\nG32\nM500\nG1 X100 Y100"
    GRID_CALIBRATION_STAGE3_GCODES = "M306 Z0\nM500\nG28\nM374"

    on_filement_profiles = QtCore.Signal()
    heating_done = QtCore.Signal()
    timeout = QtCore.Signal()
    start_grid_calibration_stage2 = QtCore.Signal()
    grid_calibration_done = QtCore.Signal()

    def __init__(self, core_model):
        super().__init__()
        self.logger = logging.getLogger(__name__)
        self._coreModel = core_model
        self._app = core_model._app
        self.execution_thread = None
        self.running_wizard = None
        self.load_filament_profiles()
        self.execution_lock = threading.Lock()
        self.heating_monitoring_thread = threading.Thread(target = self.heating_monitoring)
        self.heating_monitoring_thread.start()

    def load_filament_profiles(self):
        try:
            with open(self.FILAMENT_PROFILE_FILE) as f:
                self._filament_profiles = json.load(f)
        except OSError:
            self.logger.warning("No filament profile file found: " + self.FILAMENT_PROFILE_FILE)
            self._filament_profiles = {}

    def heating_monitoring(self):
        self.awaiting_temperature = None
        self.awaiting_temperature_index = None
        while not self._app.stop_flag:
            time.sleep(self.HEATING_MONITORING_PERIOD)
            if self.awaiting_temperature is not None and self.awaiting_temperature_index is not None:
                if time.time() - self.heating_start_time > self.HEATING_TIMEOUT:
                    self.finish()
                    self.timeout.emit()
                    self.logger.warning("Timeout while heating")
                    return
                try:
                    printer_interface = self._app.qt_interface.printer_model._getPrinterInterface()
                    temperatures = printer_interface.sender.get_temps()
                except:
                    self.logger.error("Error while getting temperatures list")
                else:
                    current_temperature = temperatures[self.awaiting_temperature_index + 1]
                    # if we ever need to wait for heater to cool, we will need to add another check here
                    if current_temperature > (self.awaiting_temperature - self.HEATING_HYSTERESIS):
                        self.heating_done.emit()
                        self.awaiting_temperature = None
                        self.awaiting_temperature_index = None

    def moving_filament(self, extrusion_direction, extruder_index):
        self.move_filement_start_time = time.time()
        self.move_filament_flag = True
        self.execute_gcodes("T" + str(extruder_index))
        if extrusion_direction:
            callback = self.extrude_filament()
        else:
            callback = self.pull_filament()
        self.register_callback(callback)
        while not self._app.stop_flag and self.move_filament_flag:
            if time.time() - self.move_filement_start_time > self.FILAMENT_EXTRUSION_TIMEOUT:
                self.finish()
                self.timeout.emit()
                self.logger.warning("Timeout while pushing or pulling filament")
                return
            time.sleep(1)

    def extrude_filement(self):
        if self.move_filament_flag:
            self.register_callback(self.extrude_filement)
            self.execute_gcodes(self.EXTRUDE_FILAMENT_GCODE)

    def pull_filement(self):
        if self.move_filament_flag:
            self.register_callback(self.pull_filement)
            self.execute_gcodes(self.PULL_FILAMENT_GCODE)

    def start_filament_move_thread(self, step, extruder_index):
        self.filament_moving_thread = threading.Thread(target=self.move_filament, args=(step, extruder_index))
        self.filament_moving_thread.start()

    def wait_for_grid_calibration_stage1_end(self, reply, executed_gcode_line):
        if self.GRID_CALIBRATION_STAGE1_GCODES.split("\n")[-1] in executed_gcode_line:
            self.flush_ok_callbacks()
            self.logger.info("End of grid calibraition stage1. Starting stage2...")
            self.start_grid_calibration_stage2.emit()

    def wait_for_grid_calibration_stage3_end(self, reply, executed_gcode_line):
        if self.GRID_CALIBRATION_STAGE3_GCODES.split("\n")[-1] in executed_gcode_line:
            self.flush_ok_callbacks()
            self.logger.info("End of grid calibraition stage3. Grid calibration done.")
            self.grid_calibration_done.emit()

    @QtCore.Property('QVariantList', notify=on_filement_profiles)
    def filament_profiles(self):
        filament_list_for_qml = []
        for key in self._filament_profiles:
            filament_list_for_qml.append({"name": key, "temp": self._filament_profiles[key]})
        return filament_list_for_qml

    @QtCore.Slot(str, int)
    def heatup_for_filament(self, filament_name, extruder_index=0):
        if not filament_name in self._filament_profiles:
            self.logger.error("Unknown filament type: %s", filament_name)
        else:
            target_temperarture = self._filament_profiles[filament_name]
            gcode = self.HEATING_GCODE + " T%d S%d" % (extruder_index, target_temperarture)
            self.execute_gcodes(gcode)
            self.heating_start_time = time.time()
            self.awaiting_temperature = target_temperarture
            self.awaiting_temperature_index = extruder_index

    @QtCore.Slot(int)
    def start_pulling_filament(self, extruder_index=0):
        self.start_filament_moving_thread(False, extruder_index)

    @QtCore.Slot(int)
    def start_pushing_filament(self, extruder_index=0):
        self.start_filament_moving_thread(True, extruder_index)

    @QtCore.Slot(str)
    def execute_gcodes(self, gcodes):
        # TODO: came up with more reliable way to get printer interface and get support for several printers interfaces
        self.logger.info("Executing gcodes:" + str(gcodes))
        with self.execution_lock:
            printer_interface = self.get_printer_interface()
            if printer_interface:
                printer_interface.printer.unbuffered_gcodes(gcodes)

    @QtCore.Slot(str)
    def start_grid_calibration(self):
        self.logger.info("Starting grid calibration")
        self.register_callback(self.wait_for_grid_calibration_stage1_end)
        self.execute_gcodes(self.GRID_CALIBRATION_STAGE1_GCODES)

    @QtCore.Slot(str)
    def start_grid_calibration_stage3(self):
        self.logger.info("Starting grid calibration stage3")
        self.register_callback(self.wait_for_grid_calibration_stage3_end)
        self.execute_gcodes(self.GRID_CALIBRATION_STAGE3_GCODES)

    @QtCore.Slot()
    def finish(self):
        self.move_filament_flag = False
        self.execute_gcodes("M104 T0 S0")
        self.execute_gcodes("M104 T1 S0")
        pi = self._app.qt_interface.printer_model._getPrinterInterface()
        if pi and pi.sender:
            pi.sender.flush_ok_callbacks()
        else:
            self.logger.warning("Warning: unable for get printer interface to flush ok callbacks.")
        self.logger.info("Wizard finished.")
