import inspect
import itertools
import os
from functools import lru_cache
from logging import getLogger
from typing import Any, Dict, List, Optional, Sequence, Set, Union
import numpy as np

from .. import layers
from ..layers.image._image_utils import guess_labels
from ..layers.utils.stack_utils import split_channels
from ..plugins.io import read_data_with_plugins
from ..types import FullLayerData, LayerData
from ..utils.colormaps import ensure_colormap_tuple
from ..utils.misc import is_sequence

logger = getLogger(__name__)


class AddLayersMixin:
    """A mixin that adds add_* methods for adding layers to the ViewerModel.

    Each method corresponds to adding one or more layers to the viewer.
    Methods that just add a single layer contain the keyword arguments and
    copies of the documentation from that the layer. These are copied and
    pasted instead of being autogenerated because IDEs like PyCharm parse the
    source code for docs instead of pulling it up dynamically.

    These methods are separated into a mixin to keep the ViewerModel class
    easier to read and make these methods easier to maintain.
    """

    def add_layer(self, layer: layers.Layer) -> layers.Layer:
        """Add a layer to the viewer.

        Parameters
        ----------
        layer : :class:`napari.layers.Layer`
            Layer to add.

        Returns
        -------
        layer : :class:`napari.layers.Layer` or list
            The layer that was added (same as input).
        """
        layer.events.select.connect(self._update_active_layer)
        layer.events.deselect.connect(self._update_active_layer)
        layer.events.status.connect(self._update_status)
        layer.events.help.connect(self._update_help)
        layer.events.interactive.connect(self._update_interactive)
        layer.events.cursor.connect(self._update_cursor)
        layer.events.cursor_size.connect(self._update_cursor_size)
        layer.events.data.connect(self._on_layers_change)
        layer.dims.events.ndisplay.connect(self._on_layers_change)
        layer.dims.events.order.connect(self._on_layers_change)
        layer.dims.events.range.connect(self._on_layers_change)
        self.layers.append(layer)
        self._update_layers(layers=[layer])

        if len(self.layers) == 1:
            self.reset_view()
        return layer

    def add_image(
        self,
        data=None,
        *,
        channel_axis=None,
        rgb=None,
        colormap=None,
        contrast_limits=None,
        gamma=1,
        interpolation='nearest',
        rendering='mip',
        iso_threshold=0.5,
        attenuation=0.05,
        name=None,
        metadata=None,
        scale=None,
        translate=None,
        opacity=1,
        blending=None,
        visible=True,
        multiscale=None,
    ) -> Union[layers.Image, List[layers.Image]]:
        """Add an image layer to the layers list.

        Parameters
        ----------
        data : array or list of array
            Image data. Can be N dimensional. If the last dimension has length
            3 or 4 can be interpreted as RGB or RGBA if rgb is `True`. If a
            list and arrays are decreasing in shape then the data is treated as
            a multiscale image.
        channel_axis : int, optional
            Axis to expand image along.  If provided, each channel in the data
            will be added as an individual image layer.  In channel_axis mode,
            all other parameters MAY be provided as lists, and the Nth value
            will be applied to the Nth channel in the data.  If a single value
            is provided, it will be broadcast to all Layers.
        rgb : bool or list
            Whether the image is rgb RGB or RGBA. If not specified by user and
            the last dimension of the data has length 3 or 4 it will be set as
            `True`. If `False` the image is interpreted as a luminance image.
            If a list then must be same length as the axis that is being
            expanded as channels.
        colormap : str, vispy.Color.Colormap, tuple, dict, list
            Colormaps to use for luminance images. If a string must be the name
            of a supported colormap from vispy or matplotlib. If a tuple the
            first value must be a string to assign as a name to a colormap and
            the second item must be a Colormap. If a dict the key must be a
            string to assign as a name to a colormap and the value must be a
            Colormap. If a list then must be same length as the axis that is
            being expanded as channels, and each colormap is applied to each
            new image layer.
        contrast_limits : list (2,)
            Color limits to be used for determining the colormap bounds for
            luminance images. If not passed is calculated as the min and max of
            the image. If list of lists then must be same length as the axis
            that is being expanded and then each colormap is applied to each
            image.
        gamma : list, float
            Gamma correction for determining colormap linearity. Defaults to 1.
            If a list then must be same length as the axis that is being
            expanded as channels.
        interpolation : str or list
            Interpolation mode used by vispy. Must be one of our supported
            modes. If a list then must be same length as the axis that is being
            expanded as channels.
        rendering : str or list
            Rendering mode used by vispy. Must be one of our supported
            modes. If a list then must be same length as the axis that is being
            expanded as channels.
        iso_threshold : float or list
            Threshold for isosurface. If a list then must be same length as the
            axis that is being expanded as channels.
        attenuation : float or list
            Attenuation rate for attenuated maximum intensity projection. If a
            list then must be same length as the axis that is being expanded as
            channels.
        name : str or list of str
            Name of the layer.  If a list then must be same length as the axis
            that is being expanded as channels.
        metadata : dict or list of dict
            Layer metadata. If a list then must be a list of dicts with the
            same length as the axis that is being expanded as channels.
        scale : tuple of float or list
            Scale factors for the layer. If a list then must be a list of
            tuples of float with the same length as the axis that is being
            expanded as channels.
        translate : tuple of float or list
            Translation values for the layer. If a list then must be a list of
            tuples of float with the same length as the axis that is being
            expanded as channels.
        opacity : float or list
            Opacity of the layer visual, between 0.0 and 1.0.  If a list then
            must be same length as the axis that is being expanded as channels.
        blending : str or list
            One of a list of preset blending modes that determines how RGB and
            alpha values of the layer visual get mixed. Allowed values are
            {'opaque', 'translucent', and 'additive'}. If a list then
            must be same length as the axis that is being expanded as channels.
        visible : bool or list of bool
            Whether the layer visual is currently being displayed.
            If a list then must be same length as the axis that is
            being expanded as channels.
        multiscale : bool
            Whether the data is a multiscale image or not. Multiscale data is
            represented by a list of array like image data. If not specified by
            the user and if the data is a list of arrays that decrease in shape
            then it will be taken to be multiscale. The first image in the list
            should be the largest.

        Returns
        -------
        layer : :class:`napari.layers.Image` or list
            The newly-created image layer or list of image layers.
        """

        if colormap is not None:
            # standardize colormap argument(s) to strings, and make sure they
            # are in AVAILABLE_COLORMAPS.  This will raise one of many various
            # errors if the colormap argument is invalid.  See
            # ensure_colormap_tuple for details
            if isinstance(colormap, list):
                colormap = [ensure_colormap_tuple(c)[0] for c in colormap]
            else:
                colormap, _ = ensure_colormap_tuple(colormap)

        # doing this here for IDE/console autocompletion in add_image function.
        kwargs = {
            'rgb': rgb,
            'colormap': colormap,
            'contrast_limits': contrast_limits,
            'gamma': gamma,
            'interpolation': interpolation,
            'rendering': rendering,
            'iso_threshold': iso_threshold,
            'attenuation': attenuation,
            'name': name,
            'metadata': metadata,
            'scale': scale,
            'translate': translate,
            'opacity': opacity,
            'blending': blending,
            'visible': visible,
            'multiscale': multiscale,
        }

        # these arguments are *already* iterables in the single-channel case.
        iterable_kwargs = {'scale', 'translate', 'contrast_limits', 'metadata'}

        if channel_axis is None:
            kwargs['colormap'] = kwargs['colormap'] or 'gray'
            kwargs['blending'] = kwargs['blending'] or 'translucent'
            # Helpful message if someone tries to add mulit-channel kwargs,
            # but forget the channel_axis arg
            for k, v in kwargs.items():
                if k not in iterable_kwargs and is_sequence(v):
                    raise TypeError(
                        f"Received sequence for argument '{k}', "
                        "did you mean to specify a 'channel_axis'? "
                    )

            return self.add_layer(layers.Image(data, **kwargs))
        else:
            layerdata_list = split_channels(data, channel_axis, **kwargs)

            layer_list = list()
            for image, i_kwargs, _ in layerdata_list:
                layer = self.add_layer(layers.Image(image, **i_kwargs))
                layer_list.append(layer)

            return layer_list

    def add_points(
        self,
        data=None,
        *,
        properties=None,
        text=None,
        symbol='o',
        size=10,
        edge_width=1,
        edge_color='black',
        edge_color_cycle=None,
        edge_colormap='viridis',
        edge_contrast_limits=None,
        face_color='white',
        face_color_cycle=None,
        face_colormap='viridis',
        face_contrast_limits=None,
        n_dimensional=False,
        name=None,
        metadata=None,
        scale=None,
        translate=None,
        opacity=1,
        blending='translucent',
        visible=True,
    ) -> layers.Points:
        """Add a points layer to the layers list.

        Parameters
        ----------
        data : array (N, D)
            Coordinates for N points in D dimensions.
        properties : dict {str: array (N,)}, DataFrame
            Properties for each point. Each property should be an array of length N,
            where N is the number of points.
        text : str, dict
            Text to be displayed with the points. If text is set to a key in properties,
            the value of that property will be displayed. Multiple properties can be
            composed using f-string-like syntax (e.g., '{property_1}, {float_property:.2f}).
            A dictionary can be provided with keyword arguments to set the text values
            and display properties. See TextManager.__init__() for the valid keyword arguments.
            For example usage, see /napari/examples/add_points_with_text.py.
        symbol : str
            Symbol to be used for the point markers. Must be one of the
            following: arrow, clobber, cross, diamond, disc, hbar, ring,
            square, star, tailed_arrow, triangle_down, triangle_up, vbar, x.
        size : float, array
            Size of the point marker. If given as a scalar, all points are made
            the same size. If given as an array, size must be the same
            broadcastable to the same shape as the data.
        edge_width : float
            Width of the symbol edge in pixels.
        edge_color : str, array-like
            Color of the point marker border. Numeric color values should be RGB(A).
        edge_color_cycle : np.ndarray, list
            Cycle of colors (provided as string name, RGB, or RGBA) to map to edge_color if a
            categorical attribute is used color the vectors.
        edge_colormap : str, vispy.color.colormap.Colormap
            Colormap to set edge_color if a continuous attribute is used to set face_color.
            See vispy docs for details: http://vispy.org/color.html#vispy.color.Colormap
        edge_contrast_limits : None, (float, float)
            clims for mapping the property to a color map. These are the min and max value
            of the specified property that are mapped to 0 and 1, respectively.
            The default value is None. If set the none, the clims will be set to
            (property.min(), property.max())
        face_color : str, array-like
            Color of the point marker body. Numeric color values should be RGB(A).
        face_color_cycle : np.ndarray, list
            Cycle of colors (provided as string name, RGB, or RGBA) to map to face_color if a
            categorical attribute is used color the vectors.
        face_colormap : str, vispy.color.colormap.Colormap
            Colormap to set face_color if a continuous attribute is used to set face_color.
            See vispy docs for details: http://vispy.org/color.html#vispy.color.Colormap
        face_contrast_limits : None, (float, float)
            clims for mapping the property to a color map. These are the min and max value
            of the specified property that are mapped to 0 and 1, respectively.
            The default value is None. If set the none, the clims will be set to
            (property.min(), property.max())
        n_dimensional : bool
            If True, renders points not just in central plane but also in all
            n-dimensions according to specified point marker size.
        name : str
            Name of the layer.
        metadata : dict
            Layer metadata.
        scale : tuple of float
            Scale factors for the layer.
        translate : tuple of float
            Translation values for the layer.
        opacity : float
            Opacity of the layer visual, between 0.0 and 1.0.
        blending : str
            One of a list of preset blending modes that determines how RGB and
            alpha values of the layer visual get mixed. Allowed values are
            {'opaque', 'translucent', and 'additive'}.
        visible : bool
            Whether the layer visual is currently being displayed.

        Returns
        -------
        layer : :class:`napari.layers.Points`
            The newly-created points layer.

        Notes
        -----
        See vispy's marker visual docs for more details:
        http://api.vispy.org/en/latest/visuals.html#vispy.visuals.MarkersVisual
        """
        if data is None:
            ndim = max(self.dims.ndim, 2)
            data = np.empty([0, ndim])

        layer = layers.Points(
            data=data,
            properties=properties,
            text=text,
            symbol=symbol,
            size=size,
            edge_width=edge_width,
            edge_color=edge_color,
            edge_color_cycle=edge_color_cycle,
            edge_colormap=edge_colormap,
            edge_contrast_limits=edge_contrast_limits,
            face_color=face_color,
            face_color_cycle=face_color_cycle,
            face_colormap=face_colormap,
            face_contrast_limits=face_contrast_limits,
            n_dimensional=n_dimensional,
            name=name,
            metadata=metadata,
            scale=scale,
            translate=translate,
            opacity=opacity,
            blending=blending,
            visible=visible,
        )
        self.add_layer(layer)
        return layer

    def add_labels(
        self,
        data,
        *,
        num_colors=50,
        properties=None,
        color=None,
        seed=0.5,
        name=None,
        metadata=None,
        scale=None,
        translate=None,
        opacity=0.7,
        blending='translucent',
        visible=True,
        multiscale=None,
    ) -> layers.Labels:
        """Add a labels (or segmentation) layer to the layers list.

        An image-like layer where every pixel contains an integer ID
        corresponding to the region it belongs to.

        Using the viewer's label editing tools (painting, erasing) will
        modify the input-array in-place.

        To avoid this, pass a copy as follows:
            layer = viewer.add_labels(data.copy())
            # do some painting/editing

        Get the modified labels as follows:
            result = layer.data

        Parameters
        ----------
        data : array or list of array
            Labels data as an array or multiscale.
        num_colors : int
            Number of unique colors to use in colormap.
        properties : dict {str: array (N,)}, DataFrame
            Properties for each label. Each property should be an array of
            length N, where N is the number of labels, and the first property
            corresponds to background.
        color : dict of int to str or array
            Custom label to color mapping. Values must be valid color names or
            RGBA arrays.
        seed : float
            Seed for colormap random generator.
        name : str
            Name of the layer.
        metadata : dict
            Layer metadata.
        scale : tuple of float
            Scale factors for the layer.
        translate : tuple of float
            Translation values for the layer.
        opacity : float
            Opacity of the layer visual, between 0.0 and 1.0.
        blending : str
            One of a list of preset blending modes that determines how RGB and
            alpha values of the layer visual get mixed. Allowed values are
            {'opaque', 'translucent', and 'additive'}.
        visible : bool
            Whether the layer visual is currently being displayed.
        multiscale : bool
            Whether the data is a multiscale image or not. Multiscale data is
            represented by a list of array like image data. If not specified by
            the user and if the data is a list of arrays that decrease in shape
            then it will be taken to be multiscale. The first image in the list
            should be the largest.

        Returns
        -------
        layer : :class:`napari.layers.Labels`
            The newly-created labels layer.
        """
        layer = layers.Labels(
            data,
            num_colors=num_colors,
            properties=properties,
            color=color,
            seed=seed,
            name=name,
            metadata=metadata,
            scale=scale,
            translate=translate,
            opacity=opacity,
            blending=blending,
            visible=visible,
            multiscale=multiscale,
        )
        self.add_layer(layer)
        return layer

    def add_shapes(
        self,
        data=None,
        *,
        ndim=None,
        properties=None,
        text=None,
        shape_type='rectangle',
        edge_width=1,
        edge_color='black',
        edge_color_cycle=None,
        edge_colormap='viridis',
        edge_contrast_limits=None,
        face_color='white',
        face_color_cycle=None,
        face_colormap='viridis',
        face_contrast_limits=None,
        z_index=0,
        name=None,
        metadata=None,
        scale=None,
        translate=None,
        opacity=0.7,
        blending='translucent',
        visible=True,
    ) -> layers.Shapes:
        """Add a shapes layer to the layers list.

        Parameters
        ----------
        data : list or array
            List of shape data, where each element is an (N, D) array of the
            N vertices of a shape in D dimensions. Can be an 3-dimensional
            array if each shape has the same number of vertices.
        ndim : int
            Number of dimensions for shapes. When data is not None, ndim must be D.
            An empty shapes layer can be instantiated with arbitrary ndim.
        properties : dict {str: array (N,)}, DataFrame
            Properties for each shape. Each property should be an array of
            length N, where N is the number of shapes.
        text : str, dict
            Text to be displayed with the shapes. If text is set to a key in properties,
            the value of that property will be displayed. Multiple properties can be
            composed using f-string-like syntax (e.g., '{property_1}, {float_property:.2f}).
            A dictionary can be provided with keyword arguments to set the text values
            and display properties. See TextManager.__init__() for the valid keyword arguments.
            For example usage, see /napari/examples/add_shapes_with_text.py.
        shape_type : string or list
            String of shape shape_type, must be one of "{'line', 'rectangle',
            'ellipse', 'path', 'polygon'}". If a list is supplied it must be
            the same length as the length of `data` and each element will be
            applied to each shape otherwise the same value will be used for all
            shapes.
        edge_width : float or list
            Thickness of lines and edges. If a list is supplied it must be the
            same length as the length of `data` and each element will be
            applied to each shape otherwise the same value will be used for all
            shapes.
        edge_color : str, array-like
            If string can be any color name recognized by vispy or hex value if
            starting with `#`. If array-like must be 1-dimensional array with 3
            or 4 elements. If a list is supplied it must be the same length as
            the length of `data` and each element will be applied to each shape
            otherwise the same value will be used for all shapes.
        edge_color_cycle : np.ndarray, list
            Cycle of colors (provided as string name, RGB, or RGBA) to map to edge_color if a
            categorical attribute is used color the vectors.
        edge_colormap : str, vispy.color.colormap.Colormap
            Colormap to set edge_color if a continuous attribute is used to set face_color.
            See vispy docs for details: http://vispy.org/color.html#vispy.color.Colormap
        edge_contrast_limits : None, (float, float)
            clims for mapping the property to a color map. These are the min and max value
            of the specified property that are mapped to 0 and 1, respectively.
            The default value is None. If set the none, the clims will be set to
            (property.min(), property.max())
        face_color : str, array-like
            If string can be any color name recognized by vispy or hex value if
            starting with `#`. If array-like must be 1-dimensional array with 3
            or 4 elements. If a list is supplied it must be the same length as
            the length of `data` and each element will be applied to each shape
            otherwise the same value will be used for all shapes.
        face_color_cycle : np.ndarray, list
            Cycle of colors (provided as string name, RGB, or RGBA) to map to face_color if a
            categorical attribute is used color the vectors.
        face_colormap : str, vispy.color.colormap.Colormap
            Colormap to set face_color if a continuous attribute is used to set face_color.
            See vispy docs for details: http://vispy.org/color.html#vispy.color.Colormap
        face_contrast_limits : None, (float, float)
            clims for mapping the property to a color map. These are the min and max value
            of the specified property that are mapped to 0 and 1, respectively.
            The default value is None. If set the none, the clims will be set to
            (property.min(), property.max())
        z_index : int or list
            Specifier of z order priority. Shapes with higher z order are
            displayed ontop of others. If a list is supplied it must be the
            same length as the length of `data` and each element will be
            applied to each shape otherwise the same value will be used for all
            shapes.
        name : str
            Name of the layer.
        metadata : dict
            Layer metadata.
        scale : tuple of float
            Scale factors for the layer.
        translate : tuple of float
            Translation values for the layer.
        opacity : float
            Opacity of the layer visual, between 0.0 and 1.0.
        blending : str
            One of a list of preset blending modes that determines how RGB and
            alpha values of the layer visual get mixed. Allowed values are
            {'opaque', 'translucent', and 'additive'}.
        visible : bool
            Whether the layer visual is currently being displayed.

        Returns
        -------
        layer : :class:`napari.layers.Shapes`
            The newly-created shapes layer.
        """
        if data is None:
            if ndim is None:
                ndim = max(self.dims.ndim, 2)
            data = np.empty((0, 0, ndim))

        layer = layers.Shapes(
            data=data,
            ndim=ndim,
            properties=properties,
            text=text,
            shape_type=shape_type,
            edge_width=edge_width,
            edge_color=edge_color,
            edge_color_cycle=edge_color_cycle,
            edge_colormap=edge_colormap,
            edge_contrast_limits=edge_contrast_limits,
            face_color=face_color,
            face_color_cycle=face_color_cycle,
            face_colormap=face_colormap,
            face_contrast_limits=face_contrast_limits,
            z_index=z_index,
            name=name,
            metadata=metadata,
            scale=scale,
            translate=translate,
            opacity=opacity,
            blending=blending,
            visible=visible,
        )
        self.add_layer(layer)
        return layer

    def add_surface(
        self,
        data,
        *,
        colormap='gray',
        contrast_limits=None,
        gamma=1,
        name=None,
        metadata=None,
        scale=None,
        translate=None,
        opacity=1,
        blending='translucent',
        visible=True,
    ) -> layers.Surface:
        """Add a surface layer to the layers list.

        Parameters
        ----------
        data : 3-tuple of array
            The first element of the tuple is an (N, D) array of vertices of
            mesh triangles. The second is an (M, 3) array of int of indices
            of the mesh triangles. The third element is the (K0, ..., KL, N)
            array of values used to color vertices where the additional L
            dimensions are used to color the same mesh with different values.
        colormap : str, vispy.Color.Colormap, tuple, dict
            Colormap to use for luminance images. If a string must be the name
            of a supported colormap from vispy or matplotlib. If a tuple the
            first value must be a string to assign as a name to a colormap and
            the second item must be a Colormap. If a dict the key must be a
            string to assign as a name to a colormap and the value must be a
            Colormap.
        contrast_limits : list (2,)
            Color limits to be used for determining the colormap bounds for
            luminance images. If not passed is calculated as the min and max of
            the image.
        gamma : float
            Gamma correction for determining colormap linearity. Defaults to 1.
        name : str
            Name of the layer.
        metadata : dict
            Layer metadata.
        scale : tuple of float
            Scale factors for the layer.
        translate : tuple of float
            Translation values for the layer.
        opacity : float
            Opacity of the layer visual, between 0.0 and 1.0.
        blending : str
            One of a list of preset blending modes that determines how RGB and
            alpha values of the layer visual get mixed. Allowed values are
            {'opaque', 'translucent', and 'additive'}.
        visible : bool
            Whether the layer visual is currently being displayed.

        Returns
        -------
        layer : :class:`napari.layers.Surface`
            The newly-created surface layer.
        """
        layer = layers.Surface(
            data,
            colormap=colormap,
            contrast_limits=contrast_limits,
            gamma=gamma,
            name=name,
            metadata=metadata,
            scale=scale,
            translate=translate,
            opacity=opacity,
            blending=blending,
            visible=visible,
        )
        self.add_layer(layer)
        return layer

    def add_vectors(
        self,
        data,
        *,
        properties=None,
        edge_width=1,
        edge_color='red',
        edge_color_cycle=None,
        edge_colormap='viridis',
        edge_contrast_limits=None,
        length=1,
        name=None,
        metadata=None,
        scale=None,
        translate=None,
        opacity=0.7,
        blending='translucent',
        visible=True,
    ) -> layers.Vectors:
        """Add a vectors layer to the layers list.

        Parameters
        ----------
        data : (N, 2, D) or (N1, N2, ..., ND, D) array
            An (N, 2, D) array is interpreted as "coordinate-like" data and a
            list of N vectors with start point and projections of the vector in
            D dimensions. An (N1, N2, ..., ND, D) array is interpreted as
            "image-like" data where there is a length D vector of the
            projections at each pixel.
        properties : dict {str: array (N,)}, DataFrame
            Properties for each vector. Each property should be an array of length N,
            where N is the number of vectors.
        edge_width : float
            Width for all vectors in pixels.
        length : float
            Multiplicative factor on projections for length of all vectors.
        edge_color : str
            Color of all of the vectors.
        edge_color_cycle : np.ndarray, list
            Cycle of colors (provided as string name, RGB, or RGBA) to map to edge_color if a
            categorical attribute is used color the vectors.
        edge_colormap : str, vispy.color.colormap.Colormap
            Colormap to set vector color if a continuous attribute is used to set edge_color.
            See vispy docs for details: http://vispy.org/color.html#vispy.color.Colormap
        edge_contrast_limits : None, (float, float)
            clims for mapping the property to a color map. These are the min and max value
            of the specified property that are mapped to 0 and 1, respectively.
            The default value is None. If set the none, the clims will be set to
            (property.min(), property.max())
        name : str
            Name of the layer.
        metadata : dict
            Layer metadata.
        scale : tuple of float
            Scale factors for the layer.
        translate : tuple of float
            Translation values for the layer.
        opacity : float
            Opacity of the layer visual, between 0.0 and 1.0.
        blending : str
            One of a list of preset blending modes that determines how RGB and
            alpha values of the layer visual get mixed. Allowed values are
            {'opaque', 'translucent', and 'additive'}.
        visible : bool
            Whether the layer visual is currently being displayed.

        Returns
        -------
        layer : :class:`napari.layers.Vectors`
            The newly-created vectors layer.
        """
        layer = layers.Vectors(
            data,
            properties=properties,
            edge_width=edge_width,
            edge_color=edge_color,
            edge_color_cycle=edge_color_cycle,
            edge_colormap=edge_colormap,
            edge_contrast_limits=edge_contrast_limits,
            length=length,
            name=name,
            metadata=metadata,
            scale=scale,
            translate=translate,
            opacity=opacity,
            blending=blending,
            visible=visible,
        )
        self.add_layer(layer)
        return layer

    def open(
        self,
        path: Union[str, Sequence[str]],
        stack: bool = False,
        plugin: Optional[str] = None,
        layer_type: Optional[str] = None,
        **kwargs,
    ) -> List[layers.Layer]:
        """Open a path or list of paths with plugins, and add layers to viewer.

        A list of paths will be handed one-by-one to the napari_get_reader hook
        if stack is False, otherwise the full list is passed to each plugin
        hook.

        Parameters
        ----------
        path : str or list of str
            A filepath, directory, or URL (or a list of any) to open.
        stack : bool, optional
            If a list of strings is passed and ``stack`` is ``True``, then the
            entire list will be passed to plugins.  It is then up to individual
            plugins to know how to handle a list of paths.  If ``stack`` is
            ``False``, then the ``path`` list is broken up and passed to plugin
            readers one by one.  by default False.
        plugin : str, optional
            Name of a plugin to use.  If provided, will force ``path`` to be
            read with the specified ``plugin``.  If the requested plugin cannot
            read ``path``, an exception will be raised.
        layer_type : str, optional
            If provided, will force data read from ``path`` to be passed to the
            corresponding ``add_<layer_type>`` method (along with any
            additional) ``kwargs`` provided to this function.  This *may*
            result in exceptions if the data returned from the path is not
            compatible with the layer_type.
        **kwargs
            All other keyword arguments will be passed on to the respective
            ``add_layer`` method.

        Returns
        -------
        layers : list
            A list of any layers that were added to the viewer.
        """
        paths = [path] if isinstance(path, str) else path
        paths = [os.fspath(path) for path in paths]  # PathObjects -> str
        if not isinstance(paths, (tuple, list)):
            raise ValueError(
                "'path' argument must be a string, list, or tuple"
            )

        if stack:
            return self._add_layers_with_plugins(
                paths, kwargs, plugin=plugin, layer_type=layer_type
            )

        added: List[layers.Layer] = []  # for layers that get added
        for _path in paths:
            added.extend(
                self._add_layers_with_plugins(
                    _path, kwargs, plugin=plugin, layer_type=layer_type
                )
            )

        return added

    def _add_layers_with_plugins(
        self,
        path_or_paths: Union[str, Sequence[str]],
        kwargs: Optional[dict] = None,
        plugin: Optional[str] = None,
        layer_type: Optional[str] = None,
    ) -> List[layers.Layer]:
        """Load a path or a list of paths into the viewer using plugins.

        This function is mostly called from self.open_path, where the ``stack``
        argument determines whether a list of strings is handed to plugins one
        at a time, or en-masse.

        Parameters
        ----------
        path_or_paths : str or list of str
            A filepath, directory, or URL (or a list of any) to open. If a
            list, the assumption is that the list is to be treated as a stack.
        kwargs : dict, optional
            keyword arguments that will be used to overwrite any of those that
            are returned in the meta dict from plugins.
        plugin : str, optional
            Name of a plugin to use.  If provided, will force ``path`` to be
            read with the specified ``plugin``.  If the requested plugin cannot
            read ``path``, an exception will be raised.
        layer_type : str, optional
            If provided, will force data read from ``path`` to be passed to the
            corresponding ``add_<layer_type>`` method (along with any
            additional) ``kwargs`` provided to this function.  This *may*
            result in exceptions if the data returned from the path is not
            compatible with the layer_type.

        Returns
        -------
        List[layers.Layer]
            A list of any layers that were added to the viewer.
        """
        layer_data = read_data_with_plugins(path_or_paths, plugin=plugin)

        # glean layer names from filename. These will be used as *fallback*
        # names, if the plugin does not return a name kwarg in their meta dict.
        if isinstance(path_or_paths, str):
            filenames = itertools.repeat(path_or_paths)
        elif is_sequence(path_or_paths):
            if len(path_or_paths) == len(layer_data):
                filenames = iter(path_or_paths)
            else:
                # if a list of paths has been returned as a list of layer data
                # without a 1:1 relationship between the two lists we iterate
                # over the first name
                filenames = itertools.repeat(path_or_paths[0])

        # add each layer to the viewer
        added: List[layers.Layer] = []  # for layers that get added
        for data, filename in zip(layer_data, filenames):
            basename, ext = os.path.splitext(os.path.basename(filename))
            _data = _unify_data_and_user_kwargs(
                data, kwargs, layer_type, fallback_name=basename
            )
            # actually add the layer
            new = self._add_layer_from_data(*_data)
            # some add_* methods return a List[Layer], others just a Layer
            # we want to always return a list
            added.extend(new if isinstance(new, list) else [new])
        return added

    def _add_layer_from_data(
        self, data, meta: dict = None, layer_type: Optional[str] = None
    ) -> Union[layers.Layer, List[layers.Layer]]:
        """Add arbitrary layer data to the viewer.

        Primarily intended for usage by reader plugin hooks.

        Parameters
        ----------
        data : Any
            Data in a format that is valid for the corresponding `add_*` method
            of the specified ``layer_type``.
        meta : dict, optional
            Dict of keyword arguments that will be passed to the corresponding
            `add_*` method.  MUST NOT contain any keyword arguments that are
            not valid for the corresponding method.
        layer_type : str
            Type of layer to add.  MUST have a corresponding add_* method on
            on the viewer instance.  If not provided, the layer is assumed to
            be "image", unless data.dtype is one of (np.int32, np.uint32,
            np.int64, np.uint64), in which case it is assumed to be "labels".

        Raises
        ------
        ValueError
            If ``layer_type`` is not one of the recognized layer types.
        TypeError
            If any keyword arguments in ``meta`` are unexpected for the
            corresponding `add_*` method for this layer_type.

        Examples
        --------
        A typical use case might be to upack a tuple of layer data with a
        specified layer_type.

        >>> viewer = napari.Viewer()
        >>> data = (
        ...     np.random.random((10, 2)) * 20,
        ...     {'face_color': 'blue'},
        ...     'points',
        ... )
        >>> viewer._add_layer_from_data(*data)

        """

        layer_type = (layer_type or '').lower()

        # assumes that big integer type arrays are likely labels.
        if not layer_type:
            layer_type = guess_labels(data)

        if layer_type not in layers.NAMES:
            raise ValueError(
                f"Unrecognized layer_type: '{layer_type}'. "
                f"Must be one of: {layers.NAMES}."
            )

        try:
            add_method = getattr(self, 'add_' + layer_type)
        except AttributeError:
            raise NotImplementedError(
                f"Sorry! {layer_type} is a valid layer type, but there is no "
                f"viewer.add_{layer_type} available yet."
            )

        try:
            layer = add_method(data, **(meta or {}))
        except TypeError as exc:
            if 'unexpected keyword argument' in str(exc):
                bad_key = str(exc).split('keyword argument ')[-1]
                raise TypeError(
                    "_add_layer_from_data received an unexpected keyword "
                    f"argument ({bad_key}) for layer type {layer_type}"
                ) from exc
            else:
                raise exc

        return layer


