"""camera.py

Interface to a Blackfly GigE machine vision camera using the FLIR Spinnaker PySpin API."""

import os
import sys
import logging
import argparse
import datetime

# OpenCV
import cv2 as cv

# NumPy
import numpy as np

# FLIR Spinnaker camera API
import PySpin

# initialize logging for this module
log = logging.getLogger('camera')

#================================================================
class SpinnakerCamera:
    """Representation of a camera using the FLIR Spinnaker PySpin API."""

    #----------------------------------------------------------------
    def start_continuous(self):
        #  Retrieve enumeration node from nodemap
        # In order to access the node entries, they have to be casted to a pointer type (CEnumerationPtr here)
        node_acquisition_mode = PySpin.CEnumerationPtr(self.nodemap.GetNode('AcquisitionMode'))
        if not PySpin.IsReadable(node_acquisition_mode) or not PySpin.IsWritable(node_acquisition_mode):
            log.error("Unable to set acquisition mode to continuous (enum retrieval).")
            return

        # Retrieve entry node from enumeration node
        node_acquisition_mode_continuous = node_acquisition_mode.GetEntryByName('Continuous')
        if not PySpin.IsReadable(node_acquisition_mode_continuous):
            log.error("Unable to set acquisition mode to continuous (entry retrieval)")
            return

        # Retrieve integer value from entry node
        acquisition_mode_continuous = node_acquisition_mode_continuous.GetValue()

        # Set integer value from entry node as new value of enumeration node
        node_acquisition_mode.SetIntValue(acquisition_mode_continuous)
        self.continuous_mode = True

        log.info("Acquisition mode set to continuous.")
        self.cam.BeginAcquisition()

        # Create ImageProcessor instance for post processing images
        self.processor = PySpin.ImageProcessor()

        # Set default image processor color processing method
        self.processor.SetColorProcessing(PySpin.SPINNAKER_COLOR_PROCESSING_ALGORITHM_HQ_LINEAR)

        return

    #----------------------------------------------------------------
    def end_continuous(self):
        if self.continuous_mode:
            log.info("Ending continuous acquisition mode.")
            self.cam.EndAcquisition()
            self.continuous_mode = False

    #----------------------------------------------------------------
    def acquire_image(self):
        """Fetch the next camera image, returning either a numpy pixel array or None."""
        image_result = self.cam.GetNextImage(1000)

        if image_result.IsIncomplete():
            if self.args.verbose:
                log.debug("Image incomplete with image status %d", image_result.GetImageStatus())
            return None

        else:
            width = image_result.GetWidth()
            height = image_result.GetHeight()

            if self.args.verbose:
                log.debug("Grabbed with width = %d, height = %d", width, height)

            #  Convert image to mono 8
            # The result is an instance of PySpin.ImagePtr
            image_converted = self.processor.Convert(image_result, PySpin.PixelFormat_Mono8)

            # Retrieve image object compatible with OpenCV
            image = image_converted.GetData().reshape(height, width, 1)
            result = image

            #  Release image
            image_result.Release()

            return result

    #================================================================
    def set_capture_property(self, name, value, dtype=int):
        if dtype == float:
            node = PySpin.CFloatPtr(self.nodemap.GetNode(name))

        elif dtype == int:
            node = PySpin.CIntegerPtr(self.nodemap.GetNode(name))

        if PySpin.IsReadable(node):
            log.info("Current value of %s is %s", name, node.GetValue())
            if PySpin.IsWritable(node):
                node.SetValue(value)
                log.info("Set %s to %s", name, node.GetValue())
            else:
                log.error("Unable to write property %s.", name)
        else:
            log.error("Unable to read property %s.", name)

    #================================================================
    def log_device_info(self):
        log.info("*** DEVICE INFORMATION ***")
        node_device_information = PySpin.CCategoryPtr(self.nodemap_tldevice.GetNode('DeviceInformation'))

        if PySpin.IsReadable(node_device_information):
            features = node_device_information.GetFeatures()
            for feature in features:
                node_feature = PySpin.CValuePtr(feature)
                log.info('%s: %s', node_feature.GetName(),
                                  node_feature.ToString() if PySpin.IsReadable(node_feature) else 'Node not readable')
        else:
            log.warning("Device control information not readable.")

    #================================================================
    def log_integer_property(self, name):
        node = PySpin.CIntegerPtr(self.nodemap.GetNode(name))
        if PySpin.IsReadable(node):
            log.info("Current value of %s is %d", name, node.GetValue())
        else:
            log.error("Unable to read property %s.", name)

    def log_float_property(self, name):
        node = PySpin.CFloatPtr(self.nodemap.GetNode(name))
        if PySpin.IsReadable(node):
            log.info("Current value of %s is %f", name, node.GetValue())
        else:
            log.error("Unable to read property %s.", name)

    def log_capture_properties(self):
        log.info("*** Capture Information ***")
        self.log_float_property('AcquisitionFrameRate')
        self.log_integer_property('Width')
        self.log_integer_property('Height')
        self.log_integer_property('OffsetX')
        self.log_integer_property('OffsetY')
        self.log_integer_property('DecimationHorizontal')
        self.log_integer_property('DecimationVertical')

        # properties to see or set
        # Acquisition Frame Rate Enable (shoudl be true)
        # Height can be set to less than the full height (e.g. 540 is half the lines)
        # OffsetY can be set to the height of the top margin (e.g. 270 skips the top quarter)
        # Decimation Horizontal could be 2 to reduce pixel count
        # Decimation Vertical could be 2 to reduce pixel count  (applied before Height and OffsetY)

    #================================================================
    def __init__(self, args):

        self.args = args
        self.continuous_mode = False

        # Retrieve singleton reference to system object
        self.system = PySpin.System.GetInstance()

        # Get current library version
        version = self.system.GetLibraryVersion()
        log.info('PySpin library version: %d.%d.%d.%d', version.major, version.minor, version.type, version.build)

        # Retrieve list of cameras from the system
        self.cam_list = self.system.GetCameras()
        num_cameras = self.cam_list.GetSize()
        log.info("Number of cameras detected: %d", num_cameras)

        if num_cameras == 0:
            print("No cameras found.")
            self.cam = None

        elif num_cameras > 1:
            print("Warning, detected more than one camera, only using first one.")
            self.cam = self.cam_list[0]

        else:
            self.cam = self.cam_list[0]

        if self.cam is not None:
            # Retrieve transport layer device nodemap
            self.nodemap_tldevice = self.cam.GetTLDeviceNodeMap()

            # Optionally log device information
            self.log_device_info()

            # Initialize camera
            self.cam.Init()

            # Retrieve GenICam nodemap
            self.nodemap = self.cam.GetNodeMap()
            self.log_capture_properties()

            # configure the default capture
            self.set_capture_property('AcquisitionFrameRate', 15.0, dtype=float)
            self.set_capture_property('DecimationHorizontal', 2)
            self.set_capture_property('DecimationVertical', 2)
            self.set_capture_property('Height', 270)
            self.set_capture_property('OffsetY', 134)

            # FIXME: this could be computed
            self.capture_width = 720
            self.capture_height = 270

    #----------------------------------------------------------------
    def close(self):
        log.debug("SpinnakerCamera close called.")

        # End acquisition if active
        self.end_continuous()

        # Deinitialize camera
        self.cam.DeInit()

        # Release reference to camera
        del self.cam

        # Clear camera list before releasing system
        self.cam_list.Clear()

        # Release system instance
        self.system.ReleaseInstance()

    def __del__(self):
        log.debug("SpinnakerCamera __del__ called.")
        self.close()

#================================================================
