# Copyright © 2020, Joseph Berry, Rico Tabor (opendrop.dev@gmail.com)
# OpenDrop is released under the GNU GPL License. You are free to
# modify and distribute the code, but always under the same license
# (i.e. you cannot make commercial derivatives).
#
# If you use this software in your research, please cite the following
# journal articles:
#
# J. D. Berry, M. J. Neeson, R. R. Dagastine, D. Y. C. Chan and
# R. F. Tabor, Measurement of surface and interfacial tension using
# pendant drop tensiometry. Journal of Colloid and Interface Science 454
# (2015) 226–237. https://doi.org/10.1016/j.jcis.2015.05.012
#
# E. Huang, T. Denning, A. Skoufis, J. Qi, R. R. Dagastine, R. F. Tabor
# and J. D. Berry, OpenDrop: Open-source software for pendant drop
# tensiometry & contact angle measurements, submitted to the Journal of
# Open Source Software
#
# These citations help us not only to understand who is using and
# developing OpenDrop, and for what purpose, but also to justify
# continued development of this code and other open source resources.
#
# OpenDrop is distributed WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
# PURPOSE.  See the GNU General Public License for more details.  You
# should have received a copy of the GNU General Public License along
# with this software.  If not, see <https://www.gnu.org/licenses/>.


import asyncio
import itertools
from typing import Optional, Hashable, Iterable, MutableSequence

import numpy as np

from opendrop.app.common.image_acquirer import ImageSequenceAcquirer, CameraAcquirer
from opendrop.utility.bindable import AccessorBindable
from opendrop.utility.bindable.typing import Bindable
from opendrop.utility.misc import clamp


class AcquirerController:
    def destroy(self) -> None:
        pass


