#!/usr/bin/env python3
# ----------------------------------------------------------------------------
#
# Copyright 2018 EMVA
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# ----------------------------------------------------------------------------


# Standard library imports
from datetime import datetime
import io
import os
import pathlib
from queue import Queue
from queue import Full, Empty
import signal
import sys
from threading import Lock, Thread, Event
from threading import current_thread, main_thread
import time
from urllib.parse import unquote
import weakref
import zipfile
import tempfile
from zipfile import BadZipFile

# Related third party imports
import numpy as np

from genicam.genapi import NodeMap
from genicam.genapi import LogicalErrorException, RuntimeException
from genicam.genapi import ChunkAdapterGeneric, ChunkAdapterU3V, \
    ChunkAdapterGEV

from genicam.gentl import TimeoutException, \
    NotImplementedException, ParsingChunkDataException, NoDataException, \
    ErrorException, InvalidBufferException, InvalidParameterException
from genicam.gentl import GenericException
from genicam.gentl import GenTLProducer, BufferToken, EventManagerNewBuffer
from genicam.gentl import DEVICE_ACCESS_FLAGS_LIST, EVENT_TYPE_LIST, \
    ACQ_START_FLAGS_LIST, ACQ_STOP_FLAGS_LIST, ACQ_QUEUE_TYPE_LIST, \
    PAYLOADTYPE_INFO_IDS

# Local application/library specific imports
from harvesters._private.core.port import ConcretePort
from harvesters._private.core.statistics import Statistics
from harvesters.util.logging import get_logger
from harvesters.util.pfnc import symbolics
from harvesters.util.pfnc import uint16_formats, uint32_formats, \
    float32_formats, uint8_formats
from harvesters.util.pfnc import component_2d_formats
from harvesters.util.pfnc import lmn_444_location_formats, \
    lmno_4444_location_formats, lmn_422_location_formats, \
    lmn_411_location_formats, mono_location_formats, bayer_location_formats


_is_logging_buffer_manipulation = True if 'HARVESTERS_LOG_BUFFER_MANIPULATION' in os.environ else False
_sleep_duration_default = 0.000001  # s


class Module:
    def __init__(self, module=None, node_map=None, parent=None):
        self._module = module
        self._node_map = node_map
        self._parent = parent

    @property
    def node_map(self):
        return self._node_map

    @property
    def parent(self):
        return self._parent

    @property
    def port(self):
        return self._module.port

    def register_event(self, event_type=None):
        return self._module.register_event(event_type)

    def announce_buffer(self, buffer_token=None):
        return self._module.announce_buffer(buffer_token)


class DataStream(Module):
    def __init__(self, module=None, node_map=None, parent=None):
        super().__init__(module=module, node_map=node_map, parent=parent)

    def open(self, data_stream_id=None):
        self._module.open(data_stream_id)

    @property
    def id_(self):
        return self._module.id_

    @property
    def buffer_announce_min(self):
        return self._module.buffer_announce_min

    def defines_payload_size(self):
        return self._module.defines_payload_size()

    @property
    def payload_size(self):
        return self._module.payload_size

    def queue_buffer(self, announced_buffer=None):
        self._module.queue_buffer(announced_buffer)

    def start_acquisition(self, flags=None, num_images=None):
        self._module.start_acquisition(flags, num_images)

    def is_open(self):
        return self._module.is_open()

    def stop_acquisition(self, flags=None):
        self._module.stop_acquisition(flags)

    def revoke_buffer(self, buffer=None):
        return self._module.revoke_buffer(buffer)

    def flush_buffer_queue(self, operation=None):
        self._module.flush_buffer_queue(operation)

    def close(self):
        self._module.close()


class RemoteDevice(Module):
    def __init__(self, module=None, node_map=None, parent=None):
        super().__init__(module=module, node_map=node_map, parent=parent)

    @property
    def port(self):
        return self._parent.remote_port


class Device(Module):
    def __init__(self, module=None, node_map=None, parent=None):
        super().__init__(module=module, node_map=node_map, parent=parent)

    @property
    def data_stream_ids(self):
        return self._module.data_stream_ids

    def create_data_stream(self):
        return self._module.create_data_stream()

    @property
    def id_(self):
        return self._module.id_

    @property
    def tl_type(self):
        return self._module.tl_type

    def is_open(self):
        return self._module.is_open()

    def close(self):
        self._module.close()

    @property
    def port(self):
        return self._module.local_port


class Interface(Module):
    def __init__(self, module=None, node_map=None, parent=None):
        super().__init__(module=module, node_map=node_map, parent=parent)


class System(Module):
    def __init__(self, module=None, node_map=None, parent=None):
        super().__init__(module=module, node_map=node_map, parent=parent)


class DeviceInfo:
    def __init__(self, device_info=None):
        self._device_info = device_info

    def create_device(self):
        return self._device_info.create_device()

    def __repr__(self):
        properties = [
            'id_',
            'vendor',
            'model',
            'tl_type',
            'user_defined_name',
            'serial_number',
            'version',
        ]
        results = []
        for property in properties:
            if property is '':
                result = None
            else:
                try:
                    result = eval('self._device_info.' + property)
                except:
                    result = None
            results.append(result)

        info = '('
        delimiter = ', '
        for i, r in enumerate(results):
            if r:
                r = '\'{0}\''.format(r)
            else:
                r = 'None'
            info += '{0}={1}'.format(properties[i], r)
            info += delimiter
        info = info[:-len(delimiter)]
        info += ')'
        return info

    @property
    def id_(self):
        return self._device_info.id_

    @property
    def vendor(self):
        return self._device_info.vendor

    @property
    def model(self):
        return self._device_info.model

    @property
    def tl_type(self):
        return self._device_info.tl_type

    @property
    def user_defined_name(self):
        return self._device_info.user_defined_name

    @property
    def serial_number(self):
        return self._device_info.serial_number

    @property
    def version(self):
        return self._device_info.version


class _SignalHandler:
    _event = None
    _threads = None

    def __init__(self, *, event=None, threads=None, logger=None):
        #
        self._logger = logger or get_logger(name=__name__)

        #
        super().__init__()

        #
        assert event
        assert threads

        #
        self._event = event
        self._threads = threads

    def __call__(self, signum, frame):
        """
        A registered Python signal modules will call this method.
        """

        self._logger.debug(
            'Going to terminate threads having triggered '
            'by the event {0}.'.format(
                self._event
            )
        )

        # Set the Event:
        self._event.set()

        # Terminate the threads:
        for thread in self._threads:
            thread.stop()

        self._logger.debug(
            'Has terminated threads having triggered by '
            'the event {0}.'.format(
                self._event
            )
        )


class ThreadBase:
    def __init__(self, *, logger=None, mutex=None):
        #
        super().__init__()
        #
        self._logger = logger or get_logger(name=__name__)
        #
        self._mutex = mutex
        self._is_running = False

    def start(self):
        self._internal_start()
        self._logger.debug(
            'Started thread {:0X}.'.format(self.id_)
        )

    def _internal_start(self):
        """
        This method is abstract and should be reimplemented in any sub-class.

        Releases the acquired mutex.

        :return: None.
        """
        raise NotImplementedError

    def stop(self):
        self._internal_stop()
        self._logger.debug(
            'Stopped thread {:0X}.'.format(self.id_)
        )

    def _internal_stop(self):
        """
        This method is abstract and should be reimplemented in any sub-class.

        Releases the acquired mutex.

        :return: None.
        """
        raise NotImplementedError

    def acquire(self):
        """
        This method is abstract and should be reimplemented in any sub-class.

        Acquires a mutex.

        :return: None.
        """
        raise NotImplementedError

    def release(self):
        """
        This method is abstract and should be reimplemented in any sub-class.

        Releases the acquired mutex.

        :return: None.
        """
        raise NotImplementedError

    def is_running(self):
        """
        This method is abstract and should be reimplemented in any sub-class.

        Releases the acquired mutex.

        :return: None.
        """
        raise NotImplementedError

    @property
    def id_(self):
        """
        This method is abstract and should be reimplemented in any sub-class.

        :return: None.
        """
        raise NotImplementedError


class MutexLocker:
    def __init__(self, thread: ThreadBase=None):
        """
        :param thread:
        """
        #
        assert thread
        #
        super().__init__()
        #
        self._thread = thread
        self._locked_mutex = None

    def __enter__(self):
        #
        if self._thread is None:
            return None

        #
        self._locked_mutex = self._thread.acquire()
        return self._locked_mutex

    def __exit__(self, exc_type, exc_val, exc_tb):
        #
        if self._thread is None:
            return

        #
        self._thread.release()