@lru_cache(maxsize=1)
def valid_add_kwargs() -> Dict[str, Set[str]]:
    """Return a dict where keys are layer types & values are valid kwargs."""
    valid = dict()
    for meth in dir(AddLayersMixin):
        if not meth.startswith('add_') or meth[4:] == 'layer':
            continue
        params = inspect.signature(getattr(AddLayersMixin, meth)).parameters
        valid[meth[4:]] = set(params) - {'self', 'kwargs'}
    return valid


def _normalize_layer_data(data: LayerData) -> FullLayerData:
    """Accepts any layerdata tuple, and returns a fully qualified tuple.

    Parameters
    ----------
    data : LayerData
        1-, 2-, or 3-tuple with (data, meta, layer_type).

    Returns
    -------
    FullLayerData
        3-tuple with (data, meta, layer_type)

    Raises
    ------
    ValueError
        If data has len < 1 or len > 3, or if the second item in ``data`` is
        not a ``dict``, or the third item is not a valid layer_type ``str``
    """
    if not isinstance(data, tuple) and 0 < len(data) < 4:
        raise ValueError("LayerData must be a 1-, 2-, or 3-tuple")
    _data = list(data)
    if len(_data) > 1:
        if not isinstance(_data[1], dict):
            raise ValueError(
                "The second item in a LayerData tuple must be a dict"
            )
    else:
        _data.append(dict())
    if len(_data) > 2:
        if _data[2] not in layers.NAMES:
            raise ValueError(
                "The third item in a LayerData tuple must be one of: "
                f"{layers.NAMES!r}."
            )
    else:
        _data.append(guess_labels(_data[0]))
    return tuple(_data)  # type: ignore


