# 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 socket
import threading
import time
from typing import Any

import telnet_connection
import ffd_sender
import dual_cam


class MaxSocketConnection(telnet_connection.TelnetConnection):

    CONNECTION_TYPE = 'socket'
    BUFFER_SIZE = 1024

    def connect(self):
        try:
            self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.connection.settimeout(self.timeout)
            self.connection.connect((self.ip, self.port))
        except IOError as e:
            self.logger.error("Error connecting to %s:%s - %s" % (self.ip, self.port, e))

    def prepare_data(self, data):
        return data

    def _read_from_connection(self, timeout):
        data = None
        while True:
            t = time.monotonic()
            chunk = self.connection.recv(self.BUFFER_SIZE)
            if chunk:
                if not data:
                    data = chunk
                else:
                    data += chunk
            elif chunk == b'' and time.monotonic() - t < 0.001:
                time.sleep(self.FAIL_WAIT)
            if len(chunk) < self.BUFFER_SIZE:
                break
        return data

    def _write_to_connection(self, data):
        try:
            self.connection.sendall(data)
        except socket.timeout as e:
            raise e
        except OSError as e:
            time.sleep(self.FAIL_WAIT)
            raise e


class FFSocketConnection(MaxSocketConnection):

    DEFAULT_TIMEOUT = 5

    def __init__(self, ip, port, timeout=None, logger=None):
        super().__init__(ip, port, timeout, logger)
        self.endpoint_out = None
        self.endpoint_in = None
        self.file_transfer_endpoint_out = None

    def format_message(self, raw_message):
        return b"~" + raw_message + b"\r\n"

    def send(self, message, endpoint=None, raw=False, timeout=None):
        if not raw:
            message = self.format_message(message)
        return super().send(message, True)

    def recv(self, size=None, endpoint=None):
        return super().recv()


class Camera(dual_cam.Camera):

    def search_cameras(self):
        self.captures = []
        self.init_capture(self.url)

    def set_url(self, ip):
        self.ip = ip
        self.url = 'http://'+ip+':8080/?action=stream'
        self.cloud_camera_number = self.get_number_by_ip()

    def get_number_by_ip(self):
        numbers = [i.zfill(3) for i in self.ip.split(".")]
        try:
            number = int(''.join(numbers))
        except ValueError:
            number = 0
        return number

    def get_camera_number_for_cloud(self):
        return self.cloud_camera_number


class Sender(ffd_sender.Sender):

    RECONNECT_PAUSE = 10

    def __init__(self, parent: Any, usb_info: dict, profile: dict):
        self.ip = usb_info.get("IP", "")
        super().__init__(parent, usb_info, profile)

    def _create_connection(self, usb_info, profile):
        self.connection = FFSocketConnection(self.ip, 8899, None, self.logger)

    def _read_monitoring_data(self):
        result = super()._read_monitoring_data()
        if not result:
            self.operational_flag = False
            self.logger.warning("Error on reading monitoring data. Trying to reconnect to the printer.")
            try:
                self.connection.close()
            except Exception as e:
                self.logger.warning("Error on connection close: %s" % str(e))
            counter = self.RECONNECT_PAUSE
            while counter:
                if self.stop_flag:
                    break
                time.sleep(1)
                counter -= 1
            try:
                self.connection.connect()
                self._handshake()
                self._connect()
                result = super()._read_monitoring_data()
            except Exception as e:
                self.logger.warning("Error on reconnect attempt: %s" % str(e))
        return result

    def _start_camera(self) -> None:
        if not self.camera:
            self.camera = Camera(False)
            self.camera.set_url(self.ip)
            self.camera.search_cameras()
        if self.camera_thread:
            if self.camera_thread.is_alive():
                return
            del self.camera_thread
        self.camera.stop_flag = False
        self.camera_thread = threading.Thread(target=self.camera.main_loop)
        self.camera_thread.start()

    def _stop_camera(self) -> None:
        if self.camera_thread and self.camera_thread.is_alive():
            self.camera.stop_flag = True
