import asyncio
import paraview.modules.vtkRemotingApplication
from paraview.modules.vtkRemotingServerManager import (
    vtkClientSession,
)  # for vtkClientSession.DATA_SERVER / RENDER_SERVER
from paraview.modules.vtkRemotingPythonAsyncCore import (
    vtkPythonObservableWrapperUtilities,
)

from paraview.modules.vtkRemotingServerManagerViews import (
    vtkSMParaViewPipelineControllerWithRendering,
)


class PropertyManager(paraview.modules.vtkRemotingApplication.vtkPTSPropertyManager):
    """Set and get properties of a proxy.

    PropertyManager is a thin wrapper of vtkPTSPropertyManager that exposes a more pythonic interface.
    """

    def SetValues(self, proxy, force_push=False, **kwargs):
        """Set properties on a proxy

        Examples:
          propertyManager.SetValues(sphere, Radius=4)
          propertyManager.SetValues(sphere, Radius=4, Center=[1,2,3])
          propertyManager.SetValues(sphere, **{ "Center": [1,2,3], "Radius": 5, "StartPhi": 30 })

          propertyManager.SetValues(sphere, Radius=4, force_push=True)
          # or equivalently
          propertyManager.SetValues(sphere, Radius=4)
          propertyManager.Push(sphere)
        """
        for key, value in kwargs.items():
            self._SetValueInternal(proxy, key, value)

        if force_push:
            proxy.UpdateVTKObjects()

    def _SetValueInternal(self, proxy, name, value):
        if not isinstance(name, str):
            raise TypeError(f"name (= {name}) parameter should be string !")

        if isinstance(value, (list, tuple)):
            for i, v in enumerate(value):
                self.SetPropertyValueElement(proxy, name, v, i)
        else:
            self.SetPropertyValueElement(proxy, name, value, 0)

    def GetValues(self, proxy, *property_names):
        """Get the value

        Example:
          propertyManager.GetValues(sphere, 'Radius') - >  { "Radius": 0.5 }
          propertyManager.GetValues(sphere, 'Radius', 'Center') - >  { "Radius": 0.5, "Center": [0,0,0] }
          propertyManager.GetValues(sphere) -> { 'Center' : [0,0,0], 'Radius' : 0.5, ... }
        """

        result = dict()

        # If nothing is provided, list all properties
        if not property_names:
            property_names = []
            iterator = proxy.NewPropertyIterator()
            iterator.UnRegister(None)
            iterator.Begin()
            while not iterator.IsAtEnd():
                property_names.append(iterator.GetKey())
                iterator.Next()

        # Extract all the values of the given property_names
        for name in property_names:
            numberOfElements = proxy.GetProperty(name).GetNumberOfElements()
            if numberOfElements > 1:
                result[name] = []
                for i in range(numberOfElements):
                    result[name].append(
                        PropertyManager._vtkVariantToPythonObject(
                            self.GetPropertyValueElement(proxy, name, i)
                        )
                    )
            else:
                result[name] = PropertyManager._vtkVariantToPythonObject(
                    self.GetPropertyValueElement(proxy, name, 0)
                )

        return result

    async def Fetch(self, *proxies):
        """Fetch properties from the server.

        Returns:
        list(bool) : For each of the proxies it returns true if the any properties were changed otherwise returns false.
        """
        return self.UpdateInformation(proxies)

    def Push(self, *proxies):
        """Push modified properties to the server.

        Update the VTK objects corresponding to the proxies on the server by
        pushing the values of all modified properties (un-modified properties
        are ignored).  If the objects have not been created, they will be
        created first.
        """

        for proxy in proxies:
            proxy.UpdateVTKObjects()

    async def Update(self, proxy):
        """Invoke a remote render and update the local View"""

        if not proxy.IsA("vtkSMViewProxy"):
            raise RuntimeError("Update() is valid only for vtkSMViewProxies")

        return await vtkPythonObservableWrapperUtilities.GetFuture(proxy.Update())

    async def UpdatePipeline(self, proxy, time=0.0):
        """Calls Update() on all sources with the given time request.
        On successful completion of the request, all data information
        for each of the output ports will also be up-to-date."""

        if not proxy.IsA("vtkSMSourceProxy"):
            raise RuntimeError("UpdatePipeline is valid only for vtkSMSourceProxies")

        return await vtkPythonObservableWrapperUtilities.GetFuture(
            proxy.UpdatePipeline(time)
        )

    async def UpdateInformation(self, *proxies):
        """Fetch properties for all proxies.

        Returns:
        Awaitable list(bool) : For each of the proxies it returns true if the any properties were changed otherwise returns false.
        """
        futures = [
            vtkPythonObservableWrapperUtilities.GetFuture(proxy.UpdateInformation())
            for proxy in proxies
        ]
        return asyncio.gather(*futures)

    def InvokeCommand(
        self,
        proxy,
        command_name,
        destination_mask=vtkClientSession.DATA_SERVER | vtkClientSession.RENDER_SERVER,
    ):
        """Invoke a command at the proxy on the server

        Example:
        TODO
        """
        proxy.InvokeCommand(command_name, destination_mask)

    async def ExecuteCommand(
        self,
        proxy,
        command_name,
        destination_mask=vtkClientSession.DATA_SERVER | vtkClientSession.RENDER_SERVER,
    ):
        """
        Invoke a command at the proxy on the server and obtain command execution status.

        Returns:
          bool : True if command succeded , False otherwise.
        """

        return await vtkPythonObservableWrapperUtilities.GetFuture(
            proxy.ExecuteCommand(command_name, destination_mask)
        )

    def SetVisibility(self, representation, view, visibility):
        """Set visibility of `representation` in `view`"""

        controller = vtkSMParaViewPipelineControllerWithRendering()
        if representation.GetXMLGroup() != "representations":
            raise RuntimeError("First argument is not a representation proxy")
        if view.GetXMLGroup() != "views":
            raise RuntimeError("First argument is not a view proxy")
        if visibility:
            controller.Show(representation, view)
        else:
            controller.Hide(representation, view)

    @staticmethod
    def _vtkVariantToPythonObject(variant):
        """Convert a variant to its base type."""

        # TODO can we make this less verbose ?
        if variant.IsString():
            return variant.ToString()
        elif variant.IsFloat():
            return variant.ToFloat()
        elif variant.IsDouble():
            return variant.ToDouble()
        elif variant.IsChar():
            return variant.ToChar()
        elif variant.IsUnsignedChar():
            return variant.ToUnsignedChar()
        elif variant.IsSignedChar():
            return variant.ToSignedChar()
        elif variant.IsShort():
            return variant.ToShort()
        elif variant.IsUnsignedShort():
            return variant.ToUnsignedShort()
        elif variant.IsInt():
            return variant.ToInt()
        elif variant.IsUnsignedInt():
            return variant.ToUnsignedInt()
        elif variant.IsLong():
            return variant.ToLong()
        elif variant.IsUnsignedLong():
            return variant.ToUnsignedLong()
        elif variant.IsLongLong():
            return variant.ToLongLong()
        elif variant.IsUnsignedLongLong():
            return variant.ToUnsignedLongLong()
        elif variant.IsVTKObject():
            return variant.ToVTKObject()
        else:
            raise NotImplementedError(
                f"Unsupported type for converting variant : {variant}"
            )
