"""Brain schematics plotting for glass brain functionality."""

import json
import pathlib

from matplotlib import colors, patches, transforms
from matplotlib.path import Path


def _codes_bezier(pts):
    bezier_num = len(pts)
    # Next two lines are meant to handle both Bezier 3 and 4
    path_attr = f"CURVE{bezier_num}"
    codes = [getattr(Path, path_attr)] * (bezier_num - 1)
    return [Path.MOVETO, *codes]


def _codes_segment(pts):  # noqa: ARG001
    # pts is needed for API consistency with _codes_bezier
    return [Path.MOVETO, Path.LINETO]


def _codes(atype, pts):
    dispatch = {"bezier": _codes_bezier, "segment": _codes_segment}

    return dispatch[atype](pts)


def _invert_color(color):
    """Return inverted color.

    If color is (R, G, B) it returns (1 - R, 1 - G, 1 - B). If
    'color' can not be converted to a color it is returned
    unmodified.

    """
    try:
        color_converter = colors.ColorConverter()
        color_rgb = color_converter.to_rgb(color)
        return tuple(1 - level for level in color_rgb)
    except ValueError:
        return color


def _get_mpl_patches(
    json_content, transform=None, invert_color=False, **kwargs
):
    """Walk over the json content and build a list of matplotlib patches."""
    mpl_patches = []
    kwargs_edgecolor = kwargs.pop("edgecolor", None)
    kwargs_linewidth = kwargs.pop("linewidth", None)
    for path in json_content["paths"]:
        if kwargs_edgecolor is not None:
            edgecolor = kwargs_edgecolor
        else:
            edgecolor = path["edgecolor"]
            if invert_color:
                edgecolor = _invert_color(edgecolor)
        linewidth = kwargs_linewidth or path["linewidth"]
        path_id = path["id"]

        for item in path["items"]:
            type = item["type"]
            pts = item["pts"]
            codes = _codes(type, pts)
            path = Path(pts, codes)
            patch = patches.PathPatch(
                path,
                edgecolor=edgecolor,
                linewidth=linewidth,
                facecolor="none",
                gid=path_id,
                transform=transform,
                **kwargs,
            )

            mpl_patches.append(patch)

    return mpl_patches


def _get_json_and_transform(direction):
    """Return the json filename and an affine transform, which has \
    been tweaked by hand to fit the MNI template.
    """
    direction_to_view_name = {
        "x": "side",
        "y": "back",
        "z": "top",
        "l": "side",
        "r": "side",
    }

    direction_to_transform_params = {
        "x": [0.38, 0, 0, 0.38, -108, -70],
        "y": [0.39, 0, 0, 0.39, -73, -73],
        "z": [0.36, 0, 0, 0.37, -71, -107],
        "l": [0.38, 0, 0, 0.38, -108, -70],
        "r": [0.38, 0, 0, 0.38, -108, -70],
    }

    dirname = pathlib.Path(__file__).resolve().parent / "glass_brain_files"
    direction_to_filename = {
        _direction: dirname / f"brain_schematics_{view_name}.json"
        for _direction, view_name in direction_to_view_name.items()
    }

    direction_to_transforms = {
        _direction: transforms.Affine2D.from_values(*params)
        for _direction, params in direction_to_transform_params.items()
    }

    direction_to_json_and_transform = {
        _direction: (
            direction_to_filename[_direction],
            direction_to_transforms[_direction],
        )
        for _direction in direction_to_filename
    }

    filename_and_transform = direction_to_json_and_transform.get(direction)

    if filename_and_transform is None:
        message = (
            f"No glass brain view associated with direction '{direction}'. "
            "Possible directions are "
            f"{list(direction_to_json_and_transform.keys())}"
        )
        raise ValueError(message)

    return filename_and_transform


def _get_object_bounds(json_content, transform):
    xmin, xmax, ymin, ymax = json_content["metadata"]["bounds"]
    x0, y0 = transform.transform((xmin, ymin))
    x1, y1 = transform.transform((xmax, ymax))

    xmin, xmax = min(x0, x1), max(x0, x1)
    ymin, ymax = min(y0, y1), max(y0, y1)

    # A combination of a proportional factor (fraction of the drawing)
    # and a guestimate of the linewidth
    xmargin = (xmax - xmin) * 0.025 + 0.1
    ymargin = (ymax - ymin) * 0.025 + 0.1
    return xmin - xmargin, xmax + xmargin, ymin - ymargin, ymax + ymargin


def plot_brain_schematics(ax, direction, **kwargs):
    """Create matplotlib patches from a json custom format and plot them \
    on a matplotlib Axes.

    Parameters
    ----------
    ax : A MPL axes instance
        The axes in which the plots will be drawn.

    direction : {'x', 'y', 'z', 'l', 'r'}
        The directions of the view.

    **kwargs :
        Passed to the matplotlib patches constructor.

    Returns
    -------
    object_bounds : (xmin, xmax, ymin, ymax) tuple
        Useful for the caller to be able to set axes limits.

    """
    get_axis_bg_color = ax.get_facecolor()

    black_bg = colors.colorConverter.to_rgba(
        get_axis_bg_color
    ) == colors.colorConverter.to_rgba("k")

    json_filename, transform = _get_json_and_transform(direction)
    with json_filename.open() as json_file:
        json_content = json.load(json_file)

    mpl_patches = _get_mpl_patches(
        json_content,
        transform=transform + ax.transData,
        invert_color=black_bg,
        **kwargs,
    )

    for mpl_patch in mpl_patches:
        ax.add_patch(mpl_patch)

    object_bounds = _get_object_bounds(json_content, transform)

    return object_bounds