class ImageAcquisitionThreadBase(ThreadBase):
    def __init__(self, *, mutex=None, logger=None, image_acquire=None):
        #
        assert image_acquire
        #
        super().__init__(mutex=mutex, logger=logger)
        #
        self._ia = image_acquire
        self._worker = self._ia.worker_image_acquisition
        self._sleep_duration = self._ia.sleep_duration
        self._context = Queue(maxsize=self._ia.num_filled_buffers_to_hold)

    @property
    def queue(self):
        """
        This method is abstract and should be reimplemented in any sub-class.

        :return: None.
        """
        raise NotImplementedError


class _ImageAcquisitionThread(ImageAcquisitionThreadBase):
    def __init__(self, *, image_acquire=None, logger=None):
        """
        :param mutex:
        :param worker:
        :param logger:
        """
        #
        assert image_acquire
        #
        super().__init__(
            mutex=Lock(), logger=logger, image_acquire=image_acquire
        )

        #
        self._thread = None

    def _internal_start(self):
        """

        :return: None.
        """
        self._thread = _NativeThread(
            thread_owner=self, worker=self._worker,
            sleep_duration=self._sleep_duration,
            context=self._context
        )

        # Start running its worker method.
        self._is_running = True
        self._thread.start()

    def _internal_stop(self):
        #
        if self._thread is None:
            return

        # Prepare to terminate the worker method.
        self._thread.stop()
        self._is_running = False

        # Wait until the run methods is terminated.
        self._thread.join()

    def acquire(self):
        #
        if self._thread:
            return self._thread.acquire()
        else:
            return None

    def release(self):
        #
        if self._thread:
            self._thread.release()
        else:
            return

    @property
    def worker(self):
        #
        if self._thread:
            return self._thread.worker
        else:
            return None

    @worker.setter
    def worker(self, obj):
        #
        if self._thread:
            self._thread.worker = obj
        else:
            return

    @property
    def mutex(self):
        return self._mutex

    @property
    def id_(self):
        return self._thread.id_

    def is_running(self):
        return self._is_running

    @property
    def queue(self):
        return self._context


class _NativeThread(Thread):
    def __init__(self, thread_owner=None, worker=None,
                 sleep_duration=_sleep_duration_default,
                 context=None):
        """

        :param thread_owner:
        :param worker:
        :param sleep_duration:
        """

        #
        assert thread_owner

        #
        super().__init__(daemon=self._is_interactive())

        #
        self._worker = worker
        self._thread_owner = thread_owner
        self._sleep_duration = sleep_duration
        self._context = context

    @staticmethod
    def _is_interactive():
        #
        if bool(getattr(sys, 'ps1', sys.flags.interactive)):
            return True

        #
        try:
            from traitlets.config.application import Application as App
            return App.initialized() and App.instance().interact
        except (ImportError, AttributeError):
            return False

    def stop(self):
        with self._thread_owner.mutex:
            self._thread_owner._is_running = False

    def run(self):
        """
        Runs its worker method.

        This method will be terminated once its parent's is_running
        property turns False.
        """
        while self._thread_owner.is_running():
            if self._worker:
                self._worker(self._context)
                time.sleep(self._sleep_duration)

    def acquire(self):
        return self._thread_owner.mutex.acquire()

    def release(self):
        self._thread_owner.mutex.release()

    @property
    def id_(self):
        return self.ident

    @property
    def worker(self):
        return self._worker

    @worker.setter
    def worker(self, obj):
        self._worker = obj

    @property
    def mutex(self):
        return self._mutex



class ComponentBase:
    """
    Is a base class of various data component types.
    """
    def __init__(self, *, buffer=None):
        """
        :param buffer:
        """
        #
        assert buffer

        #
        super().__init__()

        #
        self._buffer = buffer
        self._data = None

    @property
    def data_format(self):
        """
        :return: The data type of the data component.
        """
        return self._buffer.data_format

    @property
    def data_format_namespace(self):
        """
        :return: The data type namespace of the data component.
        """
        return self._buffer.data_format

    @property
    def source_id(self):
        """
        :return: The source ID of the data component.
        """
        return self._buffer.source_id

    @property
    def data(self):
        """
        :return: The component data.
        """
        return self._data


class ComponentUnknown(ComponentBase):
    """
    Represents a data component that is classified as
    :const:`PART_DATATYPE_UNKNOWN` by the GenTL Standard.
    """
    def __init__(self):
        #
        super().__init__()


class Component2DImage(ComponentBase):
    """
    Represents a data component that is classified as
    :const:`PART_DATATYPE_2D_IMAGE` by the GenTL Standard.
    """
    def __init__(self, *, buffer=None, part=None, node_map=None, logger=None):
        """
        :param buffer:
        :param part:
        :param node_map:
        """
        #
        assert buffer
        assert node_map

        #
        super().__init__(buffer=buffer)

        self._logger = logger or get_logger(name=__name__)

        #
        self._part = part
        self._node_map = node_map
        self._data = None
        self._num_components_per_pixel = 0

        symbolic = self.data_format

        # Determine the data type:
        if self.x_padding > 0:
            # In this case, the client will have to trim the padding part.
            # so we create a NumPy array that consists of uint8 elements
            # first. The client will interpret the array in an appropriate
            # dtype in the end once he trimmed:
            dtype = 'uint8'
            bytes_per_pixel_data_component = 1
        else:
            if symbolic in uint16_formats:
                dtype = 'uint16'
                bytes_per_pixel_data_component = 2
            elif symbolic in uint32_formats:
                dtype = 'uint32'
                bytes_per_pixel_data_component = 4
            elif symbolic in float32_formats:
                dtype = 'float32'
                bytes_per_pixel_data_component = 4
            elif symbolic in uint8_formats:
                dtype = 'uint8'
                bytes_per_pixel_data_component = 1
            else:
                # Sorry, Harvester can't handle this:
                self._data = None
                return

        # Determine the number of components per pixel:
        if symbolic in lmn_444_location_formats:
            num_components_per_pixel = 3.
        elif symbolic in lmn_422_location_formats:
            num_components_per_pixel = 2.
        elif symbolic in lmn_411_location_formats:
            num_components_per_pixel = 1.5
        elif symbolic in lmno_4444_location_formats:
            num_components_per_pixel = 4.
        elif symbolic in mono_location_formats or \
                symbolic in bayer_location_formats:
            num_components_per_pixel = 1.
        else:
            # Sorry, Harvester can't handle this:
            self._data = None
            return

        self._num_components_per_pixel = num_components_per_pixel
        self._symbolic = symbolic

        #
        width = self.width
        height = self.height

        #
        if self._part:
            count = self._part.data_size
            count //= bytes_per_pixel_data_component
            data_offset = self._part.data_offset
        else:
            count = width * height
            count *= num_components_per_pixel
            count += self.y_padding
            data_offset = 0

        # Convert the Python's built-in bytes array to a Numpy array:
        if _is_logging_buffer_manipulation:
            self._logger.debug(
                'Component 2D image ('
                'len(raw_buffer): {0}, '
                'int(count): {1}, '
                'dtype: {2}, '
                'offset: {3}, '
                'pixel format: {4},'
                'x padding: {5},'
                'y padding: {6}'
                ')'.format(
                    len(self._buffer.raw_buffer),
                    int(count),
                    dtype,
                    data_offset,
                    symbolic,
                    self.x_padding,
                    self.y_padding,
                )
            )

        self._data = np.frombuffer(
            self._buffer.raw_buffer,
            count=int(count),
            dtype=dtype,
            offset=data_offset
        )

    def represent_pixel_location(self):
        """
        Returns a NumPy array that represents the 2D pixel location,
        which is defined by PFNC, of the original image data.

        You may use the returned NumPy array for a calculation to map the
        original image to another format.

        :return: A NumPy array that represents the 2D pixel location.
        """
        if self.data is None:
            return None

        #
        return self._data.reshape(
            self.height + self.y_padding,
            int(self.width * self._num_components_per_pixel + self.x_padding)
        )

    @property
    def num_components_per_pixel(self):
        """
        :return: The number of data components per pixel.
        """
        return self._num_components_per_pixel

    def __repr__(self):
        return '{0} x {1}, {2}, {3} elements,\n{4}'.format(
            self.width,
            self.height,
            self.data_format,
            self.data.size,
            self.data
        )

    @property
    def width(self):
        """
        :return: The width of the data component in the buffer in number of pixels.
        """
        try:
            if self._part:
                value = self._part.width
            else:
                value = self._buffer.width
        except GenericException:
            value = self._node_map.Width.value
        return value

    @property
    def height(self):
        """
        :return: The height of the data component in the buffer in number of pixels.
        """
        try:
            if self._part:
                value = self._part.height
            else:
                value = self._buffer.height
        except GenericException:
            value = self._node_map.Height.value
        return value

    @property
    def data_format_value(self):
        """
        :return: The data type of the data component as integer value.
        """
        try:
            if self._part:
                value = self._part.data_format
            else:
                value = self._buffer.pixel_format
        except GenericException:
            value = self._node_map.PixelFormat.value
        return value

    @property
    def data_format(self):
        """
        :return: The data type of the data component as string.
        """
        return symbolics[self.data_format_value]

    @property
    def delivered_image_height(self):
        """
        :return: The image height of the data component.
        """
        try:
            if self._part:
                value = self._part.delivered_image_height
            else:
                value = self._buffer.delivered_image_height
        except GenericException:
            value = 0
        return value

    @property
    def x_offset(self):  # TODO: Check the naming convention.
        """
        :return: The X offset of the data in the buffer in number of pixels from the image origin to handle areas of interest.
        """
        try:
            if self._part:
                value = self._part.x_offset
            else:
                value = self._buffer.offset_x
        except GenericException:
            value = self._node_map.OffsetX.value
        return value

    @property
    def y_offset(self):
        """
        :return: The Y offset of the data in the buffer in number of pixels from the image origin to handle areas of interest.
        """
        try:
            if self._part:
                value = self._part.y_offset
            else:
                value = self._buffer.offset_y
        except GenericException:
            value = self._node_map.OffsetY.value
        return value

    @property
    def x_padding(self):
        """
        Returns
        :return: The X padding of the data component in the buffer in number of pixels.
        """
        try:
            if self._part:
                value = self._part.x_padding
            else:
                value = self._buffer.padding_x
        except GenericException:
            value = 0
        return value

    @property
    def y_padding(self):
        """
        :return: The Y padding of the data component in the buffer in number of pixels.
        """
        try:
            if self._part:
                value = self._part.y_padding
            else:
                value = self._buffer.padding_y
        except GenericException:
            value = 0
        return value


