"""UI core module that describe UI abstract class."""

import abc

import numpy as np

from fury.decorators import warn_on_args_to_kwargs
from fury.interactor import CustomInteractorStyle
from fury.io import load_image
from fury.lib import (
    Actor2D,
    CellArray,
    DiskSource,
    FloatArray,
    Points,
    PolyData,
    PolyDataMapper2D,
    Polygon,
    Property2D,
    TextActor,
    Texture,
    TexturedActor2D,
)
from fury.utils import set_input


class UI(object, metaclass=abc.ABCMeta):
    """An umbrella class for all UI elements.

    While adding UI elements to the scene, we go over all the sub-elements
    that come with it and add those to the scene automatically.

    Attributes
    ----------
    position : (float, float)
        Absolute coordinates (x, y) of the lower-left corner of this
        UI component.
    center : (float, float)
        Absolute coordinates (x, y) of the center of this UI component.
    size : (int, int)
        Width and height in pixels of this UI component.
    on_left_mouse_button_pressed: function
        Callback function for when the left mouse button is pressed.
    on_left_mouse_button_released: function
        Callback function for when the left mouse button is released.
    on_left_mouse_button_clicked: function
        Callback function for when clicked using the left mouse button
        (i.e. pressed -> released).
    on_left_mouse_double_clicked: function
        Callback function for when left mouse button is double clicked
        (i.e pressed -> released -> pressed -> released).
    on_left_mouse_button_dragged: function
        Callback function for when dragging using the left mouse button.
    on_right_mouse_button_pressed: function
        Callback function for when the right mouse button is pressed.
    on_right_mouse_button_released: function
        Callback function for when the right mouse button is released.
    on_right_mouse_button_clicked: function
        Callback function for when clicking using the right mouse button
        (i.e. pressed -> released).
    on_right_mouse_double_clicked: function
        Callback function for when right mouse button is double clicked
        (i.e pressed -> released -> pressed -> released).
    on_right_mouse_button_dragged: function
        Callback function for when dragging using the right mouse button.
    on_middle_mouse_button_pressed: function
        Callback function for when the middle mouse button is pressed.
    on_middle_mouse_button_released: function
        Callback function for when the middle mouse button is released.
    on_middle_mouse_button_clicked: function
        Callback function for when clicking using the middle mouse button
        (i.e. pressed -> released).
    on_middle_mouse_double_clicked: function
        Callback function for when middle mouse button is double clicked
        (i.e pressed -> released -> pressed -> released).
    on_middle_mouse_button_dragged: function
        Callback function for when dragging using the middle mouse button.
    on_key_press: function
        Callback function for when a keyboard key is pressed.

    """

    @warn_on_args_to_kwargs()
    def __init__(self, *, position=(0, 0)):
        """Init scene.

        Parameters
        ----------
        position : (float, float)
            Absolute coordinates (x, y) of the lower-left corner of this
            UI component.

        """
        self._scene = object()
        self._position = np.array([0, 0])
        self._callbacks = []

        self._setup()  # Setup needed actors and sub UI components.
        self.position = position

        self.left_button_state = "released"
        self.right_button_state = "released"
        self.middle_button_state = "released"

        self.on_left_mouse_button_pressed = lambda i_ren, obj, element: None
        self.on_left_mouse_button_dragged = lambda i_ren, obj, element: None
        self.on_left_mouse_button_released = lambda i_ren, obj, element: None
        self.on_left_mouse_button_clicked = lambda i_ren, obj, element: None
        self.on_left_mouse_double_clicked = lambda i_ren, obj, element: None
        self.on_right_mouse_button_pressed = lambda i_ren, obj, element: None
        self.on_right_mouse_button_released = lambda i_ren, obj, element: None
        self.on_right_mouse_button_clicked = lambda i_ren, obj, element: None
        self.on_right_mouse_double_clicked = lambda i_ren, obj, element: None
        self.on_right_mouse_button_dragged = lambda i_ren, obj, element: None
        self.on_middle_mouse_button_pressed = lambda i_ren, obj, element: None
        self.on_middle_mouse_button_released = lambda i_ren, obj, element: None
        self.on_middle_mouse_button_clicked = lambda i_ren, obj, element: None
        self.on_middle_mouse_double_clicked = lambda i_ren, obj, element: None
        self.on_middle_mouse_button_dragged = lambda i_ren, obj, element: None
        self.on_key_press = lambda i_ren, obj, element: None

    @abc.abstractmethod
    def _setup(self):
        """Set up this UI component.

        This is where you should create all your needed actors and sub UI
        components.

        """
        msg = "Subclasses of UI must implement `_setup(self)`."
        raise NotImplementedError(msg)

    @abc.abstractmethod
    def _get_actors(self):
        """Get the actors composing this UI component."""
        msg = "Subclasses of UI must implement `_get_actors(self)`."
        raise NotImplementedError(msg)

    @property
    def actors(self):
        """Actors composing this UI component."""
        return self._get_actors()

    @abc.abstractmethod
    def _add_to_scene(self, _scene):
        """Add all subcomponents or VTK props that compose this UI component.

        Parameters
        ----------
        _scene : Scene

        """
        msg = "Subclasses of UI must implement `_add_to_scene(self, scene)`."
        raise NotImplementedError(msg)

    def add_to_scene(self, scene):
        """Allow UI objects to add their own props to the scene.

        Parameters
        ----------
        scene : scene

        """
        self._add_to_scene(scene)

        # Get a hold on the current interactor style.
        iren = scene.GetRenderWindow().GetInteractor().GetInteractorStyle()

        for callback in self._callbacks:
            if not isinstance(iren, CustomInteractorStyle):
                msg = (
                    "The ShowManager requires `CustomInteractorStyle` in"
                    " order to use callbacks."
                )
                raise TypeError(msg)

            if callback[0] == self._scene:
                iren.add_callback(iren, callback[1], callback[2], args=[self])
            else:
                # iren.add_callback(*callback, args=[self])
                if len(callback) > 3:
                    iren.add_callback(*callback[:3], priority=callback[3], args=[self])

    @warn_on_args_to_kwargs()
    def add_callback(self, prop, event_type, callback, *, priority=0):
        """Add a callback to a specific event for this UI component.

        Parameters
        ----------
        prop : vtkProp
            The prop on which is callback is to be added.
        event_type : string
            The event code.
        callback : function
            The callback function.
        priority : int
            Higher number is higher priority.

        """
        # Actually since we need an interactor style we will add the callback
        # only when this UI component is added to the scene.
        self._callbacks.append((prop, event_type, callback, priority))

    @property
    def position(self):
        return self._position

    @position.setter
    def position(self, coords):
        coords = np.asarray(coords)
        self._set_position(coords)
        self._position = coords

    @abc.abstractmethod
    def _set_position(self, _coords):
        """Position the lower-left corner of this UI component.

        Parameters
        ----------
        _coords: (float, float)
            Absolute pixel coordinates (x, y).

        """
        msg = "Subclasses of UI must implement `_set_position(self, coords)`."
        raise NotImplementedError(msg)

    @property
    def size(self):
        return np.asarray(self._get_size(), dtype=int)

    @abc.abstractmethod
    def _get_size(self):
        msg = "Subclasses of UI must implement property `size`."
        raise NotImplementedError(msg)

    @property
    def center(self):
        return self.position + self.size / 2.0

    @center.setter
    def center(self, coords):
        """Position the center of this UI component.

        Parameters
        ----------
        coords: (float, float)
            Absolute pixel coordinates (x, y).

        """
        if not hasattr(self, "size"):
            msg = "Subclasses of UI must implement the `size` property."
            raise NotImplementedError(msg)

        new_center = np.array(coords)
        size = np.array(self.size)
        new_lower_left_corner = new_center - size / 2.0
        self.position = new_lower_left_corner

    def set_visibility(self, visibility):
        """Set visibility of this UI component."""
        for actor in self.actors:
            actor.SetVisibility(visibility)

    def handle_events(self, actor):
        self.add_callback(
            actor, "LeftButtonPressEvent", self.left_button_click_callback
        )
        self.add_callback(
            actor, "LeftButtonReleaseEvent", self.left_button_release_callback
        )
        self.add_callback(
            actor, "RightButtonPressEvent", self.right_button_click_callback
        )
        self.add_callback(
            actor, "RightButtonReleaseEvent", self.right_button_release_callback
        )
        self.add_callback(
            actor, "MiddleButtonPressEvent", self.middle_button_click_callback
        )
        self.add_callback(
            actor, "MiddleButtonReleaseEvent", self.middle_button_release_callback
        )
        self.add_callback(actor, "MouseMoveEvent", self.mouse_move_callback)
        self.add_callback(actor, "KeyPressEvent", self.key_press_callback)

    @staticmethod
    def left_button_click_callback(i_ren, obj, self):
        self.left_button_state = "pressing"
        self.on_left_mouse_button_pressed(i_ren, obj, self)
        i_ren.event.abort()

    @staticmethod
    def left_button_release_callback(i_ren, obj, self):
        if self.left_button_state == "pressing":
            self.on_left_mouse_button_clicked(i_ren, obj, self)
        self.left_button_state = "released"
        self.on_left_mouse_button_released(i_ren, obj, self)

    @staticmethod
    def right_button_click_callback(i_ren, obj, self):
        self.right_button_state = "pressing"
        self.on_right_mouse_button_pressed(i_ren, obj, self)
        i_ren.event.abort()

    @staticmethod
    def right_button_release_callback(i_ren, obj, self):
        if self.right_button_state == "pressing":
            self.on_right_mouse_button_clicked(i_ren, obj, self)
        self.right_button_state = "released"
        self.on_right_mouse_button_released(i_ren, obj, self)

    @staticmethod
    def middle_button_click_callback(i_ren, obj, self):
        self.middle_button_state = "pressing"
        self.on_middle_mouse_button_pressed(i_ren, obj, self)
        i_ren.event.abort()

    @staticmethod
    def middle_button_release_callback(i_ren, obj, self):
        if self.middle_button_state == "pressing":
            self.on_middle_mouse_button_clicked(i_ren, obj, self)
        self.middle_button_state = "released"
        self.on_middle_mouse_button_released(i_ren, obj, self)

    @staticmethod
    def mouse_move_callback(i_ren, obj, self):
        left_pressing_or_dragging = (
            self.left_button_state == "pressing" or self.left_button_state == "dragging"
        )

        right_pressing_or_dragging = (
            self.right_button_state == "pressing"
            or self.right_button_state == "dragging"
        )

        middle_pressing_or_dragging = (
            self.middle_button_state == "pressing"
            or self.middle_button_state == "dragging"
        )

        if left_pressing_or_dragging:
            self.left_button_state = "dragging"
            self.on_left_mouse_button_dragged(i_ren, obj, self)
        elif right_pressing_or_dragging:
            self.right_button_state = "dragging"
            self.on_right_mouse_button_dragged(i_ren, obj, self)
        elif middle_pressing_or_dragging:
            self.middle_button_state = "dragging"
            self.on_middle_mouse_button_dragged(i_ren, obj, self)

    @staticmethod
    def key_press_callback(i_ren, obj, self):
        self.on_key_press(i_ren, obj, self)


