# 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):

    SD_CARD_FILENAME = b"printros.gcode"
    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"
    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"

    # 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

    END_OF_LINE = b"\n"

    def __init__(self, parent, usb_info, profile):
        super().__init__(parent, usb_info, profile)
        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", job_fail=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", job_fail=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", job_fail=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", job_fail=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", job_fail=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.sdcard_printing = 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", job_fail=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", job_fail=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.sdcard_printing

    def is_paused(self):
        return self.pause_flag

    def cancel(self):
        self.cancel_flag = True
        self.cancel_sdcard_print()

    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)
