"""These are private methods we keep out of plotting.py to simplify the module."""

from __future__ import annotations

import warnings

import numpy as np

import pyvista
from pyvista.core.utilities.arrays import get_array
from pyvista.core.utilities.misc import assert_empty_kwargs

from .colors import Color
from .opts import InterpolationType
from .tools import opacity_transfer_function


def prepare_smooth_shading(mesh, scalars, texture, split_sharp_edges, feature_angle, preference):
    """Prepare a dataset for smooth shading.

    VTK requires datasets with Phong shading to have active normals.
    This requires extracting the external surfaces from non-polydata
    datasets and computing the point normals.

    Parameters
    ----------
    mesh : pyvista.DataSet
        Dataset to prepare smooth shading for.

    scalars : sequence
        Sequence of scalars.

    texture : pyvista.Texture or np.ndarray, optional
        A texture to apply to the mesh.

    split_sharp_edges : bool
        Split sharp edges exceeding 30 degrees when plotting with
        smooth shading.  Control the angle with the optional
        keyword argument ``feature_angle``.  By default this is
        ``False``.  Note that enabling this will create a copy of
        the input mesh within the plotter.  See
        :ref:`shading_example`.

    feature_angle : float
        Angle to consider an edge a sharp edge.

    preference : str
        If the number of points is identical to the number of cells.
        Either ``'point'`` or ``'cell'``.

    Returns
    -------
    pyvista.PolyData
        Always a surface as we need to compute point normals.

    """
    is_polydata = isinstance(mesh, pyvista.PolyData)
    indices_array = None

    has_scalars = scalars is not None
    use_points = False
    if has_scalars:
        if not isinstance(scalars, np.ndarray):
            scalars = np.array(scalars)
        if scalars.shape[0] == mesh.n_points and scalars.shape[0] == mesh.n_cells:
            use_points = preference == 'point'
        else:
            use_points = scalars.shape[0] == mesh.n_points

    # extract surface if not already a surface
    if not is_polydata:
        mesh = mesh.extract_surface(
            pass_pointid=use_points or texture is not None,
            pass_cellid=not use_points,
        )
        indices_array = 'vtkOriginalPointIds' if use_points else 'vtkOriginalCellIds'

    try:
        if split_sharp_edges:
            mesh = mesh.compute_normals(
                cell_normals=False,
                split_vertices=True,
                feature_angle=feature_angle,
            )
            if is_polydata:
                if has_scalars and use_points:
                    # we must track the original IDs with our own array from compute_normals
                    indices_array = 'pyvistaOriginalPointIds'
        elif mesh.point_data.active_normals is None:
            mesh.compute_normals(cell_normals=False, inplace=True)
    except TypeError as e:
        if 'Normals cannot be computed' in repr(e):
            pass
        else:
            raise

    if has_scalars and indices_array is not None:
        ind = mesh[indices_array]
        scalars = np.asarray(scalars)[ind]

    return mesh, scalars


def process_opacity(mesh, opacity, preference, n_colors, scalars, use_transparency):
    """Process opacity.

    This function accepts an opacity string or array and always
    returns an array that can be applied to a dataset for plotting.

    Parameters
    ----------
    mesh : pyvista.DataSet
        Dataset to process the opacity for.

    opacity : str, sequence
        String or array.  If string, can be a ``str`` name of a
        predefined mapping such as ``'linear'``, ``'geom'``,
        ``'sigmoid'``, ``'sigmoid3-10'``, or the key of a cell or
        point data array.

    preference : str
        When ``mesh.n_points == mesh.n_cells``, this parameter
        sets how the scalars will be mapped to the mesh. If
        ``'point'``, causes the scalars will be associated with
        the mesh points.  Can be either ``'point'`` or
        ``'cell'``.

    n_colors : int
        Number of colors to use when displaying the opacity.

    scalars : numpy.ndarray
        Dataset scalars.

    use_transparency : bool
        Invert the opacity mappings and make the values correspond
        to transparency.

    Returns
    -------
    custom_opac : bool
        If using custom opacity.

    opacity : numpy.ndarray
        Array containing the opacity.

    """
    custom_opac = False
    if isinstance(opacity, str):
        try:
            # Get array from mesh
            opacity = get_array(mesh, opacity, preference=preference, err=True)
            if np.any(opacity > 1):
                warnings.warn('Opacity scalars contain values over 1')
            if np.any(opacity < 0):
                warnings.warn('Opacity scalars contain values less than 0')
            custom_opac = True
        except KeyError:
            # Or get opacity transfer function (e.g. "linear")
            opacity = opacity_transfer_function(opacity, n_colors)
        else:
            if scalars.shape[0] != opacity.shape[0]:
                msg = 'Opacity array and scalars array must have the same number of elements.'
                raise ValueError(msg)
    elif isinstance(opacity, (np.ndarray, list, tuple)):
        opacity = np.asanyarray(opacity)
        if opacity.shape[0] in [mesh.n_cells, mesh.n_points]:
            # User could pass an array of opacities for every point/cell
            custom_opac = True
        else:
            opacity = opacity_transfer_function(opacity, n_colors)

    if use_transparency:
        if np.max(opacity) <= 1.0:
            opacity = 1 - opacity
        elif isinstance(opacity, np.ndarray):
            opacity = 255 - opacity

    return custom_opac, opacity