class Rectangle2D(UI):
    """A 2D rectangle sub-classed from UI."""

    @warn_on_args_to_kwargs()
    def __init__(self, *, size=(0, 0), position=(0, 0), color=(1, 1, 1), opacity=1.0):
        """Initialize a rectangle.

        Parameters
        ----------
        size : (int, int)
            The size of the rectangle (width, height) in pixels.
        position : (float, float)
            Coordinates (x, y) of the lower-left corner of the rectangle.
        color : (float, float, float)
            Must take values in [0, 1].
        opacity : float
            Must take values in [0, 1].

        """
        super(Rectangle2D, self).__init__(position=position)
        self.color = color
        self.opacity = opacity
        self.resize(size)

    def _setup(self):
        """Set up this UI component.

        Creating the polygon actor used internally.
        """
        # Setup four points
        size = (1, 1)
        self._points = Points()
        self._points.InsertNextPoint(0, 0, 0)
        self._points.InsertNextPoint(size[0], 0, 0)
        self._points.InsertNextPoint(size[0], size[1], 0)
        self._points.InsertNextPoint(0, size[1], 0)

        # Create the polygon
        polygon = Polygon()
        polygon.GetPointIds().SetNumberOfIds(4)  # make a quad
        polygon.GetPointIds().SetId(0, 0)
        polygon.GetPointIds().SetId(1, 1)
        polygon.GetPointIds().SetId(2, 2)
        polygon.GetPointIds().SetId(3, 3)

        # Add the polygon to a list of polygons
        polygons = CellArray()
        polygons.InsertNextCell(polygon)

        # Create a PolyData
        self._polygonPolyData = PolyData()
        self._polygonPolyData.SetPoints(self._points)
        self._polygonPolyData.SetPolys(polygons)

        # Create a mapper and actor
        mapper = PolyDataMapper2D()
        mapper = set_input(mapper, self._polygonPolyData)

        self.actor = Actor2D()
        self.actor.SetMapper(mapper)

        # Add default events listener to the VTK actor.
        self.handle_events(self.actor)

    def _get_actors(self):
        """Get the actors composing this UI component."""
        return [self.actor]

    def _add_to_scene(self, scene):
        """Add all subcomponents or VTK props that compose this UI component.

        Parameters
        ----------
        scene : scene

        """
        scene.add(self.actor)

    def _get_size(self):
        # Get 2D coordinates of two opposed corners of the rectangle.
        lower_left_corner = np.array(self._points.GetPoint(0)[:2])
        upper_right_corner = np.array(self._points.GetPoint(2)[:2])
        size = abs(upper_right_corner - lower_left_corner)
        return size

    @property
    def width(self):
        return self._points.GetPoint(2)[0]

    @width.setter
    def width(self, width):
        self.resize((width, self.height))

    @property
    def height(self):
        return self._points.GetPoint(2)[1]

    @height.setter
    def height(self, height):
        self.resize((self.width, height))

    def resize(self, size):
        """Set the button size.

        Parameters
        ----------
        size : (float, float)
            Button size (width, height) in pixels.

        """
        self._points.SetPoint(0, 0, 0, 0.0)
        self._points.SetPoint(1, size[0], 0, 0.0)
        self._points.SetPoint(2, size[0], size[1], 0.0)
        self._points.SetPoint(3, 0, size[1], 0.0)
        self._polygonPolyData.SetPoints(self._points)
        mapper = PolyDataMapper2D()
        mapper = set_input(mapper, self._polygonPolyData)

        self.actor.SetMapper(mapper)

    def _set_position(self, coords):
        """Set the lower-left corner position of this UI component.

        Parameters
        ----------
        coords: (float, float)
            Absolute pixel coordinates (x, y).

        """
        self.actor.SetPosition(*coords)

    @property
    def color(self):
        """Get the rectangle's color."""
        color = self.actor.GetProperty().GetColor()
        return np.asarray(color)

    @color.setter
    def color(self, color):
        """Set the rectangle's color.

        Parameters
        ----------
        color : (float, float, float)
            RGB. Must take values in [0, 1].

        """
        self.actor.GetProperty().SetColor(*color)

    @property
    def opacity(self):
        """Get the rectangle's opacity."""
        return self.actor.GetProperty().GetOpacity()

    @opacity.setter
    def opacity(self, opacity):
        """Set the rectangle's opacity.

        Parameters
        ----------
        opacity : float
            Degree of transparency. Must be between [0, 1].

        """
        self.actor.GetProperty().SetOpacity(opacity)