def _unify_data_and_user_kwargs(
    data: LayerData,
    kwargs: Optional[dict] = None,
    layer_type: Optional[str] = None,
    fallback_name: str = None,
) -> FullLayerData:
    """Merge data returned from plugins with options specified by user.

    If ``data == (_data, _meta, _type)``.  Then:

    - ``kwargs`` will be used to update ``_meta``
    - ``layer_type`` will replace ``_type`` and, if provided, ``_meta`` keys
        will be pruned to layer_type-appropriate kwargs
    - ``fallback_name`` is used if ``not _meta.get('name')``

    .. note:

        If a user specified both layer_type and additional keyword arguments
        to viewer.open(), it is their responsibility to make sure the kwargs
        match the layer_type.

    Parameters
    ----------
    data : LayerData
        1-, 2-, or 3-tuple with (data, meta, layer_type) returned from plugin.
    kwargs : dict, optional
        User-supplied keyword arguments, to override those in ``meta`` supplied
        by plugins.
    layer_type : str, optional
        A user-supplied layer_type string, to override the ``layer_type``
        declared by the plugin.
    fallback_name : str, optional
        A name for the layer, to override any name in ``meta`` supplied by the
        plugin.

    Returns
    -------
    FullLayerData
        Fully qualified LayerData tuple with user-provided overrides.
    """
    _data, _meta, _type = _normalize_layer_data(data)

    if layer_type:
        # the user has explicitly requested this be a certain layer type
        # strip any kwargs from the plugin that are no longer relevant
        _meta = prune_kwargs(_meta, layer_type)
        _type = layer_type

    if kwargs:
        # if user provided kwargs, use to override any meta dict values that
        # were returned by the plugin. We only prune kwargs if the user did
        # *not* specify the layer_type. This means that if a user specified
        # both layer_type and additional keyword arguments to viewer.open(),
        # it is their responsibility to make sure the kwargs match the
        # layer_type.
        _meta.update(prune_kwargs(kwargs, _type) if not layer_type else kwargs)

    if not _meta.get('name') and fallback_name:
        _meta['name'] = fallback_name
    return (_data, _meta, _type)


