#!/usr/bin/python3
# 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 sys
import os

# fix of broken paths for windows
if not sys.path:
    sys.path = []
path = os.getcwd()
if path not in sys.path:
    sys.path.insert(0, path)

import socket
import ssl
import time
import threading
import hashlib

import config
import dual_cam

## For multiprocess
# def start_camera_task(user_token, mac, host, access_code, active_camera_number):
#     cam = Camera(False)
#     cam.token = user_token
#     cam.http_client.host_id = mac
#     cam.host = host
#     cam.access_code = access_code
#     cam.active_camera_number = active_camera_number
#     cam.start()

class FakeList(list):
    def __getitem__(self, _):
        return 0

class Camera(dual_cam.Camera):
    # TODO: fix logging
    USERNAME = b'bblp'
    PORT = 6000
    TIMEOUT = 10
    ON_ERROR_SLEEP = 1
    ANTI_FLOOD_SLEEP = 1/60
    READ_CHUNK = 1024
    INACTIVE_LOOP_TIME = 3

    JPEG_START_MAGIC = bytes.fromhex("ff d8 ff e0")
    JPEG_END_MAGIC = bytes.fromhex("ff d9")
    LEN_JPEG_END_MAGIC = len(JPEG_END_MAGIC)

    #DEBUG_OUTPUT_FILE = "/tmp/bamb_cam_frame.jpeg"
    DEBUG_OUTPUT_FILE = None

    # mandatory command line arguments: user_token, mac, host, access_code, cloud_camera_number(from 1000, 1001, and so on)
    def __init__(self, autostart=True):
        self.stop_flag = False
        self.pre_init(autostart)
        self.init_settings()
        self.init_user_token()
        self.init_parameters()
        self.init()
        self.frame_lock = threading.Lock()
        self.frame = b""
        self.socket_thread = threading.Thread(target=self.socket_loop)
        self.resized = FakeList()
        self.fails = FakeList()
        self.host = ""
        self.access_code = ""
        self.active_camera_number = -1

        if autostart:
            if len(sys.argv) < 6:
                self.logger.error('Not enough command line arguments to start')
            self.host = sys.argv[3]
            self.access_code = sys.argv[4]
            old_camera_number = int(sys.argv[5])
            settings = config.get_settings()
            self.active_camera_number = old_camera_number # in case of old numeration of migration fail
            new_camera_number = int(hashlib.shake_128(self.host.encode(errors='ignore')).hexdigest(4), 16)
            if settings['camera'].get('old_ip_numeration'):
                self.cloud_camera_number = old_camera_number + 1
                self.camera_name = str(self.cloud_camera_number)
                if settings['camera'].get('migrate_numeration'):
                    number_migr_dict = {old_camera_number + 1: new_camera_number}
                    resp_json = self.http_client.change_camera_number(self.token, number_migr_dict)
                    if resp_json and isinstance(resp_json, dict) and resp_json.get('success'):
                        settings = config.get_settings()
                        settings['cam_number_migration_success'] = True
                        config.Config.instance().save_settings(settings)
                        self.logger.info('Camera number migration success: ' + str(number_migr_dict))
                        self.cloud_camera_number = new_camera_number
                        self.camera_name = str(self.host)
                    else:
                        self.logger.info('Camera number migration fail: ' + str(resp_json))
            else:
                self.cloud_camera_number = new_camera_number
                self.camera_name = str(self.host)
            self.cloud_camera_state[self.active_camera_number] = True
            self.logger.info('Internal number: ' + str(self.active_camera_number))
            self.logger.info('Cloud number: ' + str(self.cloud_camera_number))
            self.logger.info('Cloud name: ' + self.camera_name)
            self.start()

    def form_auth_msg(self, acess_code):
        bacess_code = str.encode(acess_code)
        return b'@\x00\x00\x00\x000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + \
               self.USERNAME + b'\x00' * (32 - len(self.USERNAME)) + \
               bacess_code + b'\x00' * (32 - len(bacess_code))

    def get_camera_number_for_cloud(self):
        return self.cloud_camera_number

    def get_camera_name(self, _):
        return self.camera_name

    def start(self):
        self.socket_thread.start()
        while not self.frame:
            time.sleep(self.ANTI_FLOOD_SLEEP)
            if self.stop_flag:
                self.socket_thread.join(self.TIMEOUT*2)
                return
        self.main_loop()

    def get_frame(self):
        with self.frame_lock:
            if self.frame:
                np_arr = self.np.frombuffer(self.frame, self.np.uint8)
                image = self.cv2.imdecode(np_arr, self.cv2.IMREAD_COLOR)
                image = self.resize_cv2_frame(image)
                data = self.np.array(image)
                return self.get_image_from_cv2_frame(data)

    def socket_loop(self):
        ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE
        auth_msg = self.form_auth_msg(self.access_code)
        while not self.stop_flag: # for reconnection
            try:
                self.logger.info('Connecting to camera socket...')
                with socket.create_connection((self.host, self.PORT), self.TIMEOUT) as sock:
                    with ctx.wrap_socket(sock, server_hostname=self.host) as ssock:
                        ssock.sendall(auth_msg)
                        buf = bytearray()
                        reading_jpeg = False
                        while not self.stop_flag:
                            try:
                                received = ssock.recv(self.READ_CHUNK)
                            except socket.error:
                                self.logger.error('Camera socket read error')
                                received = None
                            if not received:
                                self.logger.error('Connection lost')
                                time.sleep(self.ON_ERROR_SLEEP)
                                break
                            buf += received
                            if not reading_jpeg:
                                index = buf.find(self.JPEG_START_MAGIC)
                                if index >= 0:
                                    reading_jpeg = True
                                    buf = buf[index:]
                                time.sleep(self.ANTI_FLOOD_SLEEP)
                            index = buf.find(self.JPEG_END_MAGIC)
                            if index >= 0:
                                reading_jpeg = False
                                with self.frame_lock:
                                    self.frame = bytes(buf[:index + self.LEN_JPEG_END_MAGIC])
                                buf = buf[index + self.LEN_JPEG_END_MAGIC:]
                                if self.DEBUG_OUTPUT_FILE:
                                    print(".", file=sys.stderr, end="", flush=True)
                                    with open(self.DEBUG_OUTPUT_FILE, 'wb') as f:
                                        f.write(self.frame)
            except socket.error:
                self.logger.error('No connection to camera socket')
                time.sleep(self.ON_ERROR_SLEEP)
                break

    def main_loop(self):
        time.sleep(self.MIN_LOOP_TIME)
        try:
            while not self.stop_flag:
                frame_start_time = time.monotonic()
                if self.cloud_camera_state[self.active_camera_number]:
                    frame = self.get_frame()
                    if frame:
                        self.send_frame(frame)
                    min_loop_time = self.MIN_LOOP_TIME
                else:
                    self.send_frame(self.SAME_IMAGE)
                    min_loop_time = self.INACTIVE_LOOP_TIME
                while time.monotonic() < frame_start_time + 0.001 + min_loop_time:
                    time.sleep(0.01)
        except KeyboardInterrupt:
            self.logger.info("KeyboardInterrupt catched. Terminating")
            self.stop_flag = True
        self.socket_thread.join(self.TIMEOUT*2)
        sys.exit(0)


if __name__ == '__main__':
    Camera()