class Buffer:
    """
    Is provided by an :class:`ImageAcquire` object when you call its
    :meth:`~harvesters.core.ImageAcquirer.fetch_buffer` method. It provides
    you a way to access acquired data and its relevant information.

    Note that it will never be necessary to create this object by yourself
    in general.
    """
    def __init__(self, *, buffer=None, node_map=None, logger=None):
        """
        :param buffer:
        :param node_map:
        :param logger:
        """

        #
        assert buffer
        assert node_map

        #
        self._logger = logger or get_logger(name=__name__)

        #
        super().__init__()

        #
        self._buffer = buffer
        self._node_map = node_map

        self._payload = self._build_payload(
            buffer=buffer,
            node_map=node_map,
            logger=self._logger
        )

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.queue()

    def __repr__(self):
        return '{0}'.format(self.payload.__repr__())

    @property
    def timestamp_ns(self):
        """
        :return: The timestamp in nano-second.
        """
        return self._buffer.timestamp_ns

    @property
    def timestamp(self):
        """
        :return: The timestamp in the TL specific unit.
        """
        timestamp = 0
        try:
            timestamp = self._buffer.timestamp_ns
        except GenericException:
            try:
                _ = self.timestamp_frequency
            except GenericException:
                pass
            else:
                try:
                    timestamp = self._buffer.timestamp
                except GenericException:
                    timestamp = 0

        return timestamp

    @property
    def timestamp_frequency(self):
        """
        :return: The timestamp frequency which is used to represent a timestamp.
        """
        #
        frequency = 1000000000  # Hz

        try:
            _ = self._buffer.timestamp_ns
        except GenericException:
            try:
                frequency = self._buffer.parent.parent.timestamp_frequency
            except GenericException:
                try:
                    frequency = self._node_map.GevTimestampTickFrequency.value
                except GenericException:
                    pass

        return frequency

    @property
    def payload_type(self):
        """
        :return: The payload type that the :class:`Buffer` object contains.
        """

        return self._buffer.payload_type

    @property
    def payload(self):
        """
        :return: A containing object which derives from :class:`PayloadBase` class.
        """
        return self._payload

    def queue(self):
        """
        Queues the buffer to prepare for the upcoming image acquisition. Once
        the buffer is queued, the :class:`Buffer` object will be obsolete.
        You'll have nothing to do with it.

        Note that you have to return the ownership of the fetched buffers to
        the :class:`ImageAcquirer` object before stopping image acquisition
        calling this method because the :class:`ImageAcquirer` object tries
        to clear the self-allocated buffers when it stops image acquisition.
        """
        #
        if _is_logging_buffer_manipulation:
            self._logger.debug(
                'Queued Buffer module #{0}'
                ' containing frame #{1}'
                ' to DataStream module {2}'
                ' of Device module {3}'
                '.'.format(
                    self._buffer.context,
                    self._buffer.frame_id,
                    self._buffer.parent.id_,
                    self._buffer.parent.parent.id_
                )
            )

        self._buffer.parent.queue_buffer(self._buffer)

    @staticmethod
    def _build_payload(*, buffer=None, node_map=None, logger=None):
        #
        assert buffer
        assert node_map

        #
        p_type = buffer.payload_type
        if p_type == PAYLOADTYPE_INFO_IDS.PAYLOAD_TYPE_UNKNOWN:
            payload = PayloadUnknown(
                buffer=buffer, node_map=node_map, logger=logger
            )
        elif p_type == PAYLOADTYPE_INFO_IDS.PAYLOAD_TYPE_IMAGE or \
                buffer.payload_type == PAYLOADTYPE_INFO_IDS.PAYLOAD_TYPE_CHUNK_DATA:
            payload = PayloadImage(
                buffer=buffer, node_map=node_map, logger=logger
            )
        elif p_type == PAYLOADTYPE_INFO_IDS.PAYLOAD_TYPE_RAW_DATA:
            payload = PayloadRawData(
                buffer=buffer, node_map=node_map, logger=logger
            )
        elif p_type == PAYLOADTYPE_INFO_IDS.PAYLOAD_TYPE_FILE:
            payload = PayloadFile(
                buffer=buffer, node_map=node_map, logger=logger
            )
        elif p_type == PAYLOADTYPE_INFO_IDS.PAYLOAD_TYPE_JPEG:
            payload = PayloadJPEG(
                buffer=buffer, node_map=node_map, logger=logger
            )
        elif p_type == PAYLOADTYPE_INFO_IDS.PAYLOAD_TYPE_JPEG2000:
            payload = PayloadJPEG2000(
                buffer=buffer, node_map=node_map, logger=logger
            )
        elif p_type == PAYLOADTYPE_INFO_IDS.PAYLOAD_TYPE_H264:
            payload = PayloadH264(
                buffer=buffer, node_map=node_map, logger=logger
            )
        elif p_type == PAYLOADTYPE_INFO_IDS.PAYLOAD_TYPE_CHUNK_ONLY:
            payload = PayloadChunkOnly(
                buffer=buffer, node_map=node_map, logger=logger
            )
        elif p_type == PAYLOADTYPE_INFO_IDS.PAYLOAD_TYPE_MULTI_PART:
            payload = PayloadMultiPart(
                buffer=buffer, node_map=node_map, logger=logger
            )
        else:
            payload = None

        return payload


class PayloadBase:
    """
    Is a base class of various payload types. The types are defined by the
    GenTL Standard.
    """
    def __init__(self, *, buffer=None, logger=None):
        """
        :param buffer:
        :param logger:
        """
        #
        assert buffer

        #
        self._logger = logger or get_logger(name=__name__)

        #
        super().__init__()

        self._buffer = buffer
        self._components = []

    @property
    def payload_type(self):
        """
        TODO:
        :return:
        """
        return self._buffer.payload_type

    def _build_component(self, buffer=None, part=None, node_map=None):
        #
        if part:
            data_format = part.data_format
        else:
            data_format = buffer.pixel_format

        #
        symbolic = symbolics[data_format]
        if symbolic in component_2d_formats:
            return Component2DImage(
                buffer=buffer, part=part, node_map=node_map,
                logger=self._logger
            )

        return None

    @property
    def components(self):
        """
        :return: A :class:`list` containing objects that derive from :const:`ComponentBase` class.
        """
        return self._components


class PayloadUnknown(PayloadBase):
    """
    Represents a payload that is classified as :const:`PAYLOAD_TYPE_UNKNOWN`
    by the GenTL Standard.
    """
    def __init__(self, *, buffer=None, node_map=None, logger=None):
        """

        :param buffer:
        :param node_map:
        :param logger:
        """

        #
        assert buffer
        assert node_map

        #
        self._logger = logger or get_logger(name=__name__)

        #
        super().__init__(buffer=buffer, logger=self._logger)


class PayloadImage(PayloadBase):
    """
    Represents a payload that is classified as :const:`PAYLOAD_TYPE_IMAGE` by
    the GenTL Standard.
    """
    def __init__(self, *, buffer=None, node_map=None, logger=None):
        """

        :param buffer:
        :param node_map:
        :param logger:
        """

        #
        assert buffer
        assert node_map

        #
        self._logger = logger or get_logger(name=__name__)

        #
        super().__init__(buffer=buffer, logger=self._logger)

        # Build data components.
        self._components.append(
            self._build_component(
                buffer=buffer, node_map=node_map
            )
        )

    def __repr__(self):
        return '{0}'.format(self.components[0].__repr__())


