# 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 time
import zipfile

import config
import threaded_sender

from base_sender import BaseSender
from gcodes_buffer import GcodesBuffer


class Sender(threaded_sender.Sender):

    # 8.3 is mandatory for some file some firmwares, but not RepRapFirmware
    SD_CARD_FILENAME = b"printros.gco"
    LIST_SD_CARD_GCODE = b"M20"
    INITIALIZE_SD_CARD_GCODE = b"M21"
    RELEASE_SD_CARD_GCODE = b"M22"
    SELECT_SD_CARD_FILE_GCODE = b"M23"
    START_OR_RESUME_SD_CARD_PRINT_GCODE = b"M24"
    PAUSE_SD_CARD_PRINT_GCODE = b"M25"
    SET_SD_CARD_PRINT_POSITION_GCODE = b"M26"
    REPORT_SD_CARD_PRINT_STATUS_GCODE = b"M27"
    BEGIN_WRITING_SD_CARD_GCODE = b"M28"
    STOP_WRITING_SD_CARD_GCODE = b"M29"
    DELETE_SD_CARD_FILE_GCODE = b"M30"
    SELECT_FILE_AND_START_SD_CARD_PRINT_GCODE = b"M32"
    STOP_PRINT_GCODE = b"M0"

    # REPRAPFIRMWARE specific gcodes
    RETURN_FILE_INFORMATION_GCODE = b"M36"
    REPORT_SD_CARD_INFO_GCODE = b"M39"
    SIMULATION_MODE_GCODE = b"M37"
    SIMULATION_MODE_ON_GCODE = SIMULATION_MODE_GCODE + b" S1"
    SIMULATION_MODE_OFF_GCODE = SIMULATION_MODE_GCODE + b" S0"
    PAUSE_ON_REPRAPFIRMWARE_GCODE = b"M226"
    ALLOW_COLD_EXTR_GCODE = b"M302"
    REQUEST_JSON_REPORT_GCODE = b"M408"
    # END OF REPRAPFIRMWARE specific gcodes
    OK_WAIT_RETRY_PAUSE = 2
    CANCEL_TIMEOUT = 3

    TEMPR_REQ_GCODES = (REPORT_SD_CARD_PRINT_STATUS_GCODE, threaded_sender.Sender.GET_TEMP_GCODE, threaded_sender.Sender.REPORT_ENDSTOPS)

    END_OF_LINE = b"\n"
    PROGRESS_SEPARATOR = b"/"
    NOT_PRINTING = b"Not SD printing"

    AXIS_NAMES = ('X', 'Y', 'Z', 'U')

    def __init__(self, parent, usb_info, profile):
        super().__init__(parent, usb_info, profile)
        self.auto_unpause_repeat = False
        self.current_byte_pos = 0
        self.total_bytes = 0
        self.cancel_flag = False

    def gcodes(self, filepath, keep_file=False, only_upload=False):
        if self.is_printing():
            self.parent.register_error(260, "Error: already printing - unable to start a new print", is_blocking=False)
            return False
        read_block_size = config.get_settings().get('sdcard_upload_block_size', 4096) #16kB for ntfs and 32kB for fat32
        after_block_sleep = config.get_settings().get('sdcard_upload_after_block_sleep', 0.0)
        self.ok_timeout_enabled = False
        self.cancel_flag = False
        self.logger.info("Start loading gcodes...")
        self.stop_temp_requesting()
        self.flush_send_now_buffer()
        self.operational_flag = True
        self.upload_in_progress = True
        time.sleep(self.connection.read_timeout*2)
        self.thread_mode.clear()
        time.sleep(self.connection.read_timeout*2)
        self.operational_flag = True
        self.logger.info('Sending gcode to indicate begin on writing to sdcard')
        write_success = self.connection.send(self.BEGIN_WRITING_SD_CARD_GCODE + b" " + self.SD_CARD_FILENAME)
        if not write_success or not self.wait_ok_no_threads():
            self.parent.register_error(262, "Printer rejects gcodes upload", is_blocking=True)
            self.logger.error('Unable to get ok response to sdcard begin write gcode')
            self.thread_mode.set()
            self.upload_in_progress = False
            return False
        self.gcode_filepath = filepath
        bytes_count = 0
        lines_count = 0
        try:
            upload_begin_time = time.monotonic()
            self.logger.info('Opening file ' + filepath)
            with open(filepath, "rb") as f:
                self.logger.info('Writing blocks to printer...')
                while True:
                    if self.stop_flag or self.cancel_flag:
                        return False
                    block = f.read(read_block_size)
                    if block:
                        bytes_count += len(block)
                        lines_count += block.count(self.END_OF_LINE)
                        if not self.connection.send(block, raw=True):
                            self.logger.warning('Upload of a block had failed: ' + str(block))
                            self.parent.register_error(262, "Printer rejects gcodes upload", is_blocking=True)
                            self.upload_in_progress = False
                            return False
                        self.operational_flag = True
                        if after_block_sleep:
                            time.sleep(after_block_sleep)
                    else:
                        self.logger.info(f'Upload to sdcard complete. Lines/bytes {lines_count}/{bytes_count}')
                        break
        except OSError as e:
            self.logger.error('Error reading file: ' + str(e))
            self.parent.register_error(263, "Error on uploading gcodes", is_blocking=True)
            self.ok_timeout_enabled = self.initial_ok_timeout_status
            self.start_temp_requesting()
            self.thread_mode.set()
            self.upload_in_progress = False
            return False
        finally:
            if after_block_sleep:
                time.sleep(after_block_sleep*2)
            self.logger.info('Sending gcode to indicate finish on writing to sdcard. Total time: ' + str(time.monotonic() - upload_begin_time))
            write_success = self.connection.send(self.STOP_WRITING_SD_CARD_GCODE)
            if not write_success or not self.wait_ok_no_threads():
                self.parent.register_error(264, "Printer rejects gcode upload finish", is_blocking=True)
            if not self.keep_print_files and not keep_file:
                try:
                    os.remove(filepath)
                except:
                    pass
        if not only_upload:
            self.start_printing_sdcard_file()
            self.printing_flag = True
            self.total_bytes = bytes_count
            self.total_gcodes = lines_count
            self.filesize = bytes_count
        else:
            self.list_gcodes()
        self.auto_unpause_repeat = False
        self.ok_timeout_enabled = self.initial_ok_timeout_status
        self.operational_flag = True
        self.start_temp_requesting()
        self.thread_mode.set()
        self.upload_in_progress = False
        return True

    def start_printing_sdcard_file(self, filename=None):
        if not filename:
            filename = self.SD_CARD_FILENAME
        self.logger.info('Sending command to start printing from sdcard: ' + str(filename))
        self._send_now(self.SELECT_SD_CARD_FILE_GCODE + b" " + filename)
        if not self.sync_ok():
            time.sleep(self.OK_WAIT_RETRY_PAUSE)
            if not self.sync_ok():
                self.parent.register_error(263, "Error executing a print select gcode", is_blocking=True)
                return False
        self._send_now(self.START_OR_RESUME_SD_CARD_PRINT_GCODE)
        if not self.sync_ok():
            time.sleep(self.OK_WAIT_RETRY_PAUSE)
            if not self.sync_ok():
                self.parent.register_error(263, "Error executing a print start gcode", is_blocking=True)
                return False

    def list_gcodes(self):
        self._send_now(self.LIST_SD_CARD_GCODE + b' S3 P"/gcodes/"' + self.SD_CARD_FILENAME)

    def is_printing(self):
        return self.printing_flag

    def is_paused(self):
        return self.pause_flag

    def pause(self, no_gcodes=False, warning=True):
        if not self.is_printing():
            if warning:
                self.parent.register_error(254, "No print to pause", is_blocking=False)
        elif self.is_paused():
            if warning:
                self.parent.register_error(254, "Already in pause", is_blocking=False)
        else:
            if not no_gcodes:
                self.stop_temp_requesting()
                self.sync_ok(True)
                self._send_now(self.PAUSE_SD_CARD_PRINT_GCODE)
                if not self.sync_ok(timeout=2):
                    self.connection.send(self.PAUSE_SD_CARD_PRINT_GCODE)
                self.start_temp_requesting()
            self.pause_flag = True
            return True
        return False

    def unpause(self, no_gcodes=False, warning=True):
        if not self.is_printing():
            if warning:
                self.parent.register_error(254, "No print to resume", is_blocking=False)
        #  elif not self.is_paused():
        #      if warning:
        #          self.parent.register_error(254, "Can't resume - not paused", is_blocking=False)
        else:
            if not no_gcodes:
                self.stop_temp_requesting()
                self.sync_ok(True)
                self._send_now(self.START_OR_RESUME_SD_CARD_PRINT_GCODE)
                if not self.sync_ok(timeout=2):
                    self.connection.send(self.START_OR_RESUME_SD_CARD_PRINT_GCODE)
                self.start_temp_requesting()
            self.pause_flag = False
            return True
        return False

    def cancel(self):
        if self.is_printing():
            try:
                self.flush_send_now_buffer()
                if self.write_thread.buffer:
                    self.write_thread.buffer.clear()
            except:
                pass
            self.sync_ok(False)
            self.thread_mode.clear()
            if not self.pause_flag:
                if not self.connection.send(self.PAUSE_SD_CARD_PRINT_GCODE) or not self.wait_ok_no_threads():
                    self.logger.warning('Error sending print pause as a part of sdcard print abort')
            if not self.connection.send(self.STOP_PRINT_GCODE):
                self.sleep(self.OK_WAIT_RETRY_PAUSE)
                if not self.connection.send(self.STOP_PRINT_GCODE):
                    self.logger.warning('Error sending print abort gcode')
            timeout = self.CANCEL_TIMEOUT
            while not self.wait_ok_no_threads():
                if self.stop_flag:
                    return False
                if timeout < 0:
                    self.reset()
                else:
                    timeout -= self.write_thread.OK_WAITING_STEP
            self.pause_flag = False
            self.cancel_flag = True
            self.printing_flag = False
            self.write_thread.ready_for_command.set()
            self.thread_mode.set()
            self.logger.info("Successful cancel")
            return True
        self.logger.info("No print to cancel")
        return False

    def delete_sd_file(self, filename=None):
        if not filename:
            filename = self.SD_CARD_FILENAME
        self._send_now(self.DELETE_SD_CARD_FILE_GCODE + b" " + filename)

    def _parse_sdcard_printing_progress(self, line):
        if self.NOT_PRINTING in line:
            self.printing_flag = False
            self.pause_flag = False
        elif self.PROGRESS_SEPARATOR in line:
            try:
                before, after = line.split(self.PROGRESS_SEPARATOR)
                current_bytes_int, total_bytes = int(before.split()[-1].strip()), int(after.split()[0])
            except (TypeError, ValueError, IndexError):
                self.logger.error('Error parsing SD print progress line: ' + line.decode(errors='ignore'))
            else:
                if current_bytes_int and total_bytes:
                    # there is a bug in RepRapFirmware that can produce lines like this
                    # "SD printing byte 4294967268/629776"
                    # so two lines below are act as protection against this
                    if current_bytes_int > total_bytes:
                        current_bytes_int = 0
                    if current_bytes_int != self.current_byte_pos:
                        self.current_byte_pos = current_bytes_int
                        if self.pause_flag:
                            if self.auto_unpause_repeat:
                                self.logger.info('SD card print progress detected. Dropping pause flag')
                                self.pause_flag = False
                                self.auto_unpause_repeat = False
                            else:
                                self.logger.info('SD card print progress detected. Ignoring resume in until repeat.')
                                self.auto_unpause_repeat = True
                    self.total_bytes = total_bytes
                self.printing_flag = True
        else:
            self.logger.info('Unknown SD print progress line: ' + line.decode(errors='ignore'))

    def get_percent(self):
        if self.is_printing():
            if self.total_bytes and self.current_byte_pos:
                return round(self.current_byte_pos / self.total_bytes * 100, 2)
        return 0.0
