#Copyright (c) 2016 3D Control Systems LTD

# Author: Vladimir Avdeev <another.vic@yandex.ru>

import re
import time
import logging
import threading
import makerbot_driver
import collections
import serial
import serial.serialutil

import log
from base_sender import BaseSender


class Sender(BaseSender):

    PAUSE_STEP_TIME = 0.5
    BUFFER_OVERFLOW_WAIT = 0.01
    IDLE_WAITING_STEP = 0.1
    TEMP_UPDATE_PERIOD = 5
    GODES_BETWEEN_READ_STATE = 100
    BUFFER_OVERFLOWS_BETWEEN_STATE_UPDATE = 20
    MAX_RETRY_BEFORE_ERROR = 100

    def __init__(self, parent, usb_info, profile):
        BaseSender.__init__(self, parent, usb_info, profile)
        self.buffer = collections.deque()
        self.logger = logging.getLogger(__name__)
        self.logger.info('Makerbot printer created: %s' % usb_info)
        self.init_target_temp_regexps()
        self.execution_lock = threading.Lock()
        self.buffer_lock = threading.Lock()
        self.parser = None
        retries = 5
        self.sending_thread = None
        while not self.sending_thread:
            try:
                self.parser = self.create_parser()
                time.sleep(0.1)
                self.parser.state.values["build_name"] = '3DPrinterOS'
            except Exception as e:
                if retries > 0:
                    retries -= 1
                    self.logger.warning("Error connecting to printer %s\n%s" % (str(profile), str(e)))
                    time.sleep(1)
                else:
                    raise RuntimeError("Error connecting to printer %s\n%s" % (str(profile), str(e)))
            else:
                self.stop_flag = False
                self.pause_flag = False
                self.printing_flag = False
                self.cancel_flag = False
                self.sending_thread = threading.Thread(target=self.send_gcodes)
                self.sending_thread.start()

    def create_parser(self):
        factory = makerbot_driver.MachineFactory()
        machine = factory.build_from_port(self.usb_info.get('COM'))
        assembler = makerbot_driver.GcodeAssembler(machine.profile)
        parser = machine.gcodeparser
        start, end, variables = assembler.assemble_recipe()
        parser.environment.update(variables)
        return parser

    def init_target_temp_regexps(self):
        self.platform_ttemp_regexp = re.compile('\s*M109\s*S(\d+)\s*T(\d+)')
        self.extruder_ttemp_regexp = re.compile('\s*M104\s*S(\d+)\s*T(\d+)')

    def append_position_and_lift_extruder(self):
        position = self.request_position_from_printer()
        if position:
            with self.buffer_lock:
                self.buffer.appendleft('G1 Z' + str(position[2]))
            z = min(160, position[2] + 30)
            self.execute('G1  Z' + str(z))

    # length argument is used for unification with Printrun. DON'T REMOVE IT!
    def set_total_gcodes(self, length):
        self.execute(lambda: self.parser.s3g.abort_immediately())
        self.parser.state.values["build_name"] = '3DPrinterOS'
        self.parser.state.percentage = 0
        self.current_line_number = 0
        self.logger.info('Begin of GCodes')
        self.printing_flag = False
        self.execute(lambda: self.parser.s3g.set_RGB_LED(255, 255, 255, 0))

    def load_gcodes(self, gcodes):
        self.set_total_gcodes(len(gcodes))
        with self.buffer_lock:
            for code in gcodes:
                self.buffer.append(code)
        self.logger.info('Enqueued block: ' + str(len(gcodes)) + ', of total: ' + str(len(self.buffer)))

    def cancel(self, go_home=True):
        with self.buffer_lock:
            self.buffer.clear()
        self.pause_flag = False
        self.cancel_flag = True
        time.sleep(0.1) #TODO investigare is this sleep really need to be here
        self.execute(lambda: self.parser.s3g.abort_immediately())
        if self.profile.get('force_table_down_on_cancel'):
            self.execute(lambda: self.parser.s3g.find_axes_maximums(['z'], 500, 60))

    def pause(self):
        if not self.pause_flag and not self.cancel_flag:
            self.pause_flag = True
            time.sleep(0.1)
            self.append_position_and_lift_extruder()
            return True
        else:
            return False

    def unpause(self):
        if self.pause_flag and not self.cancel_flag:
            self.pause_flag = False
            return True
        else:
            return False

    def request_position_from_printer(self):
        position = self.parser.state.position.ToList()
        if position[0] is None or position[1] is None or position[2] is None:
            #self.logger.warning("Can't get current tool position to execute extruder lift") #spam
            pass
        else:
            # setting to xyz sequence format, zero is for compatibility
            # position[3] - A-gcode value from 'G1 X-12.440 Y-12.948 A5.53429'-like string.
            # TODO: check if position[3] is extrusion value. It seems like it is (increases during printing)
            self.position = [position[0], position[1], position[2], 0]  # XYZE format
            return self.position

    def get_position(self):
        self.request_position_from_printer()
        return self.position

    def emergency_stop(self):
        self.cancel(False)

    def immediate_pause(self):
        self.execute(self.parser.s3g.pause())

    def close(self):
        self.logger.info("Makerbot sender is closing...")
        self.stop_flag = True
        if threading.current_thread() != self.sending_thread:
            self.sending_thread.join(10)
            if self.sending_thread.is_alive():
                self.parent.register_error(301, "Failed to join printing thread in makerbot_printer",
                                           disconnect=True)
        if self.parser:
            if self.parser.s3g:
                try:
                    self.parser.s3g.abort_immediately()
                except Exception:
                    pass
                time.sleep(0.1)
                self.parser.s3g.close()
        self.logger.info("...done closing makerbot sender.")

    def unbuffered_gcodes(self, gcodes):
        self.logger.info("Gcodes for unbuffered execution: " + str(gcodes))
        if self.printing_flag or self.pause_flag:
            self.logger.warning("Can't execute gcodes - wrong mode")
            return False
        else:
            if not self.parser.state.values.get("build_name"):
                self.parser.state.values["build_name"] = '3DPrinterOS'
            for gcode in self.preprocess_gcodes(gcodes):
                result = self.execute(gcode)
                if result:
                    self.request_position_from_printer()
                    self.logger.info("Printers answer: " + result)
                    with self.recv_callback_lock:
                        for callback in self.recv_callback:
                            callback(result)
            self.logger.info("Gcodes were sent to printer")
            return True

    def execute(self, command):
        buffer_overflow_counter = 0
        retry_count = 0
        while not self.stop_flag:
            if buffer_overflow_counter > self.BUFFER_OVERFLOWS_BETWEEN_STATE_UPDATE:
                self.logger.debug('Makerbot BufferOverflow on %s' % command)
                buffer_overflow_counter = 0
                self.read_state()
            text = ""
            try:
                self.execution_lock.acquire()
                if isinstance(command, bytes):
                    command = command.decode("utf-8")
                command_is_gcode = isinstance(command, str)
                if command_is_gcode:
                    if self.cancel_flag:
                        self.cancel_flag = False
                        break
                    text = command
                    self.printing_flag = True
                    self.parser.execute_line(command)
                    self.set_target_temps(command)
                    #self.logger.debug("Executing: " + command)
                    result = None
                else:
                    text = command.__name__
                    result = command()
            except makerbot_driver.BufferOverflowError:
                buffer_overflow_counter += 1
                time.sleep(self.BUFFER_OVERFLOW_WAIT)
            except serial.serialutil.SerialException:
                self.logger.warning("Makerbot is retrying %i %s" % (retry_count, text))
                retry_count += 1
                if retry_count > self.MAX_RETRY_BEFORE_ERROR:
                    self.parent.register_error(302, "Makerbot can't continue because max retry of %s" % text,
                                               disconnect=True)
                    self.stop_flag = True
            except makerbot_driver.BuildCancelledError:
                self.parent.register_error(305, "Makerbot build canceled locally",
                                           job_fail=True)
                self.stop_flag = True
            except UnicodeDecodeError:
                self.parent.register_error(304, "Non ascii or unicode bytes in gcodes: %s" % command)
                retry_count += 1
            except Exception as e:
                self.parent.register_error(303, "Makerbot can't continue because of: %s " % str(e),
                                           disconnect=True)
                self.logger.exception("Traceback:")
                self.stop_flag = True
            else:
                return result
            finally:
                self.execution_lock.release()

    def read_state(self):
        platform_temp          = self.execute(lambda: self.parser.s3g.get_platform_temperature(1))
        platform_ttemp         = self.execute(lambda: self.parser.s3g.get_platform_target_temperature(1))
        head_temp1  = self.execute(lambda: self.parser.s3g.get_toolhead_temperature(0))
        head_temp2 = self.execute(lambda: self.parser.s3g.get_toolhead_temperature(1))
        head_ttemp1 = self.execute(lambda: self.parser.s3g.get_toolhead_target_temperature(0))
        head_ttemp2 = self.execute(lambda: self.parser.s3g.get_toolhead_target_temperature(1))
        self.temps = [platform_temp, head_temp1, head_temp2]
        self.target_temps = [platform_ttemp, head_ttemp1, head_ttemp2]
        # self.mb            = self.execute(lambda: self.parser.s3g.get_motherboard_status())
        #self.position      = self.execute(lambda: self.parser.s3g.get_extended_position())

    def reset(self):
        self.buffer.clear()
        self.execute(lambda: self.parser.s3g.reset())
        self.execute(lambda: self.parser.s3g.clear_buffer())

    def is_paused(self):
        return self.pause_flag

    def is_operational(self):
        return self.parser and self.parser.s3g.is_open() and self.sending_thread.is_alive()

    def set_target_temps(self, command):
        result = self.platform_ttemp_regexp.match(command)
        if result:
            self.target_temps[0] = int(result.group(1))
            self.logger.info('Heating platform to ' + str(result.group(1)))
        result = self.extruder_ttemp_regexp.match(command)
        if result:
            extruder_number = int(result.group(2)) + 1
            self.target_temps[extruder_number] = int(result.group(1))
            self.logger.info('Heating toolhead ' + str(extruder_number) + ' to ' + str(result.group(1)))

    @log.log_exception
    def send_gcodes(self):
        last_time = time.time()
        counter = 0
        while not self.stop_flag:
            counter += 1
            current_time = time.time()
            if (counter >= self.GODES_BETWEEN_READ_STATE) or (current_time - last_time > self.TEMP_UPDATE_PERIOD):
                counter = 0
                last_time = current_time
                self.read_state()
            if self.pause_flag:
                self.printing_flag = False
                time.sleep(self.PAUSE_STEP_TIME)
                continue
            try:
                if not self.buffer_lock.acquire(False):
                    raise RuntimeError
                command = self.buffer.popleft()
            except RuntimeError:
                time.sleep(self.IDLE_WAITING_STEP)
            except IndexError:
                self.buffer_lock.release()
                if self.execute(lambda: self.parser.s3g.is_finished()):
                    if self.printing_flag:
                        self.printing_flag = False
                time.sleep(self.IDLE_WAITING_STEP)
            else:
                self.buffer_lock.release()
                self.execute(command)
                self.current_line_number += 1
        self.logger.info("Makerbot sender: sender thread ends.")

    def is_printing(self):
        return self.printing_flag

    def get_percent(self):
        return self.parser.state.percentage

    def get_current_line_number(self):
        return self.current_line_number