class PayloadRawData(PayloadBase):
    """
    Represents a payload that is classified as :const:`PAYLOAD_TYPE_RAW_DATA`
    by the GenTL Standard.
    """
    def __init__(self, *, buffer=None, node_map=None, logger=None):
        """

        :param buffer:
        :param node_map:
        :param logger:
        """

        #
        assert buffer
        assert node_map

        #
        self._logger = logger or get_logger(name=__name__)

        #
        super().__init__(buffer=buffer, logger=self._logger)


class PayloadFile(PayloadBase):
    """
    Represents a payload that is classified as :const:`PAYLOAD_TYPE_FILE` by
    the GenTL Standard.
    """
    def __init__(self, *, buffer=None, node_map=None, logger=None):
        """

        :param buffer:
        :param node_map:
        :param logger:
        """

        #
        assert buffer
        assert node_map

        #
        self._logger = logger or get_logger(name=__name__)

        #
        super().__init__(buffer=buffer, logger=self._logger)


class PayloadJPEG(PayloadBase):
    """
    Represents a payload that is classified as :const:`PAYLOAD_TYPE_JPEG` by
    the GenTL Standard.
    """
    def __init__(self, *, buffer=None, node_map=None, logger=None):
        """

        :param buffer:
        :param node_map:
        :param logger:
        """

        #
        assert buffer
        assert node_map

        #
        self._logger = logger or get_logger(name=__name__)

        #
        super().__init__(buffer=buffer, logger=self._logger)


class PayloadJPEG2000(PayloadBase):
    """
    Represents a payload that is classified as :const:`PAYLOAD_TYPE_JPEG2000`
    by the GenTL Standard.
    """
    def __init__(self, *, buffer=None, node_map=None, logger=None):
        """

        :param buffer:
        :param node_map:
        :param logger:
        """

        #
        assert buffer
        assert node_map

        #
        self._logger = logger or get_logger(name=__name__)

        #
        super().__init__(buffer=buffer, logger=self._logger)


class PayloadH264(PayloadBase):
    """
    Represents a payload that is classified as :const:`PAYLOAD_TYPE_H264` by
    the GenTL Standard.
    """
    def __init__(self, *, buffer=None, node_map=None, logger=None):
        """

        :param buffer:
        :param node_map:
        :param logger:
        """

        #
        assert buffer
        assert node_map

        #
        self._logger = logger or get_logger(name=__name__)

        #
        super().__init__(buffer=buffer, logger=self._logger)


class PayloadChunkOnly(PayloadBase):
    """
    Represents a payload that is classified as
    :const:`PAYLOAD_TYPE_CHUNK_ONLY` by the GenTL Standard.
    """
    def __init__(self, *, buffer=None, node_map=None, logger=None):
        #
        assert buffer
        assert node_map

        #
        self._logger = logger or get_logger(name=__name__)

        #
        super().__init__(buffer=buffer, logger=self._logger)


class PayloadMultiPart(PayloadBase):
    """
    Represents a payload that is classified as
    :const:`PAYLOAD_TYPE_MULTI_PART` by the GenTL Standard.
    """
    def __init__(self, *, buffer=None, node_map=None, logger=None):
        """

        :param buffer:
        :param node_map:
        :param logger:
        """

        #
        assert buffer
        assert node_map

        #
        self._logger = logger or get_logger(name=__name__)

        #
        super().__init__(buffer=buffer, logger=self._logger)
        #

        # Build data components.
        # We know the buffer consists of a set of "part" that is
        # defined by the GenTL standard.
        for i, part in enumerate(self._buffer.parts):
            self._components.append(
                self._build_component(
                    buffer=buffer, part=part, node_map=node_map
                )
            )

    def __repr__(self):
        ret = ''
        for i, c in enumerate(self.components):
            ret += 'Component #{0}: {1}\n'.format(i, c.__repr__())
        ret = ret[:-1]
        return ret