def prune_kwargs(kwargs: Dict[str, Any], layer_type: str) -> Dict[str, Any]:
    """Return copy of ``kwargs`` with only keys valid for ``add_<layer_type>``

    Parameters
    ----------
    kwargs : dict
        A key: value mapping where some or all of the keys are parameter names
        for the corresponding ``Viewer.add_<layer_type>`` method.
    layer_type : str
        The type of layer that is going to be added with these ``kwargs``.

    Returns
    -------
    pruned_kwargs : dict
        A key: value mapping where all of the keys are valid parameter names
        for the corresponding ``Viewer.add_<layer_type>`` method.

    Raises
    ------
    ValueError
        If ``AddLayersMixin`` does not provide an ``add_<layer_type>`` method
        for the provided ``layer_type``.

    Examples
    --------
    >>> test_kwargs = {
    ...     'scale': (0.75, 1),
    ...     'blending': 'additive',
    ...     'num_colors': 10,
    ... }
    >>> prune_kwargs(test_kwargs, 'image')
    {'scale': (0.75, 1), 'blending': 'additive'}

    >>> # only labels has the ``num_colors`` argument
    >>> prune_kwargs(test_kwargs, 'labels')
    {'scale': (0.75, 1), 'blending': 'additive', 'num_colors': 10}
    """
    add_method = getattr(AddLayersMixin, 'add_' + layer_type, None)
    if not add_method or layer_type == 'layer':
        raise ValueError(f"Invalid layer_type: {layer_type}")

    # get valid params for the corresponding add_<layer_type> method
    valid = valid_add_kwargs()[layer_type]
    return {k: v for k, v in kwargs.items() if k in valid}
