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

    # REPRAPFIRMWARE specific gcodes
    REPORT_SD_CARD_INFO_GCODE = b"M39"

    PAUSE_ON_REPRAPFIRMWARE_GCODE = b"M226"

    SIMULATION_MODE_GCODE = b"M37"
    SIMULATION_MODE_ON_GCODE = SIMULATION_MODE_GCODE + b" S1"
    SIMULATION_MODE_OFF_GCODE = SIMULATION_MODE_GCODE + b" S0"
    ALLOW_COLD_EXTR_GCODE = b"M302"

    RETURN_FILE_INFORMATION_GCODE = b"M36"
    REQUEST_JSON_REPORT_GCODE = b"M408"
    # END OF REPRAPFIRMWARE specific gcodes

    TEMPR_REQ_GCODES = (REPORT_SD_CARD_PRINT_STATUS_GCODE, threaded_sender.Sender.GET_TEMP_GCODE)

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

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

    def gcodes(self, filepath, keep_file=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', 16384) #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.logger.info('Sending gcode to indicate begin on writing to sdcard')
        self._send_now(self.BEGIN_WRITING_SD_CARD_GCODE + b" " + self.SD_CARD_FILENAME)
        self.gcode_filepath = filepath
        self.logger.info('Opening file and writing block to printer...')
        bytes_count = 0
        lines_count = 0
        try:
            self.write_thread.ready_for_command.clear()
            with open(filepath, "rb") as f:
                while True:
                    if self.stop_flag or self.cancel_flag:
                        self.connection.send(self.STOP_WRITING_SD_CARD_GCODE)
                        self.ok_timeout_enabled = self.initial_ok_timeout_status
                        return False
                    block = f.read(read_block_size)
                    if block:
                        bytes_count += len(block)
                        lines_count += block.count(self.END_OF_LINE)
                        self.connection.send(block, raw=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 loading file: ' + str(e))
            self.ok_timeout_enabled = self.initial_ok_timeout_status
            return False
        finally:
            self.logger.info('Sending gcode to indicate finish on writing to sdcard')
            if after_block_sleep:
                time.sleep(after_block_sleep*2)
            self.connection.send(self.STOP_WRITING_SD_CARD_GCODE)
            if not self.keep_print_files and not keep_file:
                try:
                    os.remove(filepath)
                except:
                    pass
        self.start_printing_sdcard_file()
        self.start_temp_requesting()
        self.total_bytes = bytes_count
        self.total_gcodes = lines_count
        self.filesize = bytes_count
        self.printing_flag = True
        self.ok_timeout_enabled = self.initial_ok_timeout_status
        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)
        self._send_now(self.START_OR_RESUME_SD_CARD_PRINT_GCODE)

    def is_printing(self):
        return self.printing_flag

    def is_paused(self):
        return self.pause_flag

    def pause(self, _=False):
        if not self.is_printing():
            self.parent.register_error(254, "No print to pause", is_blocking=False)
        elif self.is_paused():
            self.parent.register_error(254, "Already in pause", is_blocking=False)
        else:
            self._send_now(self.PAUSE_SD_CARD_PRINT_GCODE)
            self.pause_flag = True
            return True
        return False

    def unpause(self, _=False):
        if not self.is_printing():
            self.parent.register_error(254, "No print to resume", is_blocking=False)
        elif not self.is_paused():
            self.parent.register_error(254, "Can't resume - not paused", is_blocking=False)
        else:
            self._send_now(self.START_OR_RESUME_SD_CARD_PRINT_GCODE)
            self.pause_flag = False
            return True
        return False

    def cancel(self):
        if self.is_printing():
            try:
                if self.write_thread:
                    self.flush_send_now_buffer()
                    if self.write_thread.buffer:
                        self.write_thread.buffer.clear()
            except:
                pass
            if not self.pause_flag:
                self._send_now(self.PAUSE_SD_CARD_PRINT_GCODE)
            self._send_now(b"M0")
            self.pause_flag = False
            time.sleep(1)
            self.sync_ready_for_command(True)
            self.cancel_flag = True
            self.printing_flag = False
            return True
        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
        else:
            if 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])
                    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 aganst this
                        if current_bytes_int > total_bytes:
                            current_bytes_int = 0
                        self.current_byte_pos = current_bytes_int
                        self.total_bytes = total_bytes
                    self.printing_flag = True
                except:
                    self.logger.error('Error parsing sd progress line: ' + str(line))

    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