class ImageAcquirer:
    """
    Manages everything you need to acquire images from the connecting device.
    """

    #
    _event = Event()
    _specialized_tl_type = ['U3V', 'GEV']

    def _create_acquisition_thread(self):
        return _ImageAcquisitionThread(image_acquire=self, logger=self._logger)

    def __init__(
            self, *, parent=None, device=None,
            profiler=None, logger=None,
            sleep_duration=_sleep_duration_default,
            file_path=None
    ):
        """

        :param parent:
        :param device:
        :param profiler:
        :param logger:
        :param sleep_duration:
        :param file_path: (Optional) Set a path to camera description file which you want to load on the target node map instead of the one which the device declares.
        """

        #
        self._logger = logger or get_logger(name=__name__)

        #
        assert parent
        assert device

        #
        super().__init__()

        #
        self._parent = parent

        #
        interface = device.parent
        system = interface.parent

        env_var = 'HARVESTERS_XML_FILE_DIR'
        if env_var in os.environ:
            self._xml_dir = os.getenv(env_var)
        else:
            self._xml_dir = None


        #
        try:
            node_map = _get_port_connected_node_map(
                port=system.port, logger=self._logger,
                xml_dir=self._xml_dir
            )
        except GenericException as e:
            self._logger.error(e, exc_info=True)
        else:
            self._system = System(module=system, node_map=node_map)

        #
        try:
            node_map = _get_port_connected_node_map(
                port=interface.port, logger=self._logger,
                xml_dir=self._xml_dir
            )
        except GenericException as e:
            self._logger.error(e, exc_info=True)
        else:
            self._interface = Interface(
                module=interface, node_map=node_map, parent=self._system
            )

        #
        try:
            node_map = _get_port_connected_node_map(
                port=device.local_port, logger=self._logger,
                xml_dir=self._xml_dir
            )  # Local device's node map
        except GenericException as e:
            self._logger.error(e, exc_info=True)
        else:
            self._device = Device(
                module=device, node_map=node_map, parent=self._interface
            )

        #
        try:
            node_map = _get_port_connected_node_map(
                port=device.remote_port, logger=self._logger,
                file_path=file_path, xml_dir=self._xml_dir
            )  # Remote device's node map
        except GenericException as e:
            self._logger.error(e, exc_info=True)
        else:
            self._remote_device = RemoteDevice(
                module=self._device, node_map=node_map, parent=self._device
            )

        #
        self._data_streams = []
        self._event_new_buffer_managers = []

        self._create_ds_at_connection = True
        if self._create_ds_at_connection:
            self._setup_data_streams()

        #
        self._profiler = profiler

        #
        self._num_filled_buffers_to_hold = 1

        #
        self._sleep_duration = sleep_duration
        self._thread_image_acquisition = self._create_acquisition_thread()

        # Prepare handling the SIGINT event:
        self._threads = []
        self._threads.append(self._thread_image_acquisition)

        # Create a signal handler if it's being run in the main thread:
        self._sigint_handler = None
        if current_thread() is main_thread():
            self._sigint_handler = _SignalHandler(
                event=self._event, threads=self._threads, logger=self._logger
            )
            signal.signal(signal.SIGINT, self._sigint_handler)
            self._logger.info('Created a signal handler for SIGINT.')

        #
        self._num_images_to_acquire = -1

        #
        self._timeout_for_image_acquisition = 1  # ms

        #
        self._statistics = Statistics()

        #
        self._announced_buffers = []

        #
        self._has_acquired_1st_image = False
        self._is_acquiring_images = False
        self._keep_latest = True

        # Determine the default value:
        num_buffers_default = 16
        try:
            self._min_num_buffers = self._data_streams[0].buffer_announce_min
        except InvalidParameterException as e:
            # In general, a GenTL Producer should not raise the
            # InvalidParameterException to the inquiry for
            # STREAM_INFO_BUF_ANNOUNCE_MIN because it is totally legal
            # but we have observed a fact that there is at least one on
            # the market. As a workaround we involve this try-except block:
            self._logger.debug(e, exc_info=True)
            self._min_num_buffers = num_buffers_default
            self._num_buffers = num_buffers_default
        else:
            self._num_buffers = max(
                num_buffers_default, self._min_num_buffers
            )

        #
        self._signal_stop_image_acquisition = None

        #
        self._logger.info(
            'Instantiated an ImageAcquirer object for {0}.'.format(
                self._device.id_
            )
        )

        #
        self._chunk_adapter = self._get_chunk_adapter(
            device=self.device, node_map=self.remote_device.node_map
        )

        # A callback method when it's called when a new buffer is delivered:
        self._on_new_buffer_arrival = None

        #
        self._finalizer = weakref.finalize(self, self._destroy)

        #
        try:
            self._afr = self._remote_device.node_map.AcquisitionFrameRate
        except AttributeError:
            self._afr = None
        self._fetching_cycle_s = None

    @staticmethod
    def _get_chunk_adapter(*, device=None, node_map=None):
        if device.tl_type == 'U3V':
            return ChunkAdapterU3V(node_map)
        elif device.tl_type == 'GEV':
            return ChunkAdapterGEV(node_map)
        else:
            return ChunkAdapterGeneric(node_map)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self._finalizer()

    def destroy(self):
        self._finalizer()

    @property
    def on_new_buffer_arrival(self):
        return self._on_new_buffer_arrival

    @on_new_buffer_arrival.setter
    def on_new_buffer_arrival(self, value):
        self._on_new_buffer_arrival = value

    @property
    def keep_latest(self):
        return self._keep_latest

    @keep_latest.setter
    def keep_latest(self, value):
        self._keep_latest = value

    @property
    def num_buffers(self):
        return self._num_buffers

    @num_buffers.setter
    def num_buffers(self, value):
        #
        if value >= self._min_num_buffers:
            self._num_buffers = value
        else:
            raise ValueError(
                'The number of buffers must be '
                'greater than or equal to {0}'.format(
                    self._min_num_buffers
                )
            )

    @property
    def sleep_duration(self):
        return self._sleep_duration

    @property
    def min_num_buffers(self):
        return self._min_num_buffers

    @property
    def num_filled_buffers_to_hold(self):
        return self._num_filled_buffers_to_hold

    @num_filled_buffers_to_hold.setter
    def num_filled_buffers_to_hold(self, value):
        if 0 < value <= self._num_buffers:
            # Update the value:
            self._num_filled_buffers_to_hold = value

            # Move the stored buffers to the temporary list object:
            buffers = []
            while not self.thread_image_acquisition.queue.empty():
                buffers.append(
                    self.thread_image_acquisition.queue.get_nowait()
                )

            # Create a thread object again:
            self._thread_image_acquisition = self._create_acquisition_thread()

            # Move the buffers back to the newly created Queue object:
            while len(buffers) > 0:
                try:
                    self.thread_image_acquisition.queue.put(buffers.pop(0))
                except Full as e:
                    # Can't put it because the queue is full.
                    # Discard the buffer:
                    self._logger.debug(e, exc_info=True)
                    buffer = buffers.pop(0)
                    buffer.parent.queue_buffer(buffer)

        else:
            raise ValueError(
                'The number of filled buffers to hold must be '
                'greater than zero and '
                'smaller than or equal to {0}'.format(
                    self._num_buffers
                )
            )

    @property
    def num_holding_filled_buffers(self):
        return self.thread_image_acquisition.queue.qsize()

    @property
    def data_streams(self):
        return self._data_streams

    @property
    def remote_device(self):
        """
        :return: The remote device.
        """
        return self._remote_device

    @property
    def device(self):
        """
        :return: The proxy :class:`Device` module object of the connecting remote device.
        """
        return self._device

    @property
    def interface(self):
        """
        :return: The parent :class:`Interface` module object of the connecting remote device.
        """
        return self._interface

    @property
    def system(self):
        """
        :return: The parent :class:`System` module object of the connecting remote device.
        """
        return self._system

    def is_acquiring_images(self):
        """
        :return: :const:`True` if it's acquiring images. Otherwise :const:`False`.
        """
        return self._is_acquiring_images

    @property
    def timeout_for_image_acquisition(self):
        return self._timeout_for_image_acquisition

    @timeout_for_image_acquisition.setter
    def timeout_for_image_acquisition(self, ms):
        with self.thread_image_acquisition:
            self._timeout_for_image_acquisition = ms

    @property
    def thread_image_acquisition(self):
        return self._thread_image_acquisition

    @thread_image_acquisition.setter
    def thread_image_acquisition(self, obj):
        self._thread_image_acquisition = obj
        self._thread_image_acquisition.worker = self.worker_image_acquisition

    @property
    def signal_stop_image_acquisition(self):
        return self._signal_stop_image_acquisition

    @signal_stop_image_acquisition.setter
    def signal_stop_image_acquisition(self, obj):
        self._signal_stop_image_acquisition = obj

    @property
    def statistics(self):
        return self._statistics

    @keep_latest.setter
    def keep_latest(self, value):
        self._keep_latest = value

    def _setup_data_streams(self):
        for i, stream_id in enumerate(self._device.data_stream_ids):
            #
            _data_stream = self._device.create_data_stream()

            try:
                _data_stream.open(stream_id)
            except GenericException as e:
                self._logger.debug(e, exc_info=True)
            else:
                self._logger.info(
                    'Opened DataStream module {0} of {1}.'.format(
                        _data_stream.id_, _data_stream.parent.id_
                    )
                )

            try:
                node_map = _get_port_connected_node_map(
                    port=_data_stream.port, logger=self._logger
                )
            except GenericException as e:
                self._logger.error(e, exc_info=True)
            else:
                self._data_streams.append(
                    DataStream(
                        module=_data_stream, node_map=node_map,
                        parent=self._device
                    )
                )
                # Create an Event Manager object for image acquisition.
                event_token = self._data_streams[i].register_event(
                    EVENT_TYPE_LIST.EVENT_NEW_BUFFER
                )
                self._event_new_buffer_managers.append(
                    EventManagerNewBuffer(event_token)
                )

    def start_image_acquisition(self):
        """
        Starts image acquisition.

        :return: None.
        """
        if not self._create_ds_at_connection:
            self._setup_data_streams()

        # Update the expected cycle to fetch a buffer:
        if self._afr and self._afr.value != 0:
            self._fetching_cycle_s = 1 / self._afr.value
        else:
            self._fetching_cycle_s = None

        #
        num_required_buffers = self._num_buffers
        for data_stream in self._data_streams:
            try:
                num_buffers = data_stream.buffer_announce_min
                if num_buffers < num_required_buffers:
                    num_buffers = num_required_buffers
            except GenericException as e:
                num_buffers = num_required_buffers
                self._logger.debug(e, exc_info=True)

            if data_stream.defines_payload_size():
                buffer_size = data_stream.payload_size
            else:
                buffer_size = self.remote_device.node_map.PayloadSize.value

            raw_buffers = self._create_raw_buffers(
                num_buffers, buffer_size
            )

            buffer_tokens = self._create_buffer_tokens(
                raw_buffers
            )

            self._announced_buffers = self._announce_buffers(
                data_stream=data_stream, _buffer_tokens=buffer_tokens
            )

            self._queue_announced_buffers(
                data_stream=data_stream, buffers=self._announced_buffers
            )

        # Reset the number of images to acquire.
        try:
            acq_mode = self.remote_device.node_map.AcquisitionMode.value
            if acq_mode == 'Continuous':
                num_images_to_acquire = -1
            elif acq_mode == 'SingleFrame':
                num_images_to_acquire = 1
            elif acq_mode == 'MultiFrame':
                num_images_to_acquire = self.remote_device.node_map.AcquisitionFrameCount.value
            else:
                num_images_to_acquire = -1
        except GenericException as e:
            # The node doesn't exist.
            num_images_to_acquire = -1
            self._logger.debug(e, exc_info=True)

        self._num_images_to_acquire = num_images_to_acquire

        try:
            # We're ready to start image acquisition. Lock the device's
            # transport layer related features:
            self.remote_device.node_map.TLParamsLocked.value = 1
        except GenericException:
            # SFNC < 2.0
            pass

        # Start image acquisition.
        self._is_acquiring_images = True

        for data_stream in self._data_streams:
            data_stream.start_acquisition(
                ACQ_START_FLAGS_LIST.ACQ_START_FLAGS_DEFAULT,
                self._num_images_to_acquire
            )

        #
        if self.thread_image_acquisition:
            self.thread_image_acquisition.start()

        #
        self.remote_device.node_map.AcquisitionStart.execute()

        self._logger.info(
            '{0} started image acquisition.'.format(self._device.id_)
        )

        if self._profiler:
            self._profiler.print_diff()

    def worker_image_acquisition(self, context=None):
        for event_manager in self._event_new_buffer_managers:
            try:
                if self.is_acquiring_images():
                    event_manager.update_event_data(
                        self._timeout_for_image_acquisition
                    )
                else:
                    return
            except TimeoutException:
                continue
            else:
                _now = datetime.now().timestamp()
                # Check if the delivered buffer is complete:
                if event_manager.buffer.is_complete():
                    #
                    if _is_logging_buffer_manipulation:
                        self._logger.debug(
                            'Acquired Buffer module #{0}'
                            ' containing frame #{1}'
                            ' from DataStream module {2}'
                            ' of Device module {3}'
                            '.'.format(
                                event_manager.buffer.context,
                                event_manager.buffer.frame_id,
                                event_manager.parent.id_,
                                event_manager.parent.parent.id_
                            )
                        )

                    queue = context if context else None
                    if self.keep_latest:
                        # We want to keep the latest ones:
                        with MutexLocker(self.thread_image_acquisition):
                            if not self._is_acquiring_images:
                                return

                            if queue and queue.full():
                                # Pick up the oldest one:
                                buffer = queue.get()

                                if _is_logging_buffer_manipulation:
                                    self._logger.debug(
                                        'Queued Buffer module #{0}'
                                        ' containing frame #{1}'
                                        ' to DataStream module {2}'
                                        ' of Device module {3}'
                                        '.'.format(
                                            buffer.context,
                                            buffer.frame_id,
                                            buffer.parent.id_,
                                            buffer.parent.parent.id_
                                        )
                                    )
                                # Then discard/queue it:
                                buffer.parent.queue_buffer(buffer)

                        # Get the latest buffer:
                        buffer = event_manager.buffer

                        # Then append it to the list which the user fetches later:
                        if queue:
                            queue.put(buffer)

                        # Then update the statistics using the buffer:
                        self._update_statistics(buffer)
                    else:
                        # Get the latest buffer:
                        buffer = event_manager.buffer

                        # Then update the statistics using the buffer:
                        self._update_statistics(buffer)

                        # We want to keep the oldest ones:
                        with MutexLocker(self.thread_image_acquisition):
                            #
                            if not self._is_acquiring_images:
                                return

                            #
                            if queue.full():
                                # We have not space to keep the latest one.
                                # Discard/queue the latest buffer:
                                buffer.parent.queue_buffer(buffer)
                            else:
                                # Just append it to the list:
                                if queue:
                                    queue.put(buffer)

                    #
                    if self._num_images_to_acquire >= 1:
                        self._num_images_to_acquire -= 1

                    if self._on_new_buffer_arrival:
                        self._on_new_buffer_arrival()

                    if self._num_images_to_acquire == 0:
                        #
                        if self.signal_stop_image_acquisition:
                            self.signal_stop_image_acquisition.emit()

                else:
                    # Discard/queue the latest buffer when incomplete
                    self._logger.debug(
                        'Acquired buffer is complete: {0}'.format(
                            event_manager.buffer.is_complete()
                        )
                    )

                    # Queue the incomplete buffer; we have nothing to do
                    # with it:
                    data_stream = event_manager.buffer.parent
                    data_stream.queue_buffer(event_manager.buffer)

                    #
                    with MutexLocker(self.thread_image_acquisition):
                        if not self._is_acquiring_images:
                            return

    def _update_chunk_data(self, buffer=None):
        try:
            if buffer.num_chunks == 0:
                """
                self._logger.debug(
                    'The buffer does not contain any chunk data.'
                )
                """
                return
        except (ParsingChunkDataException, ErrorException) as e:
            #self._logger.error(e, exc_info=True)
            pass
        except (
                NotImplementedException, NoDataException,
                InvalidBufferException
            ) as e:
            #self._logger.debug(e, exc_info=True)
            pass
        else:
            """
            self._logger.debug(
                'The buffer contains chunk data.'
            )
            """

            #
            is_generic = False
            if buffer.tl_type not in self._specialized_tl_type:
                is_generic = True

            try:
                if is_generic:
                    self._chunk_adapter.attach_buffer(
                        buffer.raw_buffer, buffer.chunk_data_info_list
                    )
                else:
                    self._chunk_adapter.attach_buffer(buffer.raw_buffer)
            except GenericException as e:
                # Failed to parse the chunk data. Something must be wrong.
                self._logger.error(e, exc_info=True)
            else:
                """
                self._logger.debug(
                    'Updated the node map of {0}.'.format(
                        buffer.parent.parent.id_
                    )
                )
                """
                pass

    def fetch_buffer(self, *, timeout=0, is_raw=False, cycle_s=None):
        """
        Fetches an available :class:`Buffer` object that has been filled up with a single image and returns it.

        :param timeout: Set the period [s] that defines the expiration for an available buffer delivery; if no buffer is fetched within the period then TimeoutException will be raised.
        :param is_raw: Set :const:`True` if you need a raw GenTL Buffer module; note that you'll have to manipulate the object by yourself.
        :param cycle_s: Set the cycle [s] that defines how frequently check if a buffer is available.

        :return: A :class:`Buffer` object.

        """
        #
        if not self.is_acquiring_images():
            # Does not make any sense. Raise TimeoutException:
            raise TimeoutException

        #
        if cycle_s:
            # Use the specified cycle:
            _cycle_s = cycle_s
        else:
            # Use the library default value:
            _cycle_s = 0.0001

        watch_timeout = True if timeout > 0 else False
        buffer = None

        base = time.time()
        while buffer is None:
            if watch_timeout and (time.time() - base) > timeout:
                # Expired the suggested period; give it up:
                raise TimeoutException
            else:
                with MutexLocker(self.thread_image_acquisition):
                    try:
                        _buffer = self.thread_image_acquisition.queue.get(
                            block=True, timeout=_cycle_s
                        )
                    except Empty:
                        continue
                    else:
                        if is_raw:
                            buffer = _buffer
                        else:
                            # Update the chunk data:
                            self._update_chunk_data(buffer=_buffer)
                            #
                            buffer = Buffer(
                                buffer=_buffer,
                                node_map=self.remote_device.node_map,
                                logger=self._logger
                            )

        if _is_logging_buffer_manipulation:
            self._logger.debug(
                'Fetched Buffer module #{0}'
                ' containing frame #{1}'
                ' of DataStream module {2}'
                ' of Device module {2}'
                '.'.format(
                    buffer._buffer.context,
                    buffer._buffer.frame_id,
                    buffer._buffer.parent.id_,
                    buffer._buffer.parent.parent.id_
                )
            )

        return buffer

    def _update_statistics(self, buffer):
        #
        assert buffer

        #
        self._statistics.increment_num_images()
        self._statistics.update_timestamp(buffer)

    @staticmethod
    def _create_raw_buffers(num_buffers, size):
        #
        assert num_buffers >= 0
        assert size >= 0

        # Instantiate a list object.
        raw_buffers = []

        # Append bytes objects to the list.
        # The number is specified by num_buffer and the buffer size is
        # specified by size.
        for _ in range(num_buffers):
            raw_buffers.append(bytes(size))

        # Then return the list.
        return raw_buffers

    @staticmethod
    def _create_buffer_tokens(raw_buffers):
        #
        assert raw_buffers

        # Instantiate a list object.
        _buffer_tokens = []

        # Append Buffer Token object to the list.
        for i, buffer in enumerate(raw_buffers):
            _buffer_tokens.append(
                BufferToken(buffer, i)
            )

        # Then returns the list.
        return _buffer_tokens

    def _announce_buffers(self, data_stream=None, _buffer_tokens=None):
        #
        assert data_stream

        #
        announced_buffers = []

        # Iterate announcing buffers in the Buffer Tokens.
        for token in _buffer_tokens:
            # Get an announced buffer.
            announced_buffer = data_stream.announce_buffer(token)

            # And append it to the list.
            announced_buffers.append(announced_buffer)

            #
            self._logger.debug(
                'Announced Buffer #{0} to DataStraem {1}.'.format(
                    announced_buffer.context,
                    data_stream.id_
                )
            )

        # Then return the list of announced Buffer objects.
        return announced_buffers

    def _queue_announced_buffers(self, data_stream=None, buffers=None):
        #
        assert data_stream

        #
        for buffer in buffers:
            data_stream.queue_buffer(buffer)
            self._logger.debug(
                'Queued Buffer module #{0}'
                ' to DataStream module {1}'
                ' of Device module {2}'
                '.'.format(
                    buffer.context,
                    data_stream.id_,
                    data_stream.parent.id_
                )
            )

    def stop_image_acquisition(self):
        """
        Stops image acquisition.

        :return: None.
        """
        if self.is_acquiring_images():
            #
            self._is_acquiring_images = False

            #
            if self.thread_image_acquisition.is_running():  # TODO
                self.thread_image_acquisition.stop()

            with MutexLocker(self.thread_image_acquisition):
                #
                self.remote_device.node_map.AcquisitionStop.execute()

                try:
                    # Unlock TLParamsLocked in order to allow full device
                    # configuration:
                    self.remote_device.node_map.TLParamsLocked.value = 0
                except GenericException:
                    # SFNC < 2.0
                    pass

                for data_stream in self._data_streams:
                    # Stop image acquisition.
                    try:
                        data_stream.stop_acquisition(
                            ACQ_STOP_FLAGS_LIST.ACQ_STOP_FLAGS_KILL
                        )
                    except GenericException as e:
                        self._logger.error(e, exc_info=True)

                    # Flash the queue for image acquisition process.
                    data_stream.flush_buffer_queue(
                        ACQ_QUEUE_TYPE_LIST.ACQ_QUEUE_ALL_DISCARD
                    )

                for event_manager in self._event_new_buffer_managers:
                    event_manager.flush_event_queue()

                if self._create_ds_at_connection:
                    self._release_buffers()
                else:
                    self._release_data_streams()

            #
            self._has_acquired_1st_image = False

            #
            self._chunk_adapter.detach_buffer()

            #
            self._logger.info(
                '{0} stopped image acquisition.'.format(self._device.id_)
            )

        if self._profiler:
            self._profiler.print_diff()

    def _destroy(self):
        """
        Destroys the :class:`ImageAcquirer` object. Once you called this
        method, all allocated resources, including buffers and the remote
        device, are released.

        :return: None.
        """
        # Ask its parent to destroy it:
        if self._device:
            self._parent._destroy_image_acquirer(self)

    def _release_data_streams(self):
        #
        self._release_buffers()

        #
        for data_stream in self._data_streams:
            if data_stream and data_stream.is_open():
                name_ds = data_stream.id_
                name_dev = data_stream.parent.id_
                data_stream.close()
                self._logger.info(
                    'Closed DataStream module {0} of {1}.'.format(
                        name_ds, name_dev
                    )
                )

        #
        self._data_streams.clear()
        self._event_new_buffer_managers.clear()

    def _release_buffers(self):
        for data_stream in self._data_streams:
            if data_stream.is_open():
                #
                for buffer in self._announced_buffers:
                    self._logger.debug(
                        'Revoked Buffer module #{0}.'.format(
                            buffer.context,
                            data_stream.id_,
                            data_stream.parent.id_
                        )
                    )
                    _ = data_stream.revoke_buffer(buffer)

        self._announced_buffers.clear()

        # Flush the queue; we don't need the buffers anymore:
        if self.thread_image_acquisition.queue:
            while not self.thread_image_acquisition.queue.empty():
                _ = self.thread_image_acquisition.queue.get_nowait()