class ImageSequenceAcquirerController(AcquirerController):
    class _ImageRegistration:
        def __init__(self, image_id: Hashable, image: np.ndarray) -> None:
            self.image_id = image_id
            self.image = image

    def __init__(
            self, *,
            acquirer: ImageSequenceAcquirer,
            source_image_out: Bindable[Optional[np.ndarray]]
    ) -> None:
        self._acquirer = acquirer
        self._source_image_out = source_image_out

        self._image_registry = []  # type: MutableSequence[self._ImageRegistration]

        self.bn_num_images = AccessorBindable(
            getter=self._get_num_images,
        )

        self._showing_image_index = None  # type: Optional[int]
        self.bn_showing_image_index = AccessorBindable(
            getter=self._get_showing_image_index,
            setter=self._set_showing_image_index,
        )

        self._showing_image_id = None  # type: Optional[Hashable]

        self.__event_connections = [
            acquirer.bn_images.on_changed.connect(
                self._hdl_acquirer_images_changed
            ),
        ]
        self._hdl_acquirer_images_changed()

    def _hdl_acquirer_images_changed(self) -> None:
        self._update_image_registry()
        self._update_showing_image()

    def _update_image_registry(self) -> None:
        acquirer_images = list(self._acquirer.bn_images.get())

        for image_reg in tuple(self._get_unassociated_registered_images(acquirer_images)):
            self._image_registry.remove(image_reg)
            self._on_image_deregistered(image_reg.image_id)

        for image in self._filter_unregistered_images(acquirer_images):
            new_image_reg = self._ImageRegistration(
                image_id=id(image),
                image=image,
            )
            self._image_registry.append(new_image_reg)
            self._on_image_registered(
                image_id=new_image_reg.image_id,
                image=image,
            )

        self.bn_num_images.poke()

    def _filter_unregistered_images(self, images: Iterable[np.ndarray]) -> Iterable[np.ndarray]:
        return itertools.filterfalse(self._is_image_registered, images)

    def _is_image_registered(self, image: np.ndarray) -> bool:
        try:
            self._get_image_reg_by_image(image)
        except ValueError:
            return False
        else:
            return True

    def _get_unassociated_registered_images(self, images: Iterable[np.ndarray]) -> Iterable[_ImageRegistration]:
        return (
            image_reg
            for image_reg in self._image_registry
            if not self._is_registered_image_associated(images, image_reg)
        )

    @staticmethod
    def _is_registered_image_associated(images: Iterable[np.ndarray], image_reg: _ImageRegistration) -> bool:
        for image in images:
            if np.array_equal(image_reg.image, image):
                return True
        else:
            return False

    def _update_showing_image(self) -> None:
        acquirer_images = list(self._acquirer.bn_images.get())
        if self._showing_image_index is None and len(acquirer_images) > 0:
            self._showing_image_index = 0
            self.bn_showing_image_index.poke()

        if self._showing_image_index is None:
            return

        new_showing_image_reg = self._get_image_reg_by_image(
            image=acquirer_images[self._showing_image_index]
        )

        new_showing_image_id = new_showing_image_reg.image_id

        if new_showing_image_id == self._showing_image_id:
            return

        self._showing_image_id = new_showing_image_id
        self._on_image_changed(new_showing_image_id)
        self._update_source_image_out()

    def _update_source_image_out(self) -> None:
        image_reg = self._get_image_reg_by_image_id(
            image_id=self._showing_image_id,
        )

        image = image_reg.image

        self._source_image_out.set(image)

    def _get_image_reg_by_image(self, image: np.ndarray) -> _ImageRegistration:
        for image_reg in self._image_registry:
            if np.array_equal(image_reg.image, image):
                return image_reg
        else:
            raise ValueError(
                "No _ImageRegistration found for image '{}'"
                .format(image)
            )

    def _get_image_reg_by_image_id(self, image_id: Hashable) -> _ImageRegistration:
        for image_reg in self._image_registry:
            if image_reg.image_id == image_id:
                return image_reg
        else:
            raise ValueError(
                "No _ImageRegistration found for image_id '{}'"
                .format(image_id)
            )

    def _get_showing_image_index(self) -> Optional[int]:
        return self._showing_image_index

    def _set_showing_image_index(self, idx: Optional[int]) -> None:
        if idx is None:
            return

        idx = clamp(idx, 0, self.bn_num_images.get() - 1)
        self._showing_image_index = idx
        self._update_showing_image()

    def _get_num_images(self) -> int:
        return len(self._acquirer.bn_images.get())

    def _on_image_registered(self, image_id: Hashable, image: np.ndarray) -> None:
        pass

    def _on_image_deregistered(self, image_id: Hashable) -> None:
        pass

    def _on_image_changed(self, image_id: Hashable) -> None:
        pass

    def destroy(self) -> None:
        for ec in self.__event_connections:
            ec.disconnect()


class CameraAcquirerController(AcquirerController):
    PREVIEW_FRAME_RATE = 3

    def __init__(
            self, *,
            acquirer: CameraAcquirer,
            source_image_out: Bindable[Optional[np.ndarray]]
    ) -> None:
        self._loop = asyncio.get_event_loop()

        self._acquirer = acquirer
        self._source_image_out = source_image_out

        self._update_loop_handle = None

        self.__event_connections = [
            acquirer.bn_camera.on_changed.connect(
                self._hdl_acquirer_camera_changed
            )
        ]

        self._hdl_acquirer_camera_changed()

    def _hdl_acquirer_camera_changed(self) -> None:
        camera = self._acquirer.bn_camera.get()

        if camera is None:
            self._cancel_pending_update_loop()
        else:
            self._update_loop()

        self._on_camera_changed()

    def _update_loop(self) -> None:
        self._cancel_pending_update_loop()

        self._update_source_image_out()

        self._update_loop_handle = self._loop.call_later(
            delay=1 / self.PREVIEW_FRAME_RATE,
            callback=self._update_loop,
        )

    def _update_source_image_out(self) -> None:
        camera = self._acquirer.bn_camera.get()
        if camera is None:
            return

        self._source_image_out.set(
            camera.capture()
        )

    def _cancel_pending_update_loop(self) -> None:
        if self._update_loop_handle is None:
            return

        self._update_loop_handle.cancel()
        self._update_loop_handle = None

    def _on_camera_changed(self) -> None:
        pass

    def destroy(self) -> None:
        self._cancel_pending_update_loop()

        for ec in self.__event_connections:
            ec.disconnect()
