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

import requests
import base_sender


class Sender(base_sender.BaseSender):

    PATH_CONNECT = '/rr_connect'
    PATH_DISCONNECT = '/rr_disconnect'
    PATH_UPLOAD = '/rr_upload'
    PATH_DELETE = '/rr_delete'
    PATH_MODEL = '/rr_model'
    PATH_GCODES = '/rr_gcode'
    PATH_FILEINFO = '/rr_fileinfo'
    #  PATH_REPLY = '/rr_reply'

    PATH_STATUS = 'status'
    PATH_JOB = 'job'
    PATH_FILES = 'files'
    PATH_TRANSFER = 'transfer'

    #  UPLOAD_FILEPATH = "3dprinteros.g"
    UPLOAD_FILEPATH = "/gcodes/3dprinteros.g"
    QUOTED_UPLOAD_FILEPATH = requests.utils.quote(UPLOAD_FILEPATH)

    TIMEOUT = 3
    UPLOAD_TIMEOUT = 240
    JOB_SUCCESS_PERCENT_THR = 95

    IDLE_STATE = "idle"
    PRINTING_STATE = "processing"
    PAUSED_STATE = "paused"

    STATUS_LOOP_SLEEP = 1

    def __init__(self, parent, usb_info, profile):
        super().__init__(parent, usb_info, profile)
        ip = usb_info.get('IP', '')
        if not ip:
            raise RuntimeError('No IP provided for network sender')
        if ip.startswith('http'):
            self.host = ''
        elif usb_info.get('HTTPS'):
            self.host = 'https://'
        else:
            self.host = 'http://'
        self.host += ip
        port = usb_info.get('PORT')
        if port:
            self.host += ":" + str(port)
        self.printer_job_filename = None
        self.upload_url = self.host + self.PATH_UPLOAD 
        self.delete_url = self.host + self.PATH_DELETE 
        self.gcodes_url = self.host + self.PATH_GCODES + "?gcode="
        self.status_url = self.host + self.PATH_MODEL + "?flags=d99fnv"
        self.fileinfo_url = self.host + self.PATH_FILEINFO
        self.session = None
        self.file_size_bytes = 0
        auth_error = self.authenticate()
        if auth_error:
            self.logger.warning(auth_error)
            raise RuntimeError(auth_error)
        self.previous_status = None
        self.status_thread = threading.Thread(target=self.status_loop)
        self.status_thread.start()

    def authenticate(self):
        self.session = requests.Session()
        password = self.settings.get('password', self.usb_info.get('PASS', 'reprap'))
        try:
            #password could be dummy, but no password argument would lead to error
            resp = self.session.get(self.host + self.PATH_CONNECT + '?password=' + password + '&time=' + str(int(time.time())))
        except (requests.exceptions.ConnectTimeout, requests.exceptions.ConnectionError):
            self.session.close()
            return 'Connection timeout'
        except Exception as e:
            self.session.close()
            self.logger.exception('Unexpected connection error:' + str(e))
            return 'Unexpected connection error'
        if not resp.ok:
            self.session.close()
            if resp.status_code == 401:
                message = 'Auth error. Check credentials.'
                self.register_error(255, message, disconnect=True)
                return message
            return 'Connection error: %s %s' % (resp.status_code, resp.text)

    def status_loop(self):
        self.previous_status = None
        while not self.stop_flag:
            try:
                resp = self.session.get(self.status_url, timeout=self.TIMEOUT)
                if resp.ok:
                    status_dict = resp.json()
                    #  self.logger.info('Status: %s', pprint.pformat(status_dict))
                    self.parse_status(status_dict)
                else:
                    self.logger.warning('Status request error: %s', resp)
                    self.operational_flag = False
            except (requests.exceptions.ConnectTimeout, requests.exceptions.HTTPError, requests.exceptions.RequestException, requests.exceptions.ConnectionError):
                self.logger.warning('Connection lost')
                self.operational_flag = False
                self.session.close()
                self.authenticate()
            except Exception as e:
                self.logger.error('Status parsing error: %s', status_dict)
                self.logger.exception('Exception while parsing status response: ')
                self.operational_flag = False
                self.session.close()
                self.authenticate()
            time.sleep(self.STATUS_LOOP_SLEEP)
            
    def parse_status(self, model_dict):
        try:
            model_dict = model_dict.get('result')
            if not isinstance(model_dict, dict):
                raise ValueError('Invalid printer status response')
        except (ValueError, AttributeError):
            self.logger.warning('Invalid status response format')
        else:
            job_info = model_dict.get('job')
            if isinstance(job_info, dict):
                duration = job_info.get('duration')
                #  warmup_duration = job_info.get('warmUpDuration', 0.0)
                #  pause_duration = job_info.get('pauseDuration', 0)
                if self.allow_estm_by_printer:
                    print_time_left = job_info.get('timesLeft', {}).get('slicer')
                    if print_time_left is None:
                        print_time_left = job_info.get('timesLeft', {}).get('file')
                    if print_time_left:
                        self.print_time_left = print_time_left
                    #  self.percent = round(100*min((duration) / (print_time_left + duration), 1), 1)
                #  else:
                #      self.percent = 0.0
                try:
                    self.file_position_bytes = int(job_info.get('filePosition')) 
                except (TypeError,ValueError):
                    self.file_position_bytes = 0
                if self.file_size_bytes:
                    self.percent = round(100*min(float(self.file_position_bytes) / float(self.file_size_bytes), 1), 1)
                else:
                    self.percent = 0.0
            else:
                self.percent = 0.0
            #  if self.preconnect_printer_job_id:
            #      if self.printers_job_id != self.preconnect_printer_job_id:
            #          self.save_current_printer_job_id(None)
            #          self.register_error(999, 'After reconnection to the printer, it is not running cloud print job. Assuming that job had failed, when print was offline.', job_fail=True)
            #      self.preconnect_printer_job_id = None
            status = model_dict.get('state', {}).get('status')
            if status == self.IDLE_STATE:
                if self.previous_status == self.PRINTING_STATE:
                    if self.percent > self.JOB_SUCCESS_PERCENT_THR:
                        self.register_print_finished_event()
                self.file_size_bytes = 0
                self.operational_flag = True
                self.pause_flag = False
                self.printing_flag = False
            elif status == self.PRINTING_STATE:
                if not self.file_size_bytes:
                    self.file_size_bytes = self.get_file_size()
                self.printing_flag = True
                self.pause_flag = False
                self.operational_flag = True
            elif status == self.PAUSED_STATE:
                self.pause_flag = True
                self.operational_flag = True
            #TODO add errors parsing 
            if status:
                self.previous_status = status
            heaters = model_dict.get('heat', {}).get('heaters', [])
            for index, heater in enumerate(heaters):
                try:
                    temp = float(heater.get('current'))
                    ttemp = float(heater.get('active'))
                except:
                    pass
                else:
                    self.temps[index] = round(temp, self.TEMPERATURE_SIGNS_AFTER_DOT)
                    self.target_temps[index] = round(ttemp, self.TEMPERATURE_SIGNS_AFTER_DOT)
            for index, axis_dict in enumerate(model_dict.get('move', {}).get('axes', [])):
                if index < 4 and index > -1:
                    try:
                        self.position[index] = float(axis_dict.get('userPosition'))
                    except (TypeError, ValueError):
                        pass
            epos_list = model_dict.get('move', {}).get('extruders', [])
            if epos_list and type(epos_list) == list: 
                epos = epos_list[0]
                if type(epos) == dict:
                    e = epos.get('position')
                    if type(e) == float:
                        self.position[3] = e

    def get_file_size(self, filename=UPLOAD_FILEPATH):
        file_size_bytes = 0
        args = '?name=' + self.QUOTED_UPLOAD_FILEPATH
        try:
            resp = self.session.get(self.fileinfo_url + args, timeout=self.TIMEOUT)
            if resp.ok:
                resp_dict = resp.json()
                file_size_bytes = int(resp_dict.get('size', 0))
        except:
            self.logger.error('Unable to parse file size')
        return file_size_bytes

    def gcodes(self, filepath, keep_file=False):
        if self.is_printing():
            self.parent.register_error(254, "Already printing", job_fail=False)
            return False
        # if self.printers_job_id:
        #     self.cancel(raise_error=False)
        success = False
        self.printers_job_id = None
        resp = None
        try:
            with open(filepath, "rb") as f:
                args = '?name=' + self.QUOTED_UPLOAD_FILEPATH
                self.session.get(self.delete_url + args, timeout=self.TIMEOUT)
                crc = self.calculate_file_crc(filepath, hexify=False)
                if crc:
                    args += '&crc=' + crc
                #  args += '&time=' + str(int(time.time()))
                resp = self.session.post(self.upload_url + args, data=f, timeout=self.UPLOAD_TIMEOUT*4)
                if resp.ok:
                    self.logger.info('Upload success. Sending print start gcode...')
                    resp = self.session.get(f'{self.gcodes_url}M32' + requests.utils.quote(f' "0:{self.UPLOAD_FILEPATH}"'), timeout=self.TIMEOUT)
                    if resp.ok:
                        success = True
                        self.logger.info('Stated print of file with size: {self.file_size_bytes}B')
                    # FIXME
                    #  time_left = 6
                    #  while not self.stop_flag and time_left:
                    #      if self.printing_flag:
                    #          success = True
                    #          self.save_current_printer_job_id(self.printers_job_id)
                    #          self.logger.info('Printer started printing')
                    #          break
                    #      time.sleep(0.01)
                    #      time_left -= 0.01
                    else:
                        self.logger.warning(f'Job start error: {resp.status_code} {resp.text}')
                else:
                    self.logger.warning(f'Upload error: {resp.status_code} {resp.text}')
        except Exception as e:
            if resp:
                status = resp.status_code
                text = resp.text
            else:
                status, text = '', ''
            self.logger.warning(f'Print start error: {status} {text}' + str(e))
        if not keep_file:
            try:
                os.remove(filepath)
            except:
                pass
        return success

    def unbuffered_gcodes(self, gcodes):
        try:
            r = self.session.get(self.gcodes_url + requests.utils.quote(gcodes))
        except:
            pass
        else:
            return r.ok

    def pause(self):
        if not self.is_printing():
            self.parent.register_error(254, "No print to pause")
        elif self.is_paused():
            self.parent.register_error(254, "Already in pause")
        else:
            try:
                resp = self.session.get(self.gcodes_url + "M25" , timeout=self.TIMEOUT)
                if resp.ok:
                    return True
                else:
                    self.parent.register_error(254, "Pause failed")
            except Exception as e:
                self.logger.warning('Error sending a pause request')
        return False

    def resume(self):
        if not self.is_paused():
            self.parent.register_error(255, "Can't resume - not paused", job_fail=False)
        else:
            try:
                resp = self.session.get(self.gcodes_url + "M24" , timeout=self.TIMEOUT)
                if resp.ok:
                    return True
                    self.parent.register_error(255, "Resume failed")
            except Exception as e:
                self.logger.warning('Error sending a resume request')
        return False

    def unpause(self):
        return self.resume()

    def cancel(self, raise_error=True):
        try:
            if not self.is_paused():
                self.session.get(self.gcodes_url + "M25" , timeout=self.TIMEOUT)
            resp = self.session.get(self.gcodes_url + "M0" , timeout=self.TIMEOUT)
            if resp.ok:
                self.save_current_printer_job_id(None)
                return True
        except:
            self.logger.warning('Error sending a cancel request')
        if raise_error:
            self.parent.register_error(257, 'Cancel failed')
        return False

    def close(self):
        self.stop_flag = True
        self.status_thread.join(1.1*self.TIMEOUT)
        if self.session:
            try:
                self.session.get(self.host + "/rr_disconnect")
            except:
                pass
            self.session.close()