def _retrieve_file_path(*, port=None, url=None, file_path=None, logger=None, xml_dir=None):
    #
    _logger = logger or get_logger(name=__name__)

    #
    if file_path:
        # A file that is specified by the client will be used:
        if not os.path.exists(file_path):
            raise LogicalErrorException(
                '{0} does not exist.'.format(file_path)
            )
    else:
        if url is None:
            # Inquire its URL information.
            if len(port.url_info_list) > 0:
                url = port.url_info_list[0].url
            else:
                raise LogicalErrorException(
                    'The target port does not hold any URL.'
                )

        _logger.info('URL: {0}'.format(url))

        # And parse the URL.
        location, others = url.split(':', 1)
        location = location.lower()

        if location == 'local':
            file_name, address, size = others.split(';')
            address = int(address, 16)

            # It may specify the schema version.
            delimiter = '?'
            if delimiter in size:
                size, _ = size.split(delimiter)
            size = int(size, 16)  # From Hex to Dec

            # Now we get the file content.
            size, binary_data = port.read(address, size)

            # Store the XML file on the host side; it may be a Zipped XML
            # file or a plain XML file:
            file_path = _save_file(
                file_dir=xml_dir, file_name=file_name,
                binary_data=binary_data
            )

        elif location == 'file':
            # '///c|/program%20files/foo.xml' ->
            # '///', 'c|/program%20files/foo.xml'
            _, _file_path = others.split('///')

            # 'c|/program%20files/foo.xml' -> 'c|/program files/foo.xml'
            _file_path = unquote(_file_path)

            # 'c|/program files/foo.xml' -> 'c:/program files/foo.xml')
            _file_path.replace('|', ':')

            # Now we get a file path that we daily use:
            file_path = _file_path

        elif location == 'http' or location == 'https':
            raise NotImplementedError(
                'Failed to parse URL {0}: Harvester has not supported '
                'downloading a device description file from vendor '
                'web site. If you must rely on the current condition,'
                'just try to make a request to the Harvester '
                'maintainer.'.format(url)
            )
        else:
            raise LogicalErrorException(
                'Failed to parse URL {0}: Unknown format.'.format(url)
            )

    return file_path


