# 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 time
import serial
import logging
import platform
import threading
import subprocess

import config

class SerialConnection:

    DEFAULT_TIMEOUT = 1
    WRITE_FAIL_WAIT = 0.5
    MAX_LINE_SIZE = 512 #protection against endless line when baudrate mismatch
    WRITE_RETRIES = 4

    def __init__(self, port_name, baudrate, timeout=DEFAULT_TIMEOUT, start_dtr=None):
        self.logger = logging.getLogger(__name__)
        self.port_name = port_name
        self.baudrate = baudrate
        self.timeout = timeout
        self.start_dtr = start_dtr
        self.port_recv_lock = threading.Lock()
        self.port_send_lock = threading.Lock()
        self.verbose = config.get_settings()['verbose']
        self.port = None
        self.connect()

    def connect(self):
        self.control_ttyhup(self.port_name, False)
        try:
            port = serial.Serial(port=self.port_name,
                                 baudrate=self.baudrate,
                                 timeout=self.timeout,
                                 parity=serial.PARITY_ODD)
            port.close()
            port.parity = serial.PARITY_NONE
            if self.start_dtr is not None:
                try:
                    port.setDTR(self.start_dtr)
                except:
                    pass
            port.open()
            self.port = port
        except serial.SerialException as e:
            self.logger.warning("Can't open serial port %s. Error:%s" % (self.port_name, str(e)))
        except Exception as e:
            self.logger.warning("Unexpected error while open serial port %s:%s" % (self.port_name, str(e)))
        else:
            self.logger.info("Opened serial port %s at baudrate %d" % (self.port_name, self.baudrate))

    def control_ttyhup(self, port_name, hup_flag):
        if hup_flag:
            hup_command = "hup"
        else:
            hup_command = "-hup"
        if platform.system() == "Linux":
            subprocess.call("stty -F %s %s" % (port_name, hup_command), shell=True)

    def recv(self, size=None):
        with self.port_recv_lock:
            if not self.port:
                self.logger.warning("Can't perform the read - no connection to serial port")
            else:
                try:
                    if size:
                        data = self.port.read(size)
                    else:
                        data = self.port.readline(self.MAX_LINE_SIZE)
                    if self.verbose:
                        self.logger.info("RECV: " + str(data))
                except serial.SerialException as e:
                    self.logger.warning("Can't read serial port %s. Error:%s" % (self.port_name, str(e)))
                except Exception as e:
                    self.logger.warning("Unexpected error while reading serial port %s:%s" % (self.port_name, str(e)))
                else:
                    try:
                        return data.decode("utf-8")
                    except UnicodeDecodeError:
                        self.logger.debug(b"Cant decode data received from printer: " + data)

    def prepare_data(self, data):
        return str(data).strip() + "\n"

    def send(self, data, raw=False):
        with self.port_send_lock:
            if not self.port:
                self.logger.warning("Can't perform the write - no connection to serial port")
            else:
                fails = 0
                if not raw:
                    data = self.prepare_data(data)
                bytes_send = 0
                if type(data) == str:
                    data = data.encode("utf-8")
                while fails <= self.WRITE_RETRIES:
                    try:
                        bytes_send += self.port.write(data)
                        if self.verbose:
                            self.logger.info("SEND: " + str(data.strip()))
                    except serial.SerialException as e:
                        self.logger.warning("Can't write to serial port %s. Error:%s" % (self.port_name, str(e)))
                        fails += 1
                        time.sleep(self.WRITE_FAIL_WAIT)
                    else:
                        if bytes_send == len(data):
                            return True
                        else:
                            self.logger.error("Critical error. The data sent to serial port was cut.")
                            break
                self.logger.error("Can't send data to serial port")

    def reset(self):
        with self.port_send_lock:
            with self.port_recv_lock:
                if self.port:
                    self.port.setDTR(1)
                    time.sleep(0.2)
                    self.port.setDTR(0)

    def close(self):
        with self.port_send_lock:
            with self.port_recv_lock:
                if self.port:
                    self.port.close()
                    self.port = None
