from __future__ import annotations
"""Mixins that split visual responsibilities out of core visual classes."""

import os

import numpy as np
from typing_extensions import Self

import vedo
import vedo.vtkclasses as vtki
from vedo import colors, utils


__docformat__ = "google"


class PointsVisualEffectsMixin:
    def add_trail(self, offset=(0, 0, 0), n=50, c=None, alpha=1.0, lw=2) -> Self:
        """
        Add a trailing line to mesh.
        This new mesh is accessible through `mesh.trail`.

        Arguments:
            offset : (float)
                set an offset vector from the object center.
            n : (int)
                number of segments
            lw : (float)
                line width of the trail

        Examples:
            - [trail.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/trail.py)

                ![](https://vedo.embl.es/images/simulations/trail.gif)

            - [airplane1.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/airplane1.py)
            - [airplane2.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/airplane2.py)
        """
        if self.trail is None:
            pos = self.pos()
            self.trail_offset = np.asarray(offset)
            self.trail_points = [pos] * n

            if c is None:
                col = self.properties.GetColor()
            else:
                col = colors.get_color(c)

            tline = vedo.shapes.Line(pos, pos, res=n, c=col, alpha=alpha, lw=lw)
            self.trail = tline  # holds the Line
            self.trail.initilized = False # so the first update will be a reset
        return self

    def update_trail(self) -> Self:
        """
        Update the trailing line of a moving object.
        """
        currentpos = self.pos()
        if not self.trail.initilized:
            self.trail_points = [currentpos] * self.trail.npoints
            self.trail.initilized = True
            return self
        self.trail_points.append(currentpos)  # cycle
        self.trail_points.pop(0)

        data = np.array(self.trail_points) + self.trail_offset
        tpoly = self.trail.dataset
        tpoly.GetPoints().SetData(utils.numpy2vtk(data, dtype=np.float32))
        return self

    def _compute_shadow(self, plane, point, direction):
        shad = self.clone()
        shad.name = "Shadow"

        tarr = shad.dataset.GetPointData().GetTCoords()
        if tarr:  # remove any texture coords
            tname = tarr.GetName()
            shad.pointdata.remove(tname)
            shad.dataset.GetPointData().SetTCoords(None)
            shad.actor.SetTexture(None)

        pts = shad.vertices
        if plane == "x":
            # shad = shad.project_on_plane('x')
            # instead do it manually so in case of alpha<1
            # we dont see glitches due to coplanar points
            # we leave a small tolerance of 0.1% in thickness
            x0, x1 = self.xbounds()
            pts[:, 0] = (pts[:, 0] - (x0 + x1) / 2) / 1000 + self.actor.GetOrigin()[0]
            shad.vertices = pts
            shad.x(point)
        elif plane == "y":
            x0, x1 = self.ybounds()
            pts[:, 1] = (pts[:, 1] - (x0 + x1) / 2) / 1000 + self.actor.GetOrigin()[1]
            shad.vertices = pts
            shad.y(point)
        elif plane == "z":
            x0, x1 = self.zbounds()
            pts[:, 2] = (pts[:, 2] - (x0 + x1) / 2) / 1000 + self.actor.GetOrigin()[2]
            shad.vertices = pts
            shad.z(point)
        else:
            shad = shad.project_on_plane(plane, point, direction)
        return shad

    def add_shadow(self, plane, point, direction=None, c=(0.6, 0.6, 0.6), alpha=1, culling=0) -> Self:
        """
        Generate a shadow out of an `Mesh` on one of the three Cartesian planes.
        The output is a new `Mesh` representing the shadow.
        This new mesh is accessible through `mesh.shadow`.
        By default the shadow mesh is placed on the bottom wall of the bounding box.

        See also `pointcloud.project_on_plane()`.

        Arguments:
            plane : (str, Plane)
                if plane is `str`, plane can be one of `['x', 'y', 'z']`,
                represents x-plane, y-plane and z-plane, respectively.
                Otherwise, plane should be an instance of `vedo.shapes.Plane`
            point : (float, array)
                if plane is `str`, point should be a float represents the intercept.
                Otherwise, point is the camera point of perspective projection
            direction : (list)
                direction of oblique projection
            culling : (int)
                choose between front [1] or backface [-1] culling or None.

        Examples:
            - [shadow1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/shadow1.py)
            - [airplane1.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/airplane1.py)
            - [airplane2.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/airplane2.py)

            ![](https://vedo.embl.es/images/simulations/57341963-b8910900-713c-11e9-898a-84b6d3712bce.gif)
        """
        shad = self._compute_shadow(plane, point, direction)
        shad.c(c).alpha(alpha)

        try:
            # Points dont have these methods
            shad.flat()
            if culling in (1, True):
                shad.frontface_culling()
            elif culling == -1:
                shad.backface_culling()
        except AttributeError:
            pass

        shad.properties.LightingOff()
        shad.actor.SetPickable(False)
        shad.actor.SetUseBounds(True)

        if shad not in self.shadows:
            self.shadows.append(shad)
            shad.info = dict(plane=plane, point=point, direction=direction)
            # shad.metadata["plane"] = plane
            # shad.metadata["point"] = point
            # print("AAAA", direction, plane, point)
            # if direction is None:
            #     direction = [0,0,0]
            # shad.metadata["direction"] = direction
        return self

    def update_shadows(self) -> Self:
        """Update the shadows of a moving object."""
        for sha in self.shadows:
            plane = sha.info["plane"]
            point = sha.info["point"]
            direction = sha.info["direction"]
            # print("update_shadows direction", direction,plane,point )
            # plane = sha.metadata["plane"]
            # point = sha.metadata["point"]
            # direction = sha.metadata["direction"]
            # if direction[0]==0 and direction[1]==0 and direction[2]==0:
            #     direction = None
            # print("BBBB", sha.metadata["direction"],
            #       sha.metadata["plane"], sha.metadata["point"])
            new_sha = self._compute_shadow(plane, point, direction)
            sha._update(new_sha.dataset)
        if self.trail:
            self.trail.update_shadows()
        return self