def _save_file(*, file_dir=None, file_name=None, binary_data=None):
    #
    assert binary_data
    assert file_name

    #
    bytes_io = io.BytesIO(binary_data)

    if file_dir is not None:
        # Create the directory if it didn't exist:
        if not os.path.exists(file_dir):
            os.makedirs(file_dir)
    else:
        file_dir = tempfile.mkdtemp(
            prefix=datetime.now().strftime('%Y%m%d%H%M%S_'),
        )

    #
    file_path = os.path.join(file_dir, file_name)

    #
    mode = 'w+'
    data_to_write = bytes_content = bytes_io.getvalue()
    if pathlib.Path(file_path).suffix.lower() == '.zip':
        mode += 'b'
    else:
        data_to_write = bytes_content.decode()
        pos = data_to_write.find('\x00')
        data_to_write = data_to_write[:pos]
    #
    with open(file_path, mode) as f:
        f.write(data_to_write)

    return file_path


def _get_port_connected_node_map(*, port=None, logger=None, file_path=None, xml_dir=None):
    #
    assert port

    #
    _logger = logger or get_logger(name=__name__)

    # Instantiate a GenICam node map object.
    node_map = NodeMap()

    #
    file_path = _retrieve_file_path(
        port=port, file_path=file_path, logger=logger, xml_dir=xml_dir
    )

    #
    if file_path is not None:
        # Every valid (zipped) XML file MUST be parsed as expected and the
        # method returns the file path where the file is located:
        # Then load the XML file content on the node map object.
        has_valid_file = True

        # In this case, the file has been identified as a Zip file but
        # has been diagnosed as BadZipFile due to a technical reason.
        # Let the NodeMap object load the file from the path:
        try:
            node_map.load_xml_from_zip_file(file_path)
        except RuntimeException:
            try:
                node_map.load_xml_from_file(file_path)
            except RuntimeException as e:
                _logger.error(e, exc_info=True)
                has_valid_file = False

        if has_valid_file:
            # Instantiate a concrete port object of the remote device's
            # port.
            concrete_port = ConcretePort(port)

            # And finally connect the concrete port on the node map
            # object.
            node_map.connect(concrete_port, port.name)

    # Then return the node map:
    return node_map


