# -*- coding: utf-8 -*-
"""
Defines a microscope object, binding a camera and stage with basic functionality.
"""
import logging
import pkg_resources
import uuid

from openflexure_microscope.stage.base import BaseStage
from openflexure_microscope.stage.mock import MockStage
from openflexure_microscope.camera.base import BaseCamera
from openflexure_microscope.camera.mock import MockStreamer

from openflexure_microscope.utilities import serialise_array_b64
from openflexure_microscope.config import user_settings

from openflexure_microscope.common.labthings_core.lock import CompositeLock


class Microscope:
    """
    A basic microscope object.

    The camera and stage should already be initialised, and passed as arguments.

    Attributes:
        lock (:py:class:`openflexure_microscope.common.lock.CompositeLock`): Composite lock controlling thread access
            to multiple pieces of hardware.
        camera (:py:class:`openflexure_microscope.camera.base.BaseCamera`): Camera object
        stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): Stage object
        task: (:py:class:`openflexure_microscope.task.TaskOrchestrator`): Threaded ask orchestrator for managing
            background tasks using microscope hardware
    """

    def __init__(self):
        # Initial attributes
        self.id = uuid.uuid4()
        self.name = self.id
        self.fov = [0, 0]
        self.camera = None
        self.stage = None

        # Initialise with an empty composite lock
        self.lock = CompositeLock([])

        # Apply settings loaded from file
        self.apply_settings(user_settings.load())

    def __enter__(self):
        """Create microscope on context enter."""
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        """Close microscope on context exit."""
        self.close()

    def close(self):
        """Shut down the microscope hardware."""
        logging.info("Closing {}".format(self))
        if self.camera:
            self.camera.close()
        if self.stage:
            self.stage.close()
        logging.info("Closed {}".format(self))

    def attach(self, camera: BaseCamera, stage: BaseStage):
        """
        Retroactively attaches a camera and stage to the microscope object.

        Allows the microscope to be created as a "dummy", with hardware communications
        opened at a later time.

        Args:
            camera (:py:class:`openflexure_microscope.camera.base.BaseCamera`): camera object
            stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): stage object
        """

        settings_full = self.read_settings()

        logging.debug("Attaching camera...")
        self.camera = (
            camera
        )  #: :py:class:`openflexure_microscope.camera.base.BaseCamera`: Picamera object
        if not self.camera:
            logging.info("No camera attached.")
        else:
            logging.info("Attached camera {}".format(camera))

            if hasattr(self.camera, "lock"):  # If camera has a lock
                logging.info("Attaching {} to composite lock.".format(self.camera.lock))
                # Add the lock to the microscope composite lock
                self.lock.locks.append(self.camera.lock)

        logging.debug("Attaching stage...")
        self.stage = (
            stage
        )  #: :py:class:`openflexure_microscope.stage.base.BaseStage`: OpenFlexure stage object
        if not self.stage:
            logging.info("No stage attached.")
        else:
            logging.info("Attached stage {}".format(stage))

            if hasattr(self.stage, "lock"):  # If stage object has a lock
                logging.info(
                    "Attaching lock {} to composite lock.".format(self.stage.lock)
                )
                # Add the lock to the microscope composite lock
                self.lock.locks.append(self.stage.lock)

        logging.info("Reapplying settings to newly attached devices")
        self.apply_settings(settings_full)

    def has_real_stage(self):
        if hasattr(self, "stage") and not isinstance(self.stage, MockStage):
            return True
        else:
            return False

    def has_real_camera(self):
        if hasattr(self, "camera") and not isinstance(self.camera, MockStreamer):
            return True
        else:
            return False

    # Create unified status
    @property
    def status(self):
        """Dictionary of the basic microscope status.

        Return:
            dict: Dictionary containing complete microscope status
        """
        state = {
            "camera": self.camera.status,
            "stage": self.stage.status,
            "version": pkg_resources.get_distribution("openflexure_microscope").version,
        }
        return state

    def apply_settings(self, config: dict):
        """
        Applies a config dictionary. Missing parameters will be left untouched.
        """
        logging.debug("Microscope: Applying config: {}".format(config))

        # If attached to a camera
        if ("camera_settings" in config) and self.camera:
            self.camera.apply_settings(config["camera_settings"])

        # If attached to a stage
        if ("stage_settings" in config) and self.stage:
            self.stage.apply_settings(config["stage_settings"])

        # Todo: tidy up with some loopy goodness
        if "id" in config:
            self.id = config["id"]
        if "name" in config:
            self.name = config["name"]
        if "fov" in config:
            self.fov = config["fov"]

    def read_settings(self, json_safe=False):
        """
        Get an updated settings dictionary.

        Reads current attributes and properties from connected hardware,
        then merges those with the currently saved settings.

        This is to ensure that settings for currently disconnected hardware
        don't get removed from the settings file.
        """

        settings_current = {"id": self.id, "name": self.name, "fov": self.fov}

        # If attached to a camera
        if self.camera:
            settings_current_camera = self.camera.read_settings()
            settings_current["camera_settings"] = settings_current_camera

        # If attached to a stage
        if self.stage:
            settings_current_stage = self.stage.read_settings()
            settings_current["stage_settings"] = settings_current_stage

        settings_full = user_settings.merge(settings_current)

        return settings_full

    def save_settings(self):
        """
        Merges the current settings back to disk
        """
        # Read curent config
        current_config = self.read_settings()
        # Save config to file
        if self.camera:
            self.camera.save_settings()
        if self.stage:
            self.stage.save_settings()
        user_settings.save(current_config, backup=True)

    @property
    def metadata(self):
        """
        Microscope system metadata, to be applied to basically all captures
        """
        system_metadata = {
            "microscope_settings": self.read_settings(),
            "microscope_state": self.status,
            "microscope_id": self.id,
            "microscope_name": self.name,
        }

        # Store an encoded copy of the PiCamera lens shading table, if it exists
        if self.camera and hasattr(self.camera, "read_lens_shading_table"):
            # Read LST. Returns None if no LST is active
            lst_arr = self.camera.read_lens_shading_table()

            b64_string, dtype, shape = serialise_array_b64(lst_arr)

            system_metadata["lens_shading_table"] = {
                "b64_string": b64_string,
                "dtype": dtype,
                "shape": shape,
            }

        return system_metadata
