# coding: utf-8

from __future__ import annotations

from typing import Dict, Optional

from jsonschema import ValidationError, validate

from supervisely._utils import take_with_default
from supervisely.annotation.tag_meta import TagValueType
from supervisely.collection.str_enum import StrEnum
from supervisely.io.json import JsonSerializable
from supervisely.project.project_type import ProjectType
from supervisely.sly_logger import logger


class LabelingInterface(str, StrEnum):
    """Enumerates supported labeling UI interfaces for a project (images, multiview, medical, etc.)."""

    DEFAULT = "default"
    MEDICAL_IMAGING_SINGLE = "medical_imaging_single"
    IMAGES_WITH_16_COLOR = "images_with_16_color"
    MULTISPECTRAL = "multispectral"
    MULTIVIEW = "multi_view"
    IMAGE_MATTING = "image_matting"
    FISHEYE = "fisheye"


class ProjectSettingsJsonFields:
    """JSON field names used in :class:`~supervisely.project.project_settings.ProjectSettings` serialization."""

    MULTI_VIEW = "multiView"
    ENABLED = "enabled"
    TAG_ID = "tagId"
    TAG_NAME = "tagName"
    IS_SYNCED = "isSynced"
    LABELING_INTERFACE = "labelingInterface"


class ProjectSettingsRequiredSchema:
    """JSON schema for validating serialized :class:`~supervisely.project.project_settings.ProjectSettings`."""

    SCHEMA = {
        "type": "object",
        "properties": {
            ProjectSettingsJsonFields.MULTI_VIEW: {
                "type": "object",
                "properties": {
                    ProjectSettingsJsonFields.ENABLED: {"type": "boolean"},
                    ProjectSettingsJsonFields.TAG_ID: {"type": ["integer", "null"]},
                    ProjectSettingsJsonFields.TAG_NAME: {"type": ["string", "null"]},
                    ProjectSettingsJsonFields.IS_SYNCED: {"type": "boolean"},
                },
                "required": [
                    ProjectSettingsJsonFields.ENABLED,
                    ProjectSettingsJsonFields.TAG_ID,
                    ProjectSettingsJsonFields.TAG_NAME,
                    ProjectSettingsJsonFields.IS_SYNCED,
                ],
                "additionalProperties": False,
            },
            ProjectSettingsJsonFields.LABELING_INTERFACE: {"type": ["string", "null"]},
        },
        "required": [ProjectSettingsJsonFields.MULTI_VIEW],
        "additionalProperties": False,
    }


def validate_project_settings_schema(data: dict) -> None:
    try:
        validate(instance=data, schema=ProjectSettingsRequiredSchema.SCHEMA)
    except ValidationError as e:
        if e.json_path == "$":
            raise ValidationError(
                f"The validation has failed with the following message: {e.message}."
            )
        msg = f"The validation of the field {e.json_path} has failed with the following message: {e.message}."
        if e.validator == "required":
            raise ValidationError(
                f"{msg} Check the correctness of the field names in the {ProjectSettingsJsonFields}."
            )
        elif e.validator == "type":
            raise ValidationError(
                f"{msg} Check the correctness of the field value types in the {ProjectSettingsRequiredSchema}."
            )
        else:
            raise ValidationError(msg)