class Harvester:
    """
    Is the class that works for you as Harvester Core. Everything begins with
    this class.
    """
    #
    def __init__(self, *, profile=False, logger=None):
        """

        :param profile:
        :param logger:
        """

        #
        self._logger = logger or get_logger(name=__name__)

        #
        super().__init__()

        #
        self._cti_files = []
        self._producers = []
        self._systems = []
        self._interfaces = []
        self._device_info_list = []
        self._ias = []

        #
        self._has_revised_device_list = False
        self._timeout_for_update = 1000  # ms

        #
        if profile:
            from harvesters._private.core.helper.profiler import Profiler
            self._profiler = Profiler()
        else:
            self._profiler = None

        if self._profiler:
            self._profiler.print_diff()

        #
        self._finalizer = weakref.finalize(self, self._reset)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self._finalizer()

    def reset(self):
        self._finalizer()

    @property
    def cti_files(self):
        """
        :return: A :class:`list` object containing :class:`str` objects.
        """
        return self._cti_files

    @property
    def device_info_list(self):
        """
        :return: A :class:`list` object containing :class:`DeviceInfo` objects
        """
        return self._device_info_list

    @property
    def timeout_for_update(self):
        return self._timeout_for_update

    @timeout_for_update.setter
    def timeout_for_update(self, ms):
        self._timeout_for_update = ms

    @property
    def has_revised_device_info_list(self):
        return self._has_revised_device_list

    @has_revised_device_info_list.setter
    def has_revised_device_info_list(self, value):
        self._has_revised_device_list = value

    def create_image_acquirer(
            self, list_index=None, *, id_=None,
            vendor=None, model=None, tl_type=None, user_defined_name=None,
            serial_number=None, version=None,
            sleep_duration=_sleep_duration_default, file_path=None,
            privilege='exclusive'
        ):
        """
        Creates an image acquirer for the specified remote device and return
        the created :class:`ImageAcquirer` object.

        :param list_index: (Optional) Set an item index of the list of :class:`DeviceInfo` objects.
        :param id_: (Optional) Set an index of the device information list.
        :param vendor: (Optional) Set a vendor name of the target device.
        :param model: (Optional) Set a model name of the target device.
        :param tl_type: (Optional) Set a transport layer type of the target device.
        :param user_defined_name: (Optional) Set a user defined name string of the target device.
        :param serial_number: (Optional) Set a serial number string of the target device.
        :param version: (Optional) Set a version number string of the target device.
        :param sleep_duration: (Optional) Set a sleep duration in second that is inserted after the image acquisition worker is executed.
        :param file_path: (Optional) Set a path to camera description file which you want to load on the target node map instead of the one which the device declares.
        :param privilege: (Optional) Set an access privilege. `exclusive`, `contorl`, and `read_only` are supported. The default is `exclusive`.

        :return: An :class:`ImageAcquirer` object that associates with the specified device.

        Note that you have to close it when you are ready to release the
        device that you have been controlled. As long as you hold it, the
        controlled device will be not available from other clients.

        """
        #
        if self.device_info_list is None:
            # TODO: Throw an exception to tell clients that there's no
            # device to connect.
            return

        # Instantiate a GenTL Device module.
        if list_index is not None:
            device = self.device_info_list[list_index].create_device()
        else:
            keys = [
                'id_', 'vendor', 'model', 'tl_type',
                'user_defined_name', 'serial_number', 'version',
            ]

            # Create a copy of the list. Do not use the original list:
            candidates = self.device_info_list.copy()

            for key in keys:
                key_value = eval(key)
                if key_value:
                    items_to_be_removed = []
                    # Find out the times to be removed from the candidates.
                    for item in candidates:
                        try:
                            if key_value != eval('item.' + key):
                                items_to_be_removed.append(item)
                        except GenericException as e:
                            # The candidate doesn't support the information.
                            self._logger.warn(e, exc_info=True)
                            pass
                    # Remove irrelevant items from the candidates.
                    for item in items_to_be_removed:
                        candidates.remove(item)

            num_candidates = len(candidates)
            if num_candidates > 1:
                raise ValueError(
                    'You have two or more candidates. '
                    'You have to pass one or more keys so that '
                    'a single candidate is specified.'
                )
            elif num_candidates == 0:
                raise ValueError(
                    'You have no candidate. '
                    'You have to pass one or more keys so that '
                    'a single candidate is specified.'
                )
            else:
                device = candidates[0].create_device()

        # Then open it.
        try:
            #
            if privilege == 'exclusive':
                _privilege = DEVICE_ACCESS_FLAGS_LIST.DEVICE_ACCESS_EXCLUSIVE
            elif privilege == 'control':
                _privilege = DEVICE_ACCESS_FLAGS_LIST.DEVICE_ACCESS_CONTROL
            elif privilege == 'read_only':
                _privilege = DEVICE_ACCESS_FLAGS_LIST.DEVICE_ACCESS_READONLY
            else:
                raise NotImplementedError(
                    '{0} is not supported.'.format(privilege)
                )

            #
            device.open(_privilege)

        except GenericException as e:
            self._logger.debug(e, exc_info=True)
            # Just re-throw the exception. The decision should be made by
            # the client but not Harvester:
            raise
        else:
            self._logger.info(
                'Opened Device module, {0}.'.format(device.id_)
            )

            # Create an :class:`ImageAcquirer` object and return it.
            ia = ImageAcquirer(
                parent=self, device=device, profiler=self._profiler,
                logger=self._logger, sleep_duration=sleep_duration,
                file_path=file_path
            )
            self._ias.append(ia)

            if self._profiler:
                self._profiler.print_diff()

        return ia

    def add_cti_file(self, file_path: str):
        """
        Adds a CTI file to work with to the CTI file list.

        :param file_path: Set a file path to the target CTI file.

        :return: None.
        """
        if not os.path.exists(file_path):
            self._logger.warning(
                'Attempted to add {0} which does not exist.'.format(file_path)
            )

        if file_path not in self._cti_files:
            self._cti_files.append(file_path)
            self._logger.info(
                'Added {0} to the CTI file list.'.format(file_path)
            )

    def remove_cti_file(self, file_path: str):
        """
        Removes the specified CTI file from the CTI file list.

        :param file_path: Set a file path to the target CTI file.

        :return: None.
        """
        if file_path in self._cti_files:
            self._cti_files.remove(file_path)
            self._logger.info(
                'Removed {0} from the CTI file list.'.format(file_path)
            )

    def remove_cti_files(self):
        """
        Removes all CTI files in the CTI file list.

        :return: None.
        """

        self._cti_files.clear()

        #
        self._logger.info('Removed the all CTI file from the list.')

    def _open_gentl_producers(self):
        #
        for file_path in self._cti_files:
            producer = GenTLProducer.create_producer()
            try:
                producer.open(file_path)
            except GenericException as e:
                self._logger.debug(e, exc_info=True)
            else:
                self._producers.append(producer)
                self._logger.info(
                    'Initialized GenTL Producer, {0}.'.format(
                        producer.path_name
                    )
                )

    def _open_systems(self):
        for producer in self._producers:
            system = producer.create_system()
            try:
                system.open()
            except GenericException as e:
                self._logger.debug(e, exc_info=True)
            else:
                self._systems.append(system)
                self._logger.info('Opened System module, {0}.'.format(
                        system.id_
                    )
                )

    def _reset(self):
        """
        Initializes the :class:`Harvester` object. Once you reset the
        :class:`Harvester` object, all allocated resources, including buffers
        and remote device, will be released.

        :return: None.
        """
        #
        for ia in self._ias:
            ia._destroy()

        self._ias.clear()

        #
        self._logger.info('Started resetting the Harvester object.')
        self.remove_cti_files()
        self._release_gentl_producers()

        if self._profiler:
            self._profiler.print_diff()

        #
        self._logger.info('Completed resetting the Harvester object.')

    def _release_gentl_producers(self):
        #
        self._release_systems()

        #
        for producer in self._producers:
            if producer and producer.is_open():
                name = producer.path_name
                producer.close()
                self._logger.info('Closed {0}.'.format(name))

        #
        self._producers.clear()

    def _release_systems(self):
        #
        self._release_interfaces()

        #
        for system in self._systems:
            if system is not None and system.is_open():
                name = system.id_
                system.close()
                self._logger.info('Closed System module, {0}.'.format(name))

        #
        self._systems.clear()

    def _release_interfaces(self):
        #
        self._release_device_info_list()

        #
        if self._interfaces is not None:
            for iface in self._interfaces:
                if iface.is_open():
                    name = iface.id_
                    iface.close()
                    self._logger.info(
                        'Closed Interface module, {0}.'.format(name)
                    )

        #
        self._interfaces.clear()

    def _release_device_info_list(self):
        #
        if self.device_info_list is not None:
            self._device_info_list.clear()

        #
        self._logger.info('Discarded the device information list.')

    def update_device_info_list(self):
        """
        Updates the device information list. You'll have to call this method
        every time you added CTI files or plugged/unplugged devices.

        :return: None.
        """
        #
        self._release_gentl_producers()

        try:
            self._open_gentl_producers()
            self._open_systems()
            #
            for system in self._systems:
                #
                system.update_interface_info_list(self.timeout_for_update)

                #
                for i_info in system.interface_info_list:
                    iface = i_info.create_interface()
                    try:
                        iface.open()
                    except GenericException as e:
                        self._logger.debug(e, exc_info=True)
                    else:
                        self._logger.info(
                            'Opened Interface module, {0}.'.format(iface.id_)
                        )
                        iface.update_device_info_list(self.timeout_for_update)
                        self._interfaces.append(iface)
                        for d_info in iface.device_info_list:
                            self.device_info_list.append(
                                DeviceInfo(device_info=d_info)
                            )

        except GenericException as e:
            self._logger.error(e, exc_info=True)
            self._has_revised_device_list = False
        else:
            self._has_revised_device_list = True

        #
        self._logger.info('Updated the device information list.')

    def _destroy_image_acquirer(self, ia):
        """
        Releases all external resources including the controlling device.
        """

        id_ = None
        if ia.device:
            #
            ia.stop_image_acquisition()

            #
            ia._release_data_streams()

            #
            id_ = ia._device.id_

            #
            if ia.remote_device.node_map:
                #
                if ia._chunk_adapter:
                    ia._chunk_adapter.detach_buffer()
                    ia._chunk_adapter = None
                    self._logger.info(
                        'Detached a buffer from the chunk adapter of {0}.'.format(
                            id_
                        )
                    )

                ia.device.node_map.disconnect()
                self._logger.info(
                    'Disconnected the port from the NodeMap of {0}.'.format(
                        id_
                    )
                )

            #
            if ia._device.is_open():
                ia._device.close()
                self._logger.info(
                    'Closed Device module, {0}.'.format(id_)
                )

        ia._device = None

        #
        if id_:
            self._logger.info(
                'Destroyed the ImageAcquirer object which {0} '
                'had belonged to.'.format(id_)
            )
        else:
            self._logger.info(
                'Destroyed an ImageAcquirer.'
            )

        if self._profiler:
            self._profiler.print_diff()

        self._ias.remove(ia)


if __name__ == '__main__':
    pass