class Disk2D(UI):
    """A 2D disk UI component."""

    @warn_on_args_to_kwargs()
    def __init__(
        self,
        outer_radius,
        *,
        inner_radius=0,
        center=(0, 0),
        color=(1, 1, 1),
        opacity=1.0,
    ):
        """Initialize a 2D Disk.

        Parameters
        ----------
        outer_radius : int
            Outer radius of the disk.
        inner_radius : int, optional
            Inner radius of the disk. A value > 0, makes a ring.
        center : (float, float), optional
            Coordinates (x, y) of the center of the disk.
        color : (float, float, float), optional
            Must take values in [0, 1].
        opacity : float, optional
            Must take values in [0, 1].

        """
        super(Disk2D, self).__init__()
        self.outer_radius = outer_radius
        self.inner_radius = inner_radius
        self.color = color
        self.opacity = opacity
        self.center = center

    def _setup(self):
        """Setup this UI component.

        Creating the disk actor used internally.

        """
        # Setting up disk actor.
        self._disk = DiskSource()
        self._disk.SetRadialResolution(10)
        self._disk.SetCircumferentialResolution(50)
        self._disk.Update()

        # Mapper
        mapper = PolyDataMapper2D()
        mapper = set_input(mapper, self._disk.GetOutputPort())

        # Actor
        self.actor = Actor2D()
        self.actor.SetMapper(mapper)

        # Add default events listener to the VTK actor.
        self.handle_events(self.actor)

    def _get_actors(self):
        """Get the actors composing this UI component."""
        return [self.actor]

    def _add_to_scene(self, scene):
        """Add all subcomponents or VTK props that compose this UI component.

        Parameters
        ----------
        scene : scene

        """
        scene.add(self.actor)

    def _get_size(self):
        diameter = 2 * self.outer_radius
        size = (diameter, diameter)
        return size

    def _set_position(self, coords):
        """Set the lower-left corner position of this UI bounding box.

        Parameters
        ----------
        coords: (float, float)
            Absolute pixel coordinates (x, y).

        """
        # Disk actor are positioned with respect to their center.
        self.actor.SetPosition(*coords + self.outer_radius)

    @property
    def color(self):
        """Get the color of this UI component."""
        color = self.actor.GetProperty().GetColor()
        return np.asarray(color)

    @color.setter
    def color(self, color):
        """Set the color of this UI component.

        Parameters
        ----------
        color : (float, float, float)
            RGB. Must take values in [0, 1].

        """
        self.actor.GetProperty().SetColor(*color)

    @property
    def opacity(self):
        """Get the opacity of this UI component."""
        return self.actor.GetProperty().GetOpacity()

    @opacity.setter
    def opacity(self, opacity):
        """Set the opacity of this UI component.

        Parameters
        ----------
        opacity : float
            Degree of transparency. Must be between [0, 1].

        """
        self.actor.GetProperty().SetOpacity(opacity)

    @property
    def inner_radius(self):
        return self._disk.GetInnerRadius()

    @inner_radius.setter
    def inner_radius(self, radius):
        self._disk.SetInnerRadius(radius)
        self._disk.Update()

    @property
    def outer_radius(self):
        return self._disk.GetOuterRadius()

    @outer_radius.setter
    def outer_radius(self, radius):
        self._disk.SetOuterRadius(radius)
        self._disk.Update()