def _common_arg_parser(
    dataset,
    theme,
    n_colors,
    scalar_bar_args,
    split_sharp_edges,
    show_scalar_bar,
    render_points_as_spheres,
    smooth_shading,
    pbr,
    clim,
    cmap,
    culling,
    name,
    nan_color,
    nan_opacity,
    texture,
    rgb,
    style,
    **kwargs,
):
    """Parse arguments in common between add_volume, composite, and mesh."""
    # supported aliases
    clim = kwargs.pop('rng', clim)
    cmap = kwargs.pop('colormap', cmap)
    culling = kwargs.pop('backface_culling', culling)
    rgb = kwargs.pop('rgba', rgb)
    vertex_color = kwargs.pop('vertex_color', theme.edge_color)
    vertex_style = kwargs.pop('vertex_style', 'points')
    vertex_opacity = kwargs.pop('vertex_opacity', 1.0)

    # Support aliases for 'back', 'front', or 'none'. Consider deprecating
    if culling is False:
        culling = 'none'
    elif culling in ['b', 'backface', True]:
        culling = 'back'
    elif culling in ['f', 'frontface']:
        culling = 'front'

    if show_scalar_bar is None:
        # use theme unless plotting RGB
        _default = theme.show_scalar_bar or scalar_bar_args
        show_scalar_bar = False if rgb else _default
    # Avoid mutating input
    scalar_bar_args = {'n_colors': n_colors} if scalar_bar_args is None else scalar_bar_args.copy()

    # theme based parameters
    if split_sharp_edges is None:
        split_sharp_edges = theme.split_sharp_edges
    feature_angle = kwargs.pop('feature_angle', theme.sharp_edges_feature_angle)
    if render_points_as_spheres is None:
        if style == 'points_gaussian':
            render_points_as_spheres = False
        else:
            render_points_as_spheres = theme.render_points_as_spheres

    if smooth_shading is None:
        smooth_shading = True if pbr else theme.smooth_shading

    if name is None:
        name = f'{type(dataset).__name__}({dataset.memory_address})'
        remove_existing_actor = False
    else:
        # check if this actor already exists
        remove_existing_actor = True

    nan_color = Color(nan_color, opacity=nan_opacity, default_color=theme.nan_color)

    if texture is False:
        texture = None

    # allow directly specifying interpolation (potential future feature)
    if 'interpolation' in kwargs:
        interpolation = kwargs.pop('interpolation')  # pragma: no cover:
    elif pbr:
        interpolation = InterpolationType.PBR
    elif smooth_shading:
        interpolation = InterpolationType.PHONG
    else:
        interpolation = theme.lighting_params.interpolation

    if 'scalar' in kwargs:
        msg = '`scalar` is an invalid keyword argument. Perhaps you mean `scalars` with an s?'
        raise TypeError(msg)

    assert_empty_kwargs(**kwargs)
    return (
        scalar_bar_args,
        split_sharp_edges,
        show_scalar_bar,
        feature_angle,
        render_points_as_spheres,
        smooth_shading,
        clim,
        cmap,
        culling,
        name,
        nan_color,
        texture,
        rgb,
        interpolation,
        remove_existing_actor,
        vertex_color,
        vertex_style,
        vertex_opacity,
    )
