import os
from dataclasses import dataclass
from typing import Optional, Set

from dataclasses_jsonschema import JsonSchemaMixin

from arcor2 import rest
from arcor2.data.common import Pose
from arcor2.data.object_type import Model3dType, Models
from arcor2.data.scene import MeshFocusAction
from arcor2.exceptions import Arcor2Exception
from arcor2.exceptions.helpers import handle

URL = os.getenv("ARCOR2_SCENE_SERVICE_URL", "http://0.0.0.0:5013")


class SceneServiceException(Arcor2Exception):
    pass


@dataclass
class MeshParameters(JsonSchemaMixin):

    mesh_scale_x: float = 1.0
    mesh_scale_y: float = 1.0
    mesh_scale_z: float = 1.0
    transform_id: str = "world"


@handle(SceneServiceException, message="Failed to add or update the collision model.")
def upsert_collision(model: Models, pose: Pose, mesh_parameters: Optional[MeshParameters] = None) -> None:
    """Adds arbitrary collision model to the collision scene.

    :param model: Box, Sphere, Cylinder, Mesh
    :param pose: Pose of the collision object.
    :param mesh_parameters: Some additional parameters might be specified for mesh collision model.
    :return:

    Example usage:

    >>> from arcor2.data.object_type import Box
    >>> from arcor2.data.common import Pose, Position, Orientation
    >>> box = Box("UniqueBoxId", 0.1, 0.1, 0.1)
    >>> scene_service.upsert_collision(box, Pose(Position(1, 0, 0), Orientation(0, 0, 0, 1)))
    """

    model_id = model.id
    params = model.to_dict()
    del params["id"]
    params[model.__class__.__name__.lower() + "Id"] = model_id

    if model.type() == Model3dType.MESH and mesh_parameters:
        params.update(mesh_parameters.to_dict())

    rest.call(rest.Method.PUT, f"{URL}/collisions/{model.type().value.lower()}", body=pose, params=params)


@handle(SceneServiceException, message="Failed to delete the collision.")
def delete_collision_id(collision_id: str) -> None:
    rest.call(rest.Method.DELETE, f"{URL}/collisions/{collision_id}")


@handle(SceneServiceException, message="Failed to list collisions.")
def collision_ids() -> Set[str]:
    return set(rest.call(rest.Method.GET, f"{URL}/collisions", list_return_type=str))


@handle(SceneServiceException, message="Failed to focus the object.")
def focus(mfa: MeshFocusAction) -> Pose:
    return rest.call(rest.Method.PUT, f"{URL}/utils/focus", body=mfa, return_type=Pose)


def delete_all_collisions() -> None:

    for cid in collision_ids():
        delete_collision_id(cid)


@handle(SceneServiceException, message="Failed to start the scene.")
def start() -> None:
    """To be called after all objects are created."""

    rest.call(rest.Method.PUT, f"{URL}/system/start")


@handle(SceneServiceException, message="Failed to stop the scene.")
def stop() -> None:
    """To be called when project is closed or when main script ends."""

    rest.call(rest.Method.PUT, f"{URL}/system/stop")


def started() -> bool:
    """Checks whether the scene is running."""

    return rest.call(rest.Method.PUT, f"{URL}/system/running", return_type=bool)


@handle(SceneServiceException, message="Failed to get transforms.")
def transforms() -> Set[str]:
    """Gets available transformations."""

    return set(rest.call(rest.Method.GET, f"{URL}/transforms", list_return_type=str))


@handle(SceneServiceException, message="Failed to add or update the transform.")
def upsert_transform(transform_id: str, parent: str, pose: Pose) -> None:
    """Add or updates transform."""

    rest.call(rest.Method.PUT, f"{URL}/transforms", body=pose, params={"transformId": transform_id, "parent": parent})


@handle(SceneServiceException, message="Failed to get the local pose.")
def local_pose(transform_id: str) -> Pose:
    """Gets relative pose to parent."""

    return rest.call(rest.Method.GET, f"{URL}/transforms/{transform_id}/localPose", return_type=Pose)


@handle(SceneServiceException, message="Failed to get the world pose.")
def world_pose(transform_id: str) -> Pose:
    """Gets absolute pose in world space."""

    return rest.call(rest.Method.GET, f"{URL}/transforms/{transform_id}/worldPose", return_type=Pose)


__all__ = [
    SceneServiceException.__name__,
    upsert_collision.__name__,
    delete_collision_id.__name__,
    collision_ids.__name__,
    focus.__name__,
    delete_all_collisions.__name__,
    start.__name__,
    stop.__name__,
    transforms.__name__,
    upsert_transform.__name__,
    local_pose.__name__,
    world_pose.__name__,
]