class TextBlock2D(UI):
    """Wrap over the default vtkTextActor and helps setting the text.

    Contains member functions for text formatting.

    Attributes
    ----------
    actor : :class:`vtkTextActor`
        The text actor.
    message : str
        The initial text while building the actor.
    position : (float, float)
        (x, y) in pixels.
    color : (float, float, float)
        RGB: Values must be between 0-1.
    bg_color : (float, float, float)
        RGB: Values must be between 0-1.
    font_size : int
        Size of the text font.
    font_family : str
        Currently only supports Arial.
    justification : str
        left, right or center.
    vertical_justification : str
        bottom, middle or top.
    bold : bool
        Makes text bold.
    italic : bool
        Makes text italicised.
    shadow : bool
        Adds text shadow.
    size : (int, int)
        Size (width, height) in pixels of the text bounding box.
    auto_font_scale : bool
        Automatically scale font according to the text bounding box.
    dynamic_bbox : bool
        Automatically resize the bounding box according to the content.

    """

    @warn_on_args_to_kwargs()
    def __init__(
        self,
        *,
        text="Text Block",
        font_size=18,
        font_family="Arial",
        justification="left",
        vertical_justification="bottom",
        bold=False,
        italic=False,
        shadow=False,
        size=None,
        color=(1, 1, 1),
        bg_color=None,
        position=(0, 0),
        auto_font_scale=False,
        dynamic_bbox=False,
    ):
        """Init class instance.

        Parameters
        ----------
        text : str
            The initial text while building the actor.
        position : (float, float)
            (x, y) in pixels.
        color : (float, float, float)
            RGB: Values must be between 0-1.
        bg_color : (float, float, float)
            RGB: Values must be between 0-1.
        font_size : int
            Size of the text font.
        font_family : str
            Currently only supports Arial.
        justification : str
            left, right or center.
        vertical_justification : str
            bottom, middle or top.
        bold : bool
            Makes text bold.
        italic : bool
            Makes text italicised.
        shadow : bool
            Adds text shadow.
        size : (int, int)
            Size (width, height) in pixels of the text bounding box.
        auto_font_scale : bool, optional
            Automatically scale font according to the text bounding box.
        dynamic_bbox : bool, optional
            Automatically resize the bounding box according to the content.

        """
        self.boundingbox = [0, 0, 0, 0]
        super(TextBlock2D, self).__init__(position=position)
        self.scene = None
        self.have_bg = bool(bg_color)
        self.color = color
        self.background_color = bg_color
        self.font_family = font_family
        self._justification = justification
        self.bold = bold
        self.italic = italic
        self.shadow = shadow
        self._vertical_justification = vertical_justification
        self._dynamic_bbox = dynamic_bbox
        self.auto_font_scale = auto_font_scale
        self.message = text
        self.font_size = font_size
        if size is not None:
            self.resize(size)
        elif not self.dynamic_bbox:
            # raise ValueError("TextBlock size is required as it is not dynamic.")
            self.resize((0, 0))

    def _setup(self):
        self.actor = TextActor()
        self.actor.GetPosition2Coordinate().SetCoordinateSystemToViewport()
        self.background = Rectangle2D()
        self.handle_events(self.actor)

    def resize(self, size):
        """Resize TextBlock2D.

        Parameters
        ----------
        size : (int, int)
            Text bounding box size(width, height) in pixels.

        """
        self.update_bounding_box(size=size)

    def _get_actors(self):
        """Get the actors composing this UI component."""
        return [self.actor] + self.background.actors

    def _add_to_scene(self, scene):
        """Add all subcomponents or VTK props that compose this UI component.

        Parameters
        ----------
        scene : scene

        """
        scene.add(self.background, self.actor)

    @property
    def message(self):
        """Get message from the text.

        Returns
        -------
        str
            The current text message.

        """
        return self.actor.GetInput()

    @message.setter
    def message(self, text):
        """Set the text message.

        Parameters
        ----------
        text : str
            The message to be set.

        """
        self.actor.SetInput(text)
        if self.dynamic_bbox:
            self.update_bounding_box()

    @property
    def font_size(self):
        """Get text font size.

        Returns
        -------
        int
            Text font size.

        """
        return self.actor.GetTextProperty().GetFontSize()

    @font_size.setter
    def font_size(self, size):
        """Set font size.

        Parameters
        ----------
        size : int
            Text font size.

        """
        if not self.auto_font_scale:
            self.actor.SetTextScaleModeToNone()
            self.actor.GetTextProperty().SetFontSize(size)

        if self.dynamic_bbox:
            self.update_bounding_box()

    @property
    def font_family(self):
        """Get font family.

        Returns
        -------
        str
            Text font family.

        """
        return self.actor.GetTextProperty().GetFontFamilyAsString()

    @font_family.setter
    def font_family(self, family="Arial"):
        """Set font family.

        Currently Arial and Courier are supported.

        Parameters
        ----------
        family : str
            The font family.

        """
        if family == "Arial":
            self.actor.GetTextProperty().SetFontFamilyToArial()
        elif family == "Courier":
            self.actor.GetTextProperty().SetFontFamilyToCourier()
        else:
            raise ValueError("Font not supported yet: {}.".format(family))

    @property
    def justification(self):
        """Get text justification.

        Returns
        -------
        str
            Text justification.

        """
        return self._justification

    @justification.setter
    def justification(self, justification):
        """Justify text.

        Parameters
        ----------
        justification : str
            Possible values are left, right, center.

        """
        self._justification = justification
        self.update_alignment()

    @property
    def vertical_justification(self):
        """Get text vertical justification.

        Returns
        -------
        str
            Text vertical justification.

        """
        return self._vertical_justification

    @vertical_justification.setter
    def vertical_justification(self, vertical_justification):
        """Justify text vertically.

        Parameters
        ----------
        vertical_justification : str
            Possible values are bottom, middle, top.

        """
        self._vertical_justification = vertical_justification
        self.update_alignment()

    @property
    def bold(self):
        """Return whether the text is bold.

        Returns
        -------
        bool
            Text is bold if True.

        """
        return self.actor.GetTextProperty().GetBold()

    @bold.setter
    def bold(self, flag):
        """Bold/un-bold text.

        Parameters
        ----------
        flag : bool
            Sets text bold if True.

        """
        self.actor.GetTextProperty().SetBold(flag)

    @property
    def italic(self):
        """Return whether the text is italicised.

        Returns
        -------
        bool
            Text is italicised if True.

        """
        return self.actor.GetTextProperty().GetItalic()

    @italic.setter
    def italic(self, flag):
        """Italicise/un-italicise text.

        Parameters
        ----------
        flag : bool
            Italicises text if True.

        """
        self.actor.GetTextProperty().SetItalic(flag)

    @property
    def shadow(self):
        """Return whether the text has shadow.

        Returns
        -------
        bool
            Text is shadowed if True.

        """
        return self.actor.GetTextProperty().GetShadow()

    @shadow.setter
    def shadow(self, flag):
        """Add/remove text shadow.

        Parameters
        ----------
        flag : bool
            Shadows text if True.

        """
        self.actor.GetTextProperty().SetShadow(flag)

    @property
    def color(self):
        """Get text color.

        Returns
        -------
        (float, float, float)
            Returns text color in RGB.

        """
        return self.actor.GetTextProperty().GetColor()

    @color.setter
    def color(self, color=(1, 0, 0)):
        """Set text color.

        Parameters
        ----------
        color : (float, float, float)
            RGB: Values must be between 0-1.

        """
        self.actor.GetTextProperty().SetColor(*color)

    @property
    def background_color(self):
        """Get background color.

        Returns
        -------
        (float, float, float) or None
            If None, there no background color.
            Otherwise, background color in RGB.

        """
        if not self.have_bg:
            return None

        return self.background.color

    @background_color.setter
    def background_color(self, color):
        """Set text color.

        Parameters
        ----------
        color : (float, float, float) or None
            If None, remove background.
            Otherwise, RGB values (must be between 0-1).

        """
        if color is None:
            # Remove background.
            self.have_bg = False
            self.background.set_visibility(False)

        else:
            self.have_bg = True
            self.background.set_visibility(True)
            self.background.color = color

    @property
    def auto_font_scale(self):
        """Return whether text font is automatically scaled.

        Returns
        -------
        bool
            Text is auto_font_scaled if True.

        """
        return self._auto_font_scale

    @auto_font_scale.setter
    def auto_font_scale(self, flag):
        """Add/remove text auto_font_scale.

        Parameters
        ----------
        flag : bool
            Automatically scales the text font if True.

        """
        self._auto_font_scale = flag
        if flag:
            self.actor.SetTextScaleModeToProp()
            self._justification = "left"
            self.update_bounding_box(size=self.size)
        else:
            self.actor.SetTextScaleModeToNone()

    @property
    def dynamic_bbox(self):
        """Automatically resize the bounding box according to the content.

        Returns
        -------
        bool
            Bounding box is dynamic if True.

        """
        return self._dynamic_bbox

    @dynamic_bbox.setter
    def dynamic_bbox(self, flag):
        """Add/remove dynamic_bbox.

        Parameters
        ----------
        flag : bool
            The text bounding box is dynamic if True.

        """
        self._dynamic_bbox = flag
        if flag:
            self.update_bounding_box()

    def update_alignment(self):
        """Update Text Alignment."""
        text_property = self.actor.GetTextProperty()
        updated_text_position = [0, 0]

        if self.justification.lower() == "left":
            text_property.SetJustificationToLeft()
            updated_text_position[0] = self.boundingbox[0]
        elif self.justification.lower() == "center":
            text_property.SetJustificationToCentered()
            updated_text_position[0] = (
                self.boundingbox[0] + (self.boundingbox[2] - self.boundingbox[0]) // 2
            )
        elif self.justification.lower() == "right":
            text_property.SetJustificationToRight()
            updated_text_position[0] = self.boundingbox[2]
        else:
            msg = "Text can only be justified left, right and center."
            raise ValueError(msg)

        if self.vertical_justification.lower() == "bottom":
            text_property.SetVerticalJustificationToBottom()
            updated_text_position[1] = self.boundingbox[1]
        elif self.vertical_justification.lower() == "middle":
            text_property.SetVerticalJustificationToCentered()
            updated_text_position[1] = (
                self.boundingbox[1] + (self.boundingbox[3] - self.boundingbox[1]) // 2
            )
        elif self.vertical_justification.lower() == "top":
            text_property.SetVerticalJustificationToTop()
            updated_text_position[1] = self.boundingbox[3]
        else:
            msg = "Vertical justification must be: bottom, middle or top."
            raise ValueError(msg)

        self.actor.SetPosition(updated_text_position)

    def cal_size_from_message(self):
        """Calculate size of background according to the message it contains."""
        lines = self.message.split("\n")
        max_length = max(len(line) for line in lines)
        return [max_length * self.font_size, len(lines) * self.font_size]

    @warn_on_args_to_kwargs()
    def update_bounding_box(self, *, size=None):
        """Update Text Bounding Box.

        Parameters
        ----------
        size : (int, int) or None
            If None, calculates bounding box.
            Otherwise, uses the given size.

        """
        if size is None:
            size = self.cal_size_from_message()

        self.boundingbox = [
            self.position[0],
            self.position[1],
            self.position[0] + size[0],
            self.position[1] + size[1],
        ]
        self.background.resize(size)

        if self.auto_font_scale:
            self.actor.SetPosition2(
                self.boundingbox[2] - self.boundingbox[0],
                self.boundingbox[3] - self.boundingbox[1],
            )
        else:
            self.update_alignment()

    def _set_position(self, position):
        """Set text actor position.

        Parameters
        ----------
        position : (float, float)
            The new position. (x, y) in pixels.

        """
        self.actor.SetPosition(*position)
        self.background.position = position

    def _get_size(self):
        bb_size = (
            self.boundingbox[2] - self.boundingbox[0],
            self.boundingbox[3] - self.boundingbox[1],
        )
        if self.dynamic_bbox or self.auto_font_scale or sum(bb_size):
            return bb_size
        return self.cal_size_from_message()