class PointsVisualAnnotationsMixin:
    def labels(
        self,
        content=None,
        on="points",
        scale=None,
        xrot=0.0,
        yrot=0.0,
        zrot=0.0,
        ratio=1,
        precision=None,
        italic=False,
        font="",
        justify="",
        c="black",
        alpha=1.0,
    ) -> vedo.Mesh | None:
        """
        Generate value or ID labels for mesh cells or points.
        For large nr. of labels use `font="VTK"` which is much faster.

        See also:
            `labels2d()`, `flagpole()`, `caption()` and `legend()`.

        Arguments:
            content : (list,int,str)
                either 'id', 'cellid', array name or array number.
                A array can also be passed (must match the nr. of points or cells).
            on : (str)
                generate labels for "cells" instead of "points"
            scale : (float)
                absolute size of labels, if left as None it is automatic
            zrot : (float)
                local rotation angle of label in degrees
            ratio : (int)
                skipping ratio, to reduce nr of labels for large meshes
            precision : (int)
                numeric precision of labels

        ```python
        from vedo import *
        s = Sphere(res=10).linewidth(1).c("orange").compute_normals()
        point_ids = s.labels('id', on="points").c('green')
        cell_ids  = s.labels('id', on="cells" ).c('black')
        show(s, point_ids, cell_ids)
        ```
        ![](https://vedo.embl.es/images/feats/labels.png)

        Examples:
            - [boundaries.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/boundaries.py)

                ![](https://vedo.embl.es/images/basic/boundaries.png)
        """

        cells = False
        if "cell" in on or "face" in on:
            cells = True
            justify = "centered" if justify == "" else justify

        if isinstance(content, str):
            if content in ("pointid", "pointsid"):
                cells = False
                content = "id"
                justify = "bottom-left" if justify == "" else justify
            if content in ("cellid", "cellsid"):
                cells = True
                content = "id"
                justify = "centered" if justify == "" else justify

        try:
            if cells:
                ns = np.sqrt(self.ncells)
                elems = self.cell_centers().points
                norms = self.cell_normals
                justify = "centered" if justify == "" else justify
            else:
                ns = np.sqrt(self.npoints)
                elems = self.vertices
                norms = self.vertex_normals
        except AttributeError:
            norms = []

        if not justify:
            justify = "bottom-left"

        hasnorms = False
        if len(norms) > 0:
            hasnorms = True

        if scale is None:
            if not ns:
                ns = 100
            scale = self.diagonal_size() / ns / 10

        arr = None
        mode = 0
        if content is None:
            mode = 0
            if cells:
                if self.dataset.GetCellData().GetScalars():
                    name = self.dataset.GetCellData().GetScalars().GetName()
                    arr = self.celldata[name]
            else:
                if self.dataset.GetPointData().GetScalars():
                    name = self.dataset.GetPointData().GetScalars().GetName()
                    arr = self.pointdata[name]
        elif isinstance(content, (str, int)):
            if content == "id":
                mode = 1
            elif cells:
                mode = 0
                arr = self.celldata[content]
            else:
                mode = 0
                arr = self.pointdata[content]
        elif utils.is_sequence(content):
            mode = 0
            arr = content

        if arr is None and mode == 0:
            vedo.logger.error("in labels(), array not found in point or cell data")
            return None

        ratio = int(ratio+0.5)
        tapp = vtki.new("AppendPolyData")
        has_inputs = False

        for i, e in enumerate(elems):
            if i % ratio:
                continue

            if mode == 1:
                txt_lab = str(i)
            else:
                if precision:
                    txt_lab = utils.precision(arr[i], precision)
                else:
                    txt_lab = str(arr[i])

            if not txt_lab:
                continue

            if font == "VTK":
                tx = vtki.new("VectorText")
                tx.SetText(txt_lab)
                tx.Update()
                tx_poly = tx.GetOutput()
            else:
                tx_poly = vedo.shapes.Text3D(txt_lab, font=font, justify=justify).dataset

            if tx_poly.GetNumberOfPoints() == 0:
                continue  ######################

            T = vtki.vtkTransform()
            T.PostMultiply()
            if italic:
                T.Concatenate([1, 0.2, 0, 0,
                               0, 1  , 0, 0,
                               0, 0  , 1, 0,
                               0, 0  , 0, 1])
            if hasnorms:
                ni = norms[i]
                if cells and font=="VTK":  # center-justify
                    bb = tx_poly.GetBounds()
                    dx, dy = (bb[1] - bb[0]) / 2, (bb[3] - bb[2]) / 2
                    T.Translate(-dx, -dy, 0)
                if xrot: T.RotateX(xrot)
                if yrot: T.RotateY(yrot)
                if zrot: T.RotateZ(zrot)
                crossvec = np.cross([0, 0, 1], ni)
                angle = np.arccos(np.dot([0, 0, 1], ni)) * 57.3
                T.RotateWXYZ(float(angle), crossvec.tolist())
                T.Translate(ni / 100)
            else:
                if xrot: T.RotateX(xrot)
                if yrot: T.RotateY(yrot)
                if zrot: T.RotateZ(zrot)
            T.Scale(scale, scale, scale)
            T.Translate(e)
            tf = vtki.new("TransformPolyDataFilter")
            tf.SetInputData(tx_poly)
            tf.SetTransform(T)
            tf.Update()
            tapp.AddInputData(tf.GetOutput())
            has_inputs = True

        if has_inputs:
            tapp.Update()
            lpoly = tapp.GetOutput()
        else:
            lpoly = vtki.vtkPolyData()
        ids = vedo.Mesh(lpoly, c=c, alpha=alpha)
        ids.properties.LightingOff()
        ids.actor.PickableOff()
        ids.actor.SetUseBounds(False)
        ids.name = "Labels"
        return ids

    def labels2d(
        self,
        content="id",
        on="points",
        scale=1.0,
        precision=4,
        font="Calco",
        justify="bottom-left",
        angle=0.0,
        frame=False,
        c="black",
        bc=None,
        alpha=1.0,
    ) -> vedo.visual.Actor2D | None:
        """
        Generate value or ID bi-dimensional labels for mesh cells or points.

        See also: `labels()`, `flagpole()`, `caption()` and `legend()`.

        Arguments:
            content : (str)
                either 'id', 'cellid', or array name
            on : (str)
                generate labels for "cells" instead of "points" (the default)
            scale : (float)
                size scaling of labels
            precision : (int)
                precision of numeric labels
            angle : (float)
                local rotation angle of label in degrees
            frame : (bool)
                draw a frame around the label
            bc : (str)
                background color of the label

        ```python
        from vedo import Sphere, show
        sph = Sphere(quads=True, res=4).compute_normals().wireframe()
        sph.celldata["zvals"] = sph.cell_centers().coordinates[:,2]
        l2d = sph.labels("zvals", on="cells", precision=2).backcolor('orange9')
        show(sph, l2d, axes=1).close()
        ```
        ![](https://vedo.embl.es/images/feats/labels2d.png)
        """
        cells = False
        if "cell" in on:
            cells = True

        if isinstance(content, str):
            if content in ("id", "pointid", "pointsid"):
                cells = False
                content = "id"
            if content in ("cellid", "cellsid"):
                cells = True
                content = "id"

        if cells:
            if content != "id" and content not in self.celldata.keys():
                vedo.logger.error(f"In labels2d: cell array {content} does not exist.")
                return None
            arr = self.dataset.GetCellData().GetScalars()
            poly = self.cell_centers().dataset
            poly.GetPointData().SetScalars(arr)
        else:
            arr = self.dataset.GetPointData().GetScalars()
            poly = self.dataset
            if content != "id" and content not in self.pointdata.keys():
                vedo.logger.error(f"In labels2d: point array {content} does not exist.")
                return None

        mp = vtki.new("LabeledDataMapper")

        if content == "id":
            mp.SetLabelModeToLabelIds()
        else:
            mp.SetLabelModeToLabelScalars()
            if precision is not None:
                dtype = arr.GetDataType()
                if dtype in (vtki.VTK_FLOAT, vtki.VTK_DOUBLE):
                    mp.SetLabelFormat(f"%-#.{precision}g")

        pr = mp.GetLabelTextProperty()
        c = colors.get_color(c)
        pr.SetColor(c)
        pr.SetOpacity(alpha)
        pr.SetFrame(frame)
        pr.SetFrameColor(c)
        pr.SetItalic(False)
        pr.BoldOff()
        pr.ShadowOff()
        pr.UseTightBoundingBoxOn()
        pr.SetOrientation(angle)
        pr.SetFontFamily(vtki.VTK_FONT_FILE)
        fl = utils.get_font_path(font)
        pr.SetFontFile(fl)
        pr.SetFontSize(int(20 * scale))

        if "cent" in justify or "mid" in justify:
            pr.SetJustificationToCentered()
        elif "rig" in justify:
            pr.SetJustificationToRight()
        elif "left" in justify:
            pr.SetJustificationToLeft()
        # ------
        if "top" in justify:
            pr.SetVerticalJustificationToTop()
        else:
            pr.SetVerticalJustificationToBottom()

        if bc is not None:
            bc = colors.get_color(bc)
            pr.SetBackgroundColor(bc)
            pr.SetBackgroundOpacity(alpha)

        mp.SetInputData(poly)
        a2d = vedo.Actor2D()
        a2d.PickableOff()
        a2d.SetMapper(mp)
        return a2d

    def legend(self, txt) -> Self:
        """Book a legend text."""
        self.info["legend"] = txt
        # self.metadata["legend"] = txt
        return self

    def flagpole(
        self,
        txt=None,
        point=None,
        offset=None,
        s=None,
        font="Calco",
        rounded=True,
        c=None,
        alpha=1.0,
        lw=2,
        italic=0.0,
        padding=0.1,
    ) -> vedo.Mesh | None:
        """
        Generate a flag pole style element to describe an object.
        Returns a `Mesh` object.

        Use flagpole.follow_camera() to make it face the camera in the scene.

        Consider using `settings.use_parallel_projection = True`
        to avoid perspective distortions.

        See also `flagpost()`.

        Arguments:
            txt : (str)
                Text to display. The default is the filename or the object name.
            point : (list)
                position of the flagpole pointer.
            offset : (list)
                text offset wrt the application point.
            s : (float)
                size of the flagpole.
            font : (str)
                font face. Check [available fonts here](https://vedo.embl.es/fonts).
            rounded : (bool)
                draw a rounded or squared box around the text.
            c : (list)
                text and box color.
            alpha : (float)
                opacity of text and box.
            lw : (float)
                line with of box frame.
            italic : (float)
                italicness of text.

        Examples:
            - [intersect2d.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/intersect2d.py)

                ![](https://vedo.embl.es/images/pyplot/intersect2d.png)

            - [goniometer.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/goniometer.py)
            - [flag_labels1.py](https://github.com/marcomusy/vedo/tree/master/examples/other/flag_labels1.py)
            - [flag_labels2.py](https://github.com/marcomusy/vedo/tree/master/examples/other/flag_labels2.py)
        """
        objs = []

        if txt is None:
            if self.filename:
                txt = self.filename.split("/")[-1]
            elif self.name:
                txt = self.name
            else:
                return None

        x0, x1, y0, y1, z0, z1 = self.bounds()
        d = self.diagonal_size()
        if point is None:
            if d:
                point = self.closest_point([(x0 + x1) / 2, (y0 + y1) / 2, z1])
                # point = self.closest_point([x1, y0, z1])
            else:  # it's a Point
                point = self.transform.position

        pt = utils.make3d(point)

        if offset is None:
            offset = [(x1 - x0) / 1.75, (y1 - y0) / 5, 0]
        offset = utils.make3d(offset)

        if s is None:
            s = d / 20

        sph = None
        if d and (z1 - z0) / d > 0.1:
            sph = vedo.shapes.Sphere(pt, r=s * 0.4, res=6)

        if c is None:
            c = np.array(self.color()) / 1.4

        lab = vedo.shapes.Text3D(
            txt, pos=pt + offset, s=s, font=font, italic=italic, justify="center"
        )
        objs.append(lab)

        if d and not sph:
            sph = vedo.shapes.Circle(pt, r=s / 3, res=16)
        objs.append(sph)

        x0, x1, y0, y1, z0, z1 = lab.bounds()
        aline = [(x0,y0,z0), (x1,y0,z0), (x1,y1,z0), (x0,y1,z0)]
        if rounded:
            box = vedo.shapes.KSpline(aline, closed=True)
        else:
            box = vedo.shapes.Line(aline, closed=True)

        cnt = [(x0 + x1) / 2, (y0 + y1) / 2, (z0 + z1) / 2]

        # box.actor.SetOrigin(cnt)
        box.scale([1 + padding, 1 + 2 * padding, 1], origin=cnt)
        objs.append(box)

        x0, x1, y0, y1, z0, z1 = box.bounds()
        if x0 < pt[0] < x1:
            c0 = box.closest_point(pt)
            c1 = [c0[0], c0[1] + (pt[1] - y0) / 4, pt[2]]
        elif (pt[0] - x0) < (x1 - pt[0]):
            c0 = [x0, (y0 + y1) / 2, pt[2]]
            c1 = [x0 + (pt[0] - x0) / 4, (y0 + y1) / 2, pt[2]]
        else:
            c0 = [x1, (y0 + y1) / 2, pt[2]]
            c1 = [x1 + (pt[0] - x1) / 4, (y0 + y1) / 2, pt[2]]

        con = vedo.shapes.Line([c0, c1, pt])
        objs.append(con)

        mobjs = vedo.merge(objs).c(c).alpha(alpha)
        mobjs.name = "FlagPole"
        mobjs.bc("tomato").pickable(False)
        mobjs.properties.LightingOff()
        mobjs.properties.SetLineWidth(lw)
        mobjs.actor.UseBoundsOff()
        mobjs.actor.SetPosition([0,0,0])
        mobjs.actor.SetOrigin(pt)
        return mobjs

        # mobjs = vedo.Assembly(objs)#.c(c).alpha(alpha)
        # mobjs.name = "FlagPole"
        # # mobjs.bc("tomato").pickable(False)
        # # mobjs.properties.LightingOff()
        # # mobjs.properties.SetLineWidth(lw)
        # # mobjs.actor.UseBoundsOff()
        # # mobjs.actor.SetPosition([0,0,0])
        # # mobjs.actor.SetOrigin(pt)
        # # print(pt)
        # return mobjs

    def flagpost(
        self,
        txt=None,
        point=None,
        offset=None,
        s=1.0,
        c="k9",
        bc="k1",
        alpha=1,
        lw=0,
        font="Calco",
        justify="center-left",
        vspacing=1.0,
    ) -> vedo.addons.Flagpost | None:
        """
        Generate a flag post style element to describe an object.

        Arguments:
            txt : (str)
                Text to display. The default is the filename or the object name.
            point : (list)
                position of the flag anchor point. The default is None.
            offset : (list)
                a 3D displacement or offset. The default is None.
            s : (float)
                size of the text to be shown
            c : (list)
                color of text and line
            bc : (list)
                color of the flag background
            alpha : (float)
                opacity of text and box.
            lw : (int)
                line with of box frame. The default is 0.
            font : (str)
                font name. Use a monospace font for better rendering. The default is "Calco".
                Type `vedo -r fonts` for a font demo.
                Check [available fonts here](https://vedo.embl.es/fonts).
            justify : (str)
                internal text justification. The default is "center-left".
            vspacing : (float)
                vertical spacing between lines.

        Examples:
            - [flag_labels2.py](https://github.com/marcomusy/vedo/tree/master/examples/other/flag_labels2.py)

            ![](https://vedo.embl.es/images/other/flag_labels2.png)
        """
        if txt is None:
            if self.filename:
                txt = self.filename.split("/")[-1]
            elif self.name:
                txt = self.name
            else:
                return None

        x0, x1, y0, y1, z0, z1 = self.bounds()
        d = self.diagonal_size()
        if point is None:
            if d:
                point = self.closest_point([(x0 + x1) / 2, (y0 + y1) / 2, z1])
            else:  # it's a Point
                point = self.transform.position

        point = utils.make3d(point)

        if offset is None:
            offset = [0, 0, (z1 - z0) / 2]
        offset = utils.make3d(offset)

        fpost = vedo.addons.Flagpost(
            txt, point, point + offset, s, c, bc, alpha, lw, font, justify, vspacing
        )
        self._caption = fpost
        return fpost

    def caption(
        self,
        txt=None,
        point=None,
        size=(0.30, 0.15),
        padding=5,
        font="Calco",
        justify="center-right",
        vspacing=1.0,
        c=None,
        alpha=1.0,
        lw=1,
        ontop=True,
    ) -> vtki.vtkCaptionActor2D | None:
        """
        Create a 2D caption to an object which follows the camera movements.
        Latex is not supported. Returns the same input object for concatenation.

        See also `flagpole()`, `flagpost()`, `labels()` and `legend()`
        with similar functionality.

        Arguments:
            txt : (str)
                text to be rendered. The default is the file name.
            point : (list)
                anchoring point. The default is None.
            size : (list)
                (width, height) of the caption box. The default is (0.30, 0.15).
            padding : (float)
                padding space of the caption box in pixels. The default is 5.
            font : (str)
                font name. Use a monospace font for better rendering. The default is "VictorMono".
                Type `vedo -r fonts` for a font demo.
                Check [available fonts here](https://vedo.embl.es/fonts).
            justify : (str)
                internal text justification. The default is "center-right".
            vspacing : (float)
                vertical spacing between lines. The default is 1.
            c : (str)
                text and box color. The default is 'lb'.
            alpha : (float)
                text and box transparency. The default is 1.
            lw : (int)
                line width in pixels. The default is 1.
            ontop : (bool)
                keep the 2d caption always on top. The default is True.

        Examples:
            - [caption.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/caption.py)

                ![](https://vedo.embl.es/images/pyplot/caption.png)

            - [flag_labels1.py](https://github.com/marcomusy/vedo/tree/master/examples/other/flag_labels1.py)
            - [flag_labels2.py](https://github.com/marcomusy/vedo/tree/master/examples/other/flag_labels2.py)
        """
        if txt is None:
            if self.filename:
                txt = self.filename.split("/")[-1]
            elif self.name:
                txt = self.name

        if not txt:  # disable it
            self._caption = None
            return None

        for r in vedo.shapes._reps:
            txt = txt.replace(r[0], r[1])

        if c is None:
            c = np.array(self.properties.GetColor()) / 2
        else:
            c = colors.get_color(c)

        if point is None:
            x0, x1, y0, y1, _, z1 = self.dataset.GetBounds()
            pt = [(x0 + x1) / 2, (y0 + y1) / 2, z1]
            point = self.closest_point(pt)

        capt = vtki.vtkCaptionActor2D()
        capt.SetAttachmentPoint(point)
        capt.SetBorder(True)
        capt.SetLeader(True)
        sph = vtki.new("SphereSource")
        sph.Update()
        capt.SetLeaderGlyphData(sph.GetOutput())
        capt.SetMaximumLeaderGlyphSize(5)
        capt.SetPadding(int(padding))
        capt.SetCaption(txt)
        capt.SetWidth(size[0])
        capt.SetHeight(size[1])
        capt.SetThreeDimensionalLeader(not ontop)

        pra = capt.GetProperty()
        pra.SetColor(c)
        pra.SetOpacity(alpha)
        pra.SetLineWidth(lw)

        pr = capt.GetCaptionTextProperty()
        pr.SetFontFamily(vtki.VTK_FONT_FILE)
        fl = utils.get_font_path(font)
        pr.SetFontFile(fl)
        pr.ShadowOff()
        pr.BoldOff()
        pr.FrameOff()
        pr.SetColor(c)
        pr.SetOpacity(alpha)
        pr.SetJustificationToLeft()
        if "top" in justify:
            pr.SetVerticalJustificationToTop()
        if "bottom" in justify:
            pr.SetVerticalJustificationToBottom()
        if "cent" in justify:
            pr.SetVerticalJustificationToCentered()
            pr.SetJustificationToCentered()
        if "left" in justify:
            pr.SetJustificationToLeft()
        if "right" in justify:
            pr.SetJustificationToRight()
        pr.SetLineSpacing(vspacing)
        return capt


