#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 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 os
import re
import time
import logging
import threading

import log
import pyusb_connection
from base_sender import BaseSender


class DremelConnection(pyusb_connection.PyUSBConnection):

    def format_message(self, raw_message):
        return "~" + str(raw_message) + "\r\n"


class Sender(BaseSender):

    INIT_GCODES = ["M115", "M650", "M114", "M105", "M119", "M105", "M104 S0 T0", "M104 S0 T1", "M140 S0"]
    FILE_PACKET_SIZE = 1024*4
    FILE_NAME = "3dprinteros.g3drem"
    STATE_READY = "MachineStatus: READY"
    STATE_CANCEL = "MachineStatus: CANCEL_BUILD"

    def __init__(self, parent, usb_info, profile):
        BaseSender.__init__(self, parent, usb_info, profile)
        self.paused = False
        self.monitoring_stop = False
        self.operational_flag = False
        self.printing_flag = False
        self.printing_started_flag = False
        self.lines_sent = 0
        self.percent = 0
        self.upload_percent = 0
        self.define_regexps()
        self.connection = DremelConnection(usb_info, profile)
        self.send_and_wait_lock = threading.Lock()
        self.handshake()
        self.connect()
        self.monitoring_thread = None
        self.monitoring_thread_start()

    def handshake(self):
        if not self.send_and_wait_for("M601 S0", "ok")[0]:
            raise RuntimeError("Cannot handshake printer and receive \"OK\"")

    def define_regexps(self):
        # T0:32 /0 T1:40 /0 B:38 /0\nok
        # T0:26 /0 B:0/0\nok\n
        # T0:200 /230 B:0/0\nok\n
        # T0:203 /220 B:0 /0\nok\n
        # T0:36 /0 B:0 /0\nok\n
        # T0:28/255 B:27/80 C:23 E:0ok
        self.temp_re = re.compile(
            '.*T0:([\d\.]+)[ ]*/([\d\.]+)(?:.*T1:(-?[\d\.]+)[ ]*/(-?[\d\.]+)|).*B:(-?[\d\.]+)[ ]*/(-?[\d\.]+).*',
            flags=re.DOTALL
        )
        self.progress_re = re.compile(".*SD printing byte (\d+)\/(\d+).*", flags=re.DOTALL)

    def connect(self):
        answer = ""
        for command in self.INIT_GCODES:
            ok, text = self.send_and_wait_for(command, "ok")
            answer += text
        if "ok" not in answer: #TODO check this
            raise RuntimeError("Cannot connect to printer and receive \"OK\"")

    def readout(self, wait_for=None):
        retries = 10
        full_answer = ""
        while not self.stop_flag:
            answer = self.connection.recv()
            if answer:
                full_answer += answer
                if wait_for and wait_for in answer:
                    return full_answer
            else:
                retries -= 1
                if (not wait_for and full_answer) or not retries:
                    return full_answer

    def send_and_wait_for(self, message, wait_for=None, endpoint=None, raw=False, timeout=None):
        with self.send_and_wait_lock:
            if self.connection.send(message, endpoint, raw, timeout):
                text = self.readout(wait_for)
                return text and wait_for in text, text
            return False, ""

    @log.log_exception
    def monitoring(self):
        self.logger.info("Monitoring thread started")
        count = 0
        previous_answer_states = [self.STATE_READY] # this variable used as protection against false error on print end
        while not self.monitoring_stop and not self.stop_flag:
            temps_ok, answer_temps = self.send_and_wait_for("M105", "ok")
            # protection from 'Cancelled manually' on printing start (order must be M27, M119)
            bytes_ok, answer_bytes = self.send_and_wait_for("M27", "ok")
            states_ok, answer_states = self.send_and_wait_for("M119", "ok")
            if not answer_temps and not answer_states and not answer_bytes: #TODO check if "and" is right operation here
                if count < 5:
                    count += 1
                    self.logger.info("Printer not answering on monitoring requests. Retry number %d out of 5" % count)
                    continue
                self.operational_flag = False
                message = "Printer not answering in monitoring. Printer lost."
                self.parent.register_error(606, message, is_blocking=True)
                break
            count = 0
            self.parse_temperature(answer_temps)
            self.operational_flag = True
            if self.STATE_READY in answer_states:
                if self.printing_flag and self.STATE_READY in previous_answer_states:
                    self.printing_flag = False
                    if self.printing_started_flag and self.lines_sent < self.total_gcodes:
                        self.parent.register_error(607, "Cancelled manually", is_blocking=True)
            elif self.printing_flag or self.STATE_CANCEL not in answer_states:
                self.printing_flag = True
                progress_match = self.progress_re.match(answer_bytes)
                if progress_match:
                    self.lines_sent = int(progress_match.group(1))
                    self.total_gcodes = int(progress_match.group(2))
                    if self.total_gcodes > 0:
                        self.percent = int(self.lines_sent / float(self.total_gcodes) * 100)
            time.sleep(1)
            previous_answer_states = answer_states
        self.logger.info("Monitoring thread stopped")

    def add_header(self, gcodes):
        current_dir = os.path.dirname(__file__)
        # header is just a dummy to display something on the print screen
        with open(os.path.join(current_dir, 'firmware', 'head_g3drem.bin'), "rb") as f:
            header = f.read()
        return header + gcodes

    def load_gcodes(self, gcodes):
        if self.operational_flag and not self.is_printing():
            if gcodes[:7] == 'g3drem ':
                self.logger.info('Build file already have g3drem header. Skip adding another one.')
            else:
                gcodes = self.add_header(gcodes)
            return self.upload_gcodes_and_print(gcodes)
        else:
            self.parent.register_error(604, "Printer already printing.", is_blocking=False)
            return False

    def unbuffered_gcodes(self, gcodes):
        self.logger.info("Gcodes to send now: " + str(gcodes))
        for gcode in self.preprocess_gcodes(gcodes):
            self.send_and_wait_for(gcode, "ok")
        self.logger.info("Gcodes were sent to printer")

    def upload_gcodes_and_print(self, gcodes):
        self.monitoring_thread_stop()
        self.percent = 0
        self.upload_percent = 0
        self.lines_sent = 0
        self.send_and_wait_for("M104 S0 T0", "ok")
        self.send_and_wait_for("M104 S0 T1", "ok")
        self.send_and_wait_for("M140 S0", "ok")
        file_size = len(gcodes)
        if not self.connection.send("M28 %d 0:/user/%s" % (file_size, self.FILE_NAME), None, False, 10000):
            self.parent.register_error(609, "Failed to execute M28 gcode", is_blocking=True)
            return False
        answers = ""
        while not self.stop_flag:
            answer = self.readout("ok")
            if not answer:
                message = "Failed start transfer file"
                self.parent.register_error(605, message, is_blocking=True)
                return False
            answers += answer
            if "ok" in answers and "Writing to file" in answers:
                self.logger.info("Received: Writing to file")
                break
            elif "open failed" in answers:
                message = "Transfer to printer failed"
                self.parent.register_error(602, message, is_blocking=True)
                return False
            elif "Disk read error" in answers:
                message = "Transfer to printer failed, error: \"Disk read error\""
                self.parent.register_error(608, message, is_blocking=True)
                return False
        chunk_start_index = 0
        counter = 0
        self.logger.info("Start uploading file...")
        while True:
            if self.stop_flag:
                return False
            chunk_end_index = min(chunk_start_index + self.FILE_PACKET_SIZE, file_size)
            chunk = gcodes[chunk_start_index:chunk_end_index]
            if not chunk:
                break
            if self.connection.send(chunk, endpoint=self.connection.file_transfer_endpoint_out, raw=True):
                counter += 1
                if counter > 4:
                    counter = 0
                    upload_percent = 100.0 * chunk_end_index / file_size
                    self.logger.info("Sent: %.2f%% %d/%d" % (self.upload_percent, chunk_end_index, file_size))
                    self.upload_percent = int(upload_percent)
            else:
                self.parent.register_error(603, "File transfer interrupted", is_blocking=True)
                return False
            chunk_start_index += self.FILE_PACKET_SIZE
        message = "File transfer interrupted"
        if self.send_and_wait_for("M29", "ok", timeout=10000)[0]:
            ok, answer = self.send_and_wait_for("M23 0:/user/%s" % self.FILE_NAME, "ok", timeout=10000)
            if not answer:
                message = "Printer not answered anything after send M23"
            elif "Disk read error" in answer:
                    message = "Disk read error"
            elif "File selected" in answer:
                self.logger.info("File transfer to printer successful")
                self.printing_flag = True
                self.printing_started_flag = True
                time.sleep(2)  # to double-protect from 'Cancelled manually' on printing start
                self.monitoring_thread_start()
                return True
        self.parent.register_error(604, message, is_blocking=True)
        return False

    def pause(self):
        if not self.paused:
            if self.send_and_wait_for("M25", "ok")[0]:
                self.paused = True
            else:
                return False

    def unpause(self):
        if self.paused:
            if self.send_and_wait_for("M24", "ok")[0]:
                self.paused = False
            else:
                return False

    def cancel(self):
        if self.send_and_wait_for("M26", "ok")[0]:
            self.pause_flag = False
            self.printing_flag = False
            if not self.send_and_wait_for("G1 X0 Y0 Z140", "ok")[0]:
                self.logger.info('Cancel error on G1')
            self.logger.info('Cancelled!')
        else:
            self.logger.info('Cancel error on M26')

    def is_operational(self):
        return self.operational_flag

    def is_paused(self):
        return self.paused

    def is_printing(self):
        return self.printing_flag

    def reset(self):
        self.connection.reset()

    def get_downloading_percent(self):
        percent = self.parent.downloader.get_percent() / 2
        if not self.percent:  # means that we started a new upload
            percent += self.upload_percent / 2
        return percent

    def get_percent(self):
        return self.percent

    def get_current_line_number(self):
        if self.printing_flag:
            return self.lines_sent
        else:
            return 0

    def parse_temperature(self, line):
        match = self.temp_re.match(line)
        if match:
            tool_temp_right = float(match.group(1))
            tool_target_temp_right = float(match.group(2))
            tool_temp_left = match.group(3)
            tool_target_temp_left = match.group(4)
            platform_temp = float(match.group(5))
            platform_target_temp = float(match.group(6))
            self.temps = [platform_temp, tool_temp_right]
            self.target_temps = [platform_target_temp, tool_target_temp_right]
            if tool_temp_left and tool_target_temp_left:
                self.temps.append(float(tool_temp_left))
                self.target_temps.append(float(tool_target_temp_left))
            return True

    def close(self):
        self.monitoring_thread_stop()
        self.send_and_wait_for("M602", "ok")
        self.stop_flag = True
        self.connection.close()

    def monitoring_thread_start(self):
        self.monitoring_stop = False
        self.monitoring_thread = threading.Thread(target=self.monitoring)
        self.monitoring_thread.start()

    def monitoring_thread_stop(self):
        self.monitoring_stop = True
        if self.monitoring_thread:
            self.monitoring_thread.join()

if __name__ == "__main__":
    logging.basicConfig(level=logging.DEBUG)
    your_file_name = "1.gcode" #replace me
    sender = None
    try:
        # with open('dremel_profile.json') as fp:
        #     sender = Sender(json.load(fp), {"VID": "2a89", "PID": "888b"})
        # time.sleep(3)
        # sender.cancel()

        sender = Sender({}, {"VID": "2a89", "PID": "8889"})
        time.sleep(3)
        # print ">>>START PRINTING!!!!"
        # with open(your_file_name, "rb") as f:
        #     your_file_content = f.read()
        # sender.load_gcodes(your_file_content)
    except Exception:
        if sender:
            sender.close()
    try:
        time.sleep(10)
    except:
        pass
    if sender:
        sender.close()