class Button2D(UI):
    """A 2D overlay button and is of type vtkTexturedActor2D.

    Currently supports::

        - Multiple icons.
        - Switching between icons.

    """

    @warn_on_args_to_kwargs()
    def __init__(self, icon_fnames, *, position=(0, 0), size=(30, 30)):
        """Init class instance.

        Parameters
        ----------
        icon_fnames : List(string, string)
            ((iconname, filename), (iconname, filename), ....)
        position : (float, float), optional
            Absolute coordinates (x, y) of the lower-left corner of the button.
        size : (int, int), optional
            Width and height in pixels of the button.

        """
        super(Button2D, self).__init__(position=position)

        self.icon_extents = {}
        self.icons = self._build_icons(icon_fnames)
        self.icon_names = [icon[0] for icon in self.icons]
        self.current_icon_id = 0
        self.current_icon_name = self.icon_names[self.current_icon_id]
        self.set_icon(self.icons[self.current_icon_id][1])
        self.resize(size)

    def _get_size(self):
        lower_left_corner = self.texture_points.GetPoint(0)
        upper_right_corner = self.texture_points.GetPoint(2)
        size = np.array(upper_right_corner) - np.array(lower_left_corner)
        return abs(size[:2])

    def _build_icons(self, icon_fnames):
        """Convert file names to ImageData.

        A pre-processing step to prevent re-read of file names during every
        state change.

        Parameters
        ----------
        icon_fnames : List(string, string)
            ((iconname, filename), (iconname, filename), ....)

        Returns
        -------
        icons : List
            A list of corresponding ImageData.

        """
        icons = []
        for icon_name, icon_fname in icon_fnames:
            icons.append((icon_name, load_image(icon_fname, as_vtktype=True)))

        return icons

    def _setup(self):
        """Set up this UI component.

        Creating the button actor used internally.

        """
        # This is highly inspired by
        # https://github.com/Kitware/VTK/blob/c3ec2495b183e3327820e927af7f8f90d34c3474/Interaction/Widgets/vtkBalloonRepresentation.cxx#L47

        self.texture_polydata = PolyData()
        self.texture_points = Points()
        self.texture_points.SetNumberOfPoints(4)

        polys = CellArray()
        polys.InsertNextCell(4)
        polys.InsertCellPoint(0)
        polys.InsertCellPoint(1)
        polys.InsertCellPoint(2)
        polys.InsertCellPoint(3)
        self.texture_polydata.SetPolys(polys)

        tc = FloatArray()
        tc.SetNumberOfComponents(2)
        tc.SetNumberOfTuples(4)
        tc.InsertComponent(0, 0, 0.0)
        tc.InsertComponent(0, 1, 0.0)
        tc.InsertComponent(1, 0, 1.0)
        tc.InsertComponent(1, 1, 0.0)
        tc.InsertComponent(2, 0, 1.0)
        tc.InsertComponent(2, 1, 1.0)
        tc.InsertComponent(3, 0, 0.0)
        tc.InsertComponent(3, 1, 1.0)
        self.texture_polydata.GetPointData().SetTCoords(tc)

        texture_mapper = PolyDataMapper2D()
        texture_mapper = set_input(texture_mapper, self.texture_polydata)

        button = TexturedActor2D()
        button.SetMapper(texture_mapper)

        self.texture = Texture()
        button.SetTexture(self.texture)

        button_property = Property2D()
        button_property.SetOpacity(1.0)
        button.SetProperty(button_property)
        self.actor = button

        # Add default events listener to the VTK actor.
        self.handle_events(self.actor)

    def _get_actors(self):
        """Get the actors composing this UI component."""
        return [self.actor]

    def _add_to_scene(self, scene):
        """Add all subcomponents or VTK props that compose this UI component.

        Parameters
        ----------
        scene : scene

        """
        scene.add(self.actor)

    def resize(self, size):
        """Resize the button.

        Parameters
        ----------
        size : (float, float)
            Button size (width, height) in pixels.

        """
        # Update actor.
        self.texture_points.SetPoint(0, 0, 0, 0.0)
        self.texture_points.SetPoint(1, size[0], 0, 0.0)
        self.texture_points.SetPoint(2, size[0], size[1], 0.0)
        self.texture_points.SetPoint(3, 0, size[1], 0.0)
        self.texture_polydata.SetPoints(self.texture_points)

    def _set_position(self, coords):
        """Set the lower-left corner position of this UI component.

        Parameters
        ----------
        coords: (float, float)
            Absolute pixel coordinates (x, y).

        """
        self.actor.SetPosition(*coords)

    @property
    def color(self):
        """Get the button's color."""
        color = self.actor.GetProperty().GetColor()
        return np.asarray(color)

    @color.setter
    def color(self, color):
        """Set the button's color.

        Parameters
        ----------
        color : (float, float, float)
            RGB. Must take values in [0, 1].

        """
        self.actor.GetProperty().SetColor(*color)

    def scale(self, factor):
        """Scale the button.

        Parameters
        ----------
        factor : (float, float)
            Scaling factor (width, height) in pixels.

        """
        self.resize(self.size * factor)

    def set_icon_by_name(self, icon_name):
        """Set the button icon using its name.

        Parameters
        ----------
        icon_name : str

        """
        icon_id = self.icon_names.index(icon_name)
        self.set_icon(self.icons[icon_id][1])

    def set_icon(self, icon):
        """Modify the icon used by the vtkTexturedActor2D.

        Parameters
        ----------
        icon : imageData

        """
        self.texture = set_input(self.texture, icon)

    def next_icon_id(self):
        """Set the next icon ID while cycling through icons."""
        self.current_icon_id += 1
        if self.current_icon_id == len(self.icons):
            self.current_icon_id = 0
        self.current_icon_name = self.icon_names[self.current_icon_id]

    def next_icon(self):
        """Increment the state of the Button.

        Also changes the icon.
        """
        self.next_icon_id()
        self.set_icon(self.icons[self.current_icon_id][1])