class MeshVisualTextureMixin:
    def texture(
        self,
        tname,
        tcoords=None,
        interpolate=True,
        repeat=True,
        edge_clamp=False,
        scale=None,
        ushift=None,
        vshift=None,
    ) -> Self:
        """
        Assign a texture to mesh from image file or predefined texture `tname`.
        If tname is set to `None` texture is disabled.
        Input tname can also be an array or a `vtkTexture`.

        Arguments:
            tname : (numpy.array, str, Image, vtkTexture, None)
                the input texture to be applied. Can be a numpy array, a path to an image file,
                a vedo Image. The None value disables texture.
            tcoords : (numpy.array, str)
                this is the (u,v) texture coordinate array. Can also be a string of an existing array
                in the mesh.
            interpolate : (bool)
                turn on/off linear interpolation of the texture map when rendering.
            repeat : (bool)
                repeat of the texture when tcoords extend beyond the [0,1] range.
            edge_clamp : (bool)
                turn on/off the clamping of the texture map when
                the texture coords extend beyond the [0,1] range.
                Only used when repeat is False, and edge clamping is supported by the graphics card.
            scale : (bool)
                scale the texture image by this factor
            ushift : (bool)
                shift u-coordinates of texture by this amount
            vshift : (bool)
                shift v-coordinates of texture by this amount

        Examples:
            - [texturecubes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/texturecubes.py)

            ![](https://vedo.embl.es/images/basic/texturecubes.png)
        """
        pd = self.dataset
        out_img = None

        if tname is None:  # disable texture
            pd.GetPointData().SetTCoords(None)
            pd.GetPointData().Modified()
            return self  ######################################

        if isinstance(tname, vtki.vtkTexture):
            tu = tname

        elif isinstance(tname, vedo.Image):
            tu = vtki.vtkTexture()
            out_img = tname.dataset

        elif utils.is_sequence(tname):
            tu = vtki.vtkTexture()
            out_img = vedo.image._get_img(tname)

        elif isinstance(tname, str):
            tu = vtki.vtkTexture()

            if "https://" in tname:
                try:
                    tname = vedo.file_io.download(tname, verbose=False)
                except:
                    vedo.logger.error(f"texture {tname} could not be downloaded")
                    return self

            fn = tname + ".jpg"
            if os.path.exists(tname):
                fn = tname
            else:
                vedo.logger.error(f"texture file {tname} does not exist")
                return self

            fnl = fn.lower()
            if ".jpg" in fnl or ".jpeg" in fnl:
                reader = vtki.new("JPEGReader")
            elif ".png" in fnl:
                reader = vtki.new("PNGReader")
            elif ".bmp" in fnl:
                reader = vtki.new("BMPReader")
            else:
                vedo.logger.error("in texture() supported files are only PNG, BMP or JPG")
                return self
            reader.SetFileName(fn)
            reader.Update()
            out_img = reader.GetOutput()

        else:
            vedo.logger.error(f"in texture() cannot understand input {type(tname)}")
            return self

        if tcoords is not None:

            if isinstance(tcoords, str):
                vtarr = pd.GetPointData().GetArray(tcoords)

            else:
                tcoords = np.asarray(tcoords)
                if tcoords.ndim != 2:
                    vedo.logger.error("tcoords must be a 2-dimensional array")
                    return self
                if tcoords.shape[0] != pd.GetNumberOfPoints():
                    vedo.logger.error("nr of texture coords must match nr of points")
                    return self
                if tcoords.shape[1] != 2:
                    vedo.logger.error("tcoords texture vector must have 2 components")
                vtarr = utils.numpy2vtk(tcoords)
                vtarr.SetName("TCoordinates")

            pd.GetPointData().SetTCoords(vtarr)
            pd.GetPointData().Modified()

        elif not pd.GetPointData().GetTCoords():

            # TCoords still void..
            # check that there are no texture-like arrays:
            names = self.pointdata.keys()
            candidate_arr = ""
            for name in names:
                vtarr = pd.GetPointData().GetArray(name)
                if vtarr.GetNumberOfComponents() != 2:
                    continue
                t0, t1 = vtarr.GetRange()
                if t0 >= 0 and t1 <= 1:
                    candidate_arr = name

            if candidate_arr:

                vtarr = pd.GetPointData().GetArray(candidate_arr)
                pd.GetPointData().SetTCoords(vtarr)
                pd.GetPointData().Modified()

            else:
                # last resource is automatic mapping
                tmapper = vtki.new("TextureMapToPlane")
                tmapper.AutomaticPlaneGenerationOn()
                tmapper.SetInputData(pd)
                tmapper.Update()
                tc = tmapper.GetOutput().GetPointData().GetTCoords()
                if scale or ushift or vshift:
                    ntc = utils.vtk2numpy(tc)
                    if scale:
                        ntc *= scale
                    if ushift:
                        ntc[:, 0] += ushift
                    if vshift:
                        ntc[:, 1] += vshift
                    tc = utils.numpy2vtk(tc)
                pd.GetPointData().SetTCoords(tc)
                pd.GetPointData().Modified()

        if out_img:
            tu.SetInputData(out_img)
        tu.SetInterpolate(interpolate)
        tu.SetRepeat(repeat)
        tu.SetEdgeClamp(edge_clamp)

        self.properties.SetColor(1, 1, 1)
        self.mapper.ScalarVisibilityOff()
        self.actor.SetTexture(tu)

        # if seam_threshold is not None:
        #     tname = self.dataset.GetPointData().GetTCoords().GetName()
        #     grad = self.gradient(tname)
        #     ugrad, vgrad = np.split(grad, 2, axis=1)
        #     ugradm, vgradm = utils.mag2(ugrad), utils.mag2(vgrad)
        #     gradm = np.log(ugradm + vgradm)
        #     largegrad_ids = np.arange(len(grad))[gradm > seam_threshold * 4]
        #     uvmap = self.pointdata[tname]
        #     # collapse triangles that have large gradient
        #     new_points = self.points.copy()
        #     for f in self.cells:
        #         if np.isin(f, largegrad_ids).all():
        #             id1, id2, id3 = f
        #             uv1, uv2, uv3 = uvmap[f]
        #             d12 = utils.mag2(uv1 - uv2)
        #             d23 = utils.mag2(uv2 - uv3)
        #             d31 = utils.mag2(uv3 - uv1)
        #             idm = np.argmin([d12, d23, d31])
        #             if idm == 0:
        #                 new_points[id1] = new_points[id3]
        #                 new_points[id2] = new_points[id3]
        #             elif idm == 1:
        #                 new_points[id2] = new_points[id1]
        #                 new_points[id3] = new_points[id1]
        #     self.points = new_points

        self.dataset.Modified()
        return self


__all__ = [
    "PointsVisualEffectsMixin",
    "PointsVisualAnnotationsMixin",
    "MeshVisualTextureMixin",
]
