# 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 json
import socket
import logging
import threading
import os.path

from usb_detect import BaseDetector


class BroadcastListener(threading.Thread):

    LISTEN_TIMEOUT = 3
    READ_RETRIES = 3
    READ_LENGTH = 1024

    def __init__(self, port):
        self.logger = logging.getLogger(self.__class__.__name__)
        self.socket = self.create_socket(port)
        self.received = {}
        threading.Thread.__init__(self, daemon=True)

    def create_socket(self, port):
        self.logger.debug("Creating listen socket for port %d" % port)
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            sock.bind(('', port))
            sock.settimeout(self.LISTEN_TIMEOUT)
        except socket.error:
            self.logger.debug("Socket init error. Port:" + str(port), exc_info=True)
        except ValueError:
            self.logger.debug("Not valid port number:" + str(port), exc_info=True)
        else:
            self.logger.debug("...done")
            return sock

    def run(self):
        if self.socket:
            read_retries_left = self.READ_RETRIES
            while read_retries_left:
                read_retries_left -= 1
                try:
                    data, addr = self.socket.recvfrom(self.READ_LENGTH)
                    if addr:
                        if not addr in self.received:
                            self.received[addr] = data
                        # else:
                        #     if self.received[addr] != data: #protection against printer's hiccups
                        #         self.received[addr] += data
                        #         self.logger.debug(f'Concatenating a broadcast resp from {addr}')
                        #     else:
                        #         self.logger.warning(f'Received what seems to be a duplicate broadcast resp from {addr}')
                except socket.error:
                    break


class BroadcastPrinterDetector(BaseDetector):

    RETRIES = 1
    RETRY_TIMEOUT = 0.1

    def __init__(self, _, profile):
        self.logger = logging.getLogger(__name__)
        self.profile = profile
        network_profile = profile['network_detect']
        self.listen_port = network_profile.get('listen_port', None)
        self.broadcast_port = network_profile.get('broadcast_port', None)
        self.target_port = network_profile.get('target_port', None)
        message = network_profile.get('message', '')
        if network_profile.get('json_message'):
            message = json.dumps(message)
        self.message = message
        self.already_detected = []
        self.discovered_printers = []

    def detect(self, already_know_ip=None, non_default_port=None):
        #TODO implement already_know_ip and non_default_port
        if self.broadcast_port and self.listen_port and self.target_port:
            self.listener = BroadcastListener(self.listen_port)
            self.listener.start()
            self.broadcast()
            self.listener.join()
        else:        
            error = "Error in config file. Section network_detect of profile: " + str(self.profile)
            self.logger.error(error)

    def create_broadcasting_socket(self):
        self.logger.debug("Creating broadcast socket on port %d" % self.broadcast_port)
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            sock.settimeout(self.listener.LISTEN_TIMEOUT-1)
            sock.bind(('', self.broadcast_port))
        except socket.error:
            self.logger.debug("Socket init error. Port: %d" % self.broadcast_port, exc_info=True)
        except ValueError:
            self.logger.debug("Not valid port number: %d" % self.broadcast_port, exc_info=True)
        else:
            return sock

    def broadcast(self):
        self.logger.debug("Sending broadcast to port %d" % self.target_port)
        bc_socket = self.create_broadcasting_socket()
        if bc_socket:
            attempt = 1
            while attempt < self.RETRIES + 1:
                try:
                    bc_socket.sendto(self.message.encode('utf-8'), ('255.255.255.255', self.target_port))
                except socket.error:
                    self.logger.debug("Error sending broadcast", exc_info=True)
                    #self.logger.debug("Timeout on port:" + str(self.listen_port))
                attempt += 1
                time.sleep(self.RETRY_TIMEOUT)
                for printer_addr, recv_data in self.listener.received.items():
                    self.process_response(recv_data, printer_addr)
            self.logger.debug("Done listening to port " + str(self.listen_port))
            bc_socket.close()

    def process_response(self, response, addr):
        if addr not in self.already_detected:
            self.already_detected.append(addr)
            printer = {'IP': addr[0], 'port': addr[1]}
            if self.profile['network_detect']['json_response']:
                try:
                    response = json.loads(response)
                except (TypeError, ValueError):
                    self.logger.debug('Response from printer should be valid json. Its malformed or not json')
                else:
                    ip_field = self.profile['network_detect']['IP_field']
                    iserial_field = self.profile['network_detect']['SNR_field']
                    vid_field = self.profile['network_detect']['VID_field']
                    pid_field = self.profile['network_detect']['PID_field']
                    printer['IP'] = response[ip_field]
                    if addr[0] != printer['IP']:
                        self.logger.warning("Detected printer IP didn't match with IP field in response")
                    printer['SNR'] = response[iserial_field]
                    printer['VID'] = self.format_vid_or_pid(response[vid_field])
                    printer['PID'] = self.format_vid_or_pid(response[pid_field])
            self.discovered_printers.append(printer)

    def close(self):
        pass # no need to close anything here, but method is still should exist until Awaitable implementation