class ProjectSettings(JsonSerializable):
    """Project settings: multi-view mode, labeling interface, etc."""

    def __init__(
        self,
        multiview_enabled: bool = False,
        multiview_tag_name: Optional[str] = None,
        multiview_tag_id: Optional[int] = None,
        multiview_is_synced: bool = False,
        labeling_interface: Optional[LabelingInterface] = None,
    ):
        """:param multiview_enabled: Enable multi-view mode.
        :type multiview_enabled: bool
        :param multiview_tag_name: Name of the tag used as group tag for multi-window mode.
        :type multiview_tag_name: str, optional
        :param multiview_tag_id: Id of the tag used as group tag for multi-window mode.
        :type multiview_tag_id: int, optional
        :param multiview_is_synced: Enable synchronization of views for multi-view mode.
        :type multiview_is_synced: bool
        :param labeling_interface: The interface for labeling images.
        :type labeling_interface: str, optional
        :raises ValidationError: if settings schema is invalid.

        :Usage Example:

            .. code-block:: python

                import supervisely as sly
                settings_json = {"multiView": {"enabled": True, "tagName": "group_tag", "tagId": None, "isSynced": False}}
                settings = sly.ProjectSettings.from_json(settings_json)
        """
        self.multiview_enabled = multiview_enabled
        self.multiview_tag_name = multiview_tag_name
        self.multiview_tag_id = multiview_tag_id
        self.multiview_is_synced = multiview_is_synced

        if labeling_interface is not None:
            if labeling_interface not in LabelingInterface.values():
                raise ValueError(
                    f"Invalid labeling interface value: {labeling_interface}. The available values: {LabelingInterface.values()}"
                )

        self.labeling_interface = labeling_interface

        if multiview_enabled is False and multiview_is_synced is True:
            logger.warn(
                "The 'Group Images sync mode' is enabled, but it won't effect while multi-view mode is disabled. Please enable the multi-view mode (a.k.a. 'Group Images mode')."
            )

    @classmethod
    def from_json(cls, data: Dict) -> ProjectSettings:
        validate_project_settings_schema(data)

        d_multiview = data[ProjectSettingsJsonFields.MULTI_VIEW]
        labeling_interface = data.get(ProjectSettingsJsonFields.LABELING_INTERFACE)

        return cls(
            multiview_enabled=d_multiview[ProjectSettingsJsonFields.ENABLED],
            multiview_tag_name=d_multiview[ProjectSettingsJsonFields.TAG_NAME],
            multiview_tag_id=d_multiview[ProjectSettingsJsonFields.TAG_ID],
            multiview_is_synced=d_multiview[ProjectSettingsJsonFields.IS_SYNCED],
            labeling_interface=labeling_interface,
        )

    def to_json(self) -> dict:
        data = {
            ProjectSettingsJsonFields.MULTI_VIEW: {
                ProjectSettingsJsonFields.ENABLED: self.multiview_enabled,
                ProjectSettingsJsonFields.TAG_NAME: self.multiview_tag_name,
                ProjectSettingsJsonFields.TAG_ID: self.multiview_tag_id,
                ProjectSettingsJsonFields.IS_SYNCED: self.multiview_is_synced,
            }
        }
        if self.labeling_interface is not None:
            data[ProjectSettingsJsonFields.LABELING_INTERFACE] = self.labeling_interface
        validate_project_settings_schema(data)
        return data

    def clone(
        self,
        multiview_enabled: bool = None,
        multiview_tag_name: Optional[str] = None,
        multiview_tag_id: Optional[int] = None,
        multiview_is_synced: bool = None,
        labeling_interface: Optional[LabelingInterface] = None,
    ):
        return ProjectSettings(
            multiview_enabled=take_with_default(multiview_enabled, self.multiview_enabled),
            multiview_tag_name=take_with_default(multiview_tag_name, self.multiview_tag_name),
            multiview_tag_id=take_with_default(multiview_tag_id, self.multiview_tag_id),
            multiview_is_synced=take_with_default(multiview_is_synced, self.multiview_is_synced),
            labeling_interface=take_with_default(labeling_interface, self.labeling_interface),
        )

    def validate(self, meta):
        if meta.project_settings.multiview_enabled is True:
            # Images multiview
            if meta.project_type == ProjectType.IMAGES:
                mtag_name = meta.project_settings.multiview_tag_name
                if mtag_name is None:
                    if meta.project_settings.multiview_tag_id is None:
                        return  # (tag_name, tag_id) == (None, None) is OK
                    mtag_name = meta.get_tag_name_by_id(meta.project_settings.multiview_tag_id)
                    if mtag_name is None:
                        raise RuntimeError(
                            f"The multi-view tag with ID={meta.project_settings.multiview_tag_id} was not found in the project meta. "
                            "Please directly add the tag meta that will be used for image grouping for multi-view labeling interface."
                        )

                multi_tag = meta.get_tag_meta(mtag_name)
                if multi_tag is None:
                    raise RuntimeError(
                        f"The multi-view tag '{mtag_name}' was not found in the project meta. Please directly add the tag meta "
                        "that will be used for image grouping for multi-view labeling interface."
                    )
                elif multi_tag.value_type != TagValueType.ANY_STRING:
                    raise RuntimeError(
                        f"The multi-view tag value type should be '{TagValueType.ANY_STRING}'. The provided type: '{multi_tag.value_type}'."
                    )

            # Video multiview
            elif meta.project_type == ProjectType.VIDEOS:
                if (
                    meta.project_settings.multiview_tag_name is not None
                    or meta.project_settings.multiview_tag_id is not None
                ):
                    raise RuntimeError(
                        "For video projects, multiview_tag_name and multiview_tag_id should be None. "
                        "Videos are grouped by datasets, not by tags."
                    )

        if meta.project_settings.labeling_interface is not None:
            if meta.project_settings.labeling_interface not in LabelingInterface.values():
                raise RuntimeError(
                    f"Invalid labeling interface value: {meta.project_settings.labeling_interface}. "
                    f"The available values: {LabelingInterface.values()}"
                )
