# -*- coding: utf-8 -*-
"""Module handling expected render output from Maya.

This module is used in :mod:`collect_render` and :mod:`collect_vray_scene`.

Note:
    To implement new renderer, just create new class inheriting from
    :class:`AExpectedFiles` and add it to :func:`ExpectedFiles.get()`.

Attributes:
    R_SINGLE_FRAME (:class:`re.Pattern`): Find single frame number.
    R_FRAME_RANGE (:class:`re.Pattern`): Find frame range.
    R_FRAME_NUMBER (:class:`re.Pattern`): Find frame number in string.
    R_LAYER_TOKEN (:class:`re.Pattern`): Find layer token in image prefixes.
    R_AOV_TOKEN (:class:`re.Pattern`): Find AOV token in image prefixes.
    R_SUBSTITUTE_AOV_TOKEN (:class:`re.Pattern`): Find and substitute AOV token
        in image prefixes.
    R_REMOVE_AOV_TOKEN (:class:`re.Pattern`): Find and remove AOV token in
        image prefixes.
    R_CLEAN_FRAME_TOKEN (:class:`re.Pattern`): Find and remove unfilled
        Renderman frame token in image prefix.
    R_CLEAN_EXT_TOKEN (:class:`re.Pattern`): Find and remove unfilled Renderman
        extension token in image prefix.
    R_SUBSTITUTE_LAYER_TOKEN (:class:`re.Pattern`): Find and substitute render
        layer token in image prefixes.
    R_SUBSTITUTE_SCENE_TOKEN (:class:`re.Pattern`): Find and substitute scene
        token in image prefixes.
    R_SUBSTITUTE_CAMERA_TOKEN (:class:`re.Pattern`): Find and substitute camera
        token in image prefixes.
    RENDERER_NAMES (dict): Renderer names mapping between reported name and
        *human readable* name.
    ImagePrefixes (dict): Mapping between renderers and their respective
        image prefix atrribute names.

Todo:
    Determine `multipart` from render instance.

"""

import types
import re
import os
from abc import ABCMeta, abstractmethod

import six

import pype.hosts.maya.lib as lib

from maya import cmds
import maya.app.renderSetup.model.renderSetup as renderSetup


R_SINGLE_FRAME = re.compile(r"^(-?)\d+$")
R_FRAME_RANGE = re.compile(r"^(?P<sf>(-?)\d+)-(?P<ef>(-?)\d+)$")
R_FRAME_NUMBER = re.compile(r".+\.(?P<frame>[0-9]+)\..+")
R_LAYER_TOKEN = re.compile(
    r".*((?:%l)|(?:<layer>)|(?:<renderlayer>)).*", re.IGNORECASE
)
R_AOV_TOKEN = re.compile(r".*%a.*|.*<aov>.*|.*<renderpass>.*", re.IGNORECASE)
R_SUBSTITUTE_AOV_TOKEN = re.compile(r"%a|<aov>|<renderpass>", re.IGNORECASE)
R_REMOVE_AOV_TOKEN = re.compile(r"_%a|_<aov>|_<renderpass>", re.IGNORECASE)
# to remove unused renderman tokens
R_CLEAN_FRAME_TOKEN = re.compile(r"\.?<f\d>\.?", re.IGNORECASE)
R_CLEAN_EXT_TOKEN = re.compile(r"\.?<ext>\.?", re.IGNORECASE)

R_SUBSTITUTE_LAYER_TOKEN = re.compile(
    r"%l|<layer>|<renderlayer>", re.IGNORECASE
)
R_SUBSTITUTE_CAMERA_TOKEN = re.compile(r"%c|<camera>", re.IGNORECASE)
R_SUBSTITUTE_SCENE_TOKEN = re.compile(r"%s|<scene>", re.IGNORECASE)

RENDERER_NAMES = {
    "mentalray": "MentalRay",
    "vray": "V-Ray",
    "arnold": "Arnold",
    "renderman": "Renderman",
    "redshift": "Redshift",
}

# not sure about the renderman image prefix
ImagePrefixes = {
    "mentalray": "defaultRenderGlobals.imageFilePrefix",
    "vray": "vraySettings.fileNamePrefix",
    "arnold": "defaultRenderGlobals.imageFilePrefix",
    "renderman": "rmanGlobals.imageFileFormat",
    "redshift": "defaultRenderGlobals.imageFilePrefix",
}


class ExpectedFiles:
    """Class grouping functionality for all supported renderers.

    Attributes:
        multipart (bool): Flag if multipart exrs are used.

    """

    multipart = False

    def __init__(self, render_instance):
        """Constructor."""
        self._render_instance = render_instance

    def get(self, renderer, layer):
        """Get expected files for given renderer and render layer.

        Args:
            renderer (str): Name of renderer
            layer (str): Name of render layer

        Returns:
            dict: Expected rendered files by AOV

        Raises:
            :exc:`UnsupportedRendererException`: If requested renderer
                is not supported. It needs to be implemented by extending
                :class:`AExpectedFiles` and added to this methods ``if``
                statement.

        """
        renderSetup.instance().switchToLayerUsingLegacyName(layer)

        if renderer.lower() == "arnold":
            return self._get_files(ExpectedFilesArnold(layer,
                                                       self._render_instance))
        elif renderer.lower() == "vray":
            return self._get_files(ExpectedFilesVray(
                layer, self._render_instance))
        elif renderer.lower() == "redshift":
            return self._get_files(ExpectedFilesRedshift(
                layer, self._render_instance))
        elif renderer.lower() == "mentalray":
            return self._get_files(ExpectedFilesMentalray(
                layer, self._render_instance))
        elif renderer.lower() == "renderman":
            return self._get_files(ExpectedFilesRenderman(
                layer, self._render_instance))
        else:
            raise UnsupportedRendererException(
                "unsupported {}".format(renderer)
            )

    def _get_files(self, renderer):
        files = renderer.get_files()
        self.multipart = renderer.multipart
        return files


@six.add_metaclass(ABCMeta)
class AExpectedFiles:
    """Abstract class with common code for all renderers.

    Attributes:
        renderer (str): name of renderer.
        layer (str): name of render layer.
        multipart (bool): flag for multipart exrs.

    """

    renderer = None
    layer = None
    multipart = False

    def __init__(self, layer, render_instance):
        """Constructor."""
        self.layer = layer
        self.render_instance = render_instance

    @abstractmethod
    def get_aovs(self):
        """To be implemented by renderer class."""
        pass

    def sanitize_camera_name(self, camera):
        """Sanitize camera name.

        Remove Maya illegal characters from camera name.

        Args:
            camera (str): Maya camera name.

        Returns:
            (str): sanitized camera name

        Example:
            >>> sanizite_camera_name('test:camera_01')
            test_camera_01

        """
        sanitized = re.sub('[^0-9a-zA-Z_]+', '_', camera)
        return sanitized

    def get_renderer_prefix(self):
        """Return prefix for specific renderer.

        This is for most renderers the same and can be overriden if needed.

        Returns:
            str: String with image prefix containing tokens

        Raises:
            :exc:`UnsupportedRendererException`: If we requested image
                prefix for renderer we know nothing about.
                See :data:`ImagePrefixes` for mapping of renderers and
                image prefixes.

        """
        try:
            file_prefix = cmds.getAttr(ImagePrefixes[self.renderer])
        except KeyError:
            raise UnsupportedRendererException(
                "Unsupported renderer {}".format(self.renderer)
            )
        return file_prefix

    def _get_layer_data(self):
        #                      ______________________________________________
        # ____________________/ ____________________________________________/
        # 1 -  get scene name  /__________________/
        # ____________________/
        scene_dir, scene_basename = os.path.split(cmds.file(q=True, loc=True))
        scene_name, _ = os.path.splitext(scene_basename)

        #                      ______________________________________________
        # ____________________/ ____________________________________________/
        # 2 -  detect renderer /__________________/
        # ____________________/
        renderer = self.renderer

        #                    ________________________________________________
        # __________________/ ______________________________________________/
        # 3 -  image prefix  /__________________/
        # __________________/
        file_prefix = self.get_renderer_prefix()

        if not file_prefix:
            raise RuntimeError("Image prefix not set")

        default_ext = cmds.getAttr("defaultRenderGlobals.imfPluginKey")

        #                    ________________________________________________
        # __________________/ ______________________________________________/
        # 4 -  get renderable cameras_____________/
        # __________________/

        # if we have <camera> token in prefix path we'll expect output for
        # every renderable camera in layer.

        renderable_cameras = self.get_renderable_cameras()
        #                    ________________________________________________
        # __________________/ ______________________________________________/
        # 5 -  get AOVs      /____________________/
        # __________________/

        enabled_aovs = self.get_aovs()

        layer_name = self.layer
        if self.layer.startswith("rs_"):
            layer_name = self.layer[3:]
        start_frame = int(self.get_render_attribute("startFrame"))
        end_frame = int(self.get_render_attribute("endFrame"))
        frame_step = int(self.get_render_attribute("byFrameStep"))
        padding = int(self.get_render_attribute("extensionPadding"))

        scene_data = {
            "frameStart": start_frame,
            "frameEnd": end_frame,
            "frameStep": frame_step,
            "padding": padding,
            "cameras": renderable_cameras,
            "sceneName": scene_name,
            "layerName": layer_name,
            "renderer": renderer,
            "defaultExt": default_ext,
            "filePrefix": file_prefix,
            "enabledAOVs": enabled_aovs,
        }
        return scene_data

    def _generate_single_file_sequence(self, layer_data):
        expected_files = []
        for cam in layer_data["cameras"]:
            file_prefix = layer_data["filePrefix"]
            mappings = (
                (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]),
                (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]),
                (R_SUBSTITUTE_CAMERA_TOKEN, self.sanitize_camera_name(cam)),
                # this is required to remove unfilled aov token, for example
                # in Redshift
                (R_REMOVE_AOV_TOKEN, ""),
                (R_CLEAN_FRAME_TOKEN, ""),
                (R_CLEAN_EXT_TOKEN, ""),
            )

            for regex, value in mappings:
                file_prefix = re.sub(regex, value, file_prefix)

            for frame in range(
                int(layer_data["frameStart"]),
                int(layer_data["frameEnd"]) + 1,
                int(layer_data["frameStep"]),
            ):
                expected_files.append(
                    "{}.{}.{}".format(
                        file_prefix,
                        str(frame).rjust(layer_data["padding"], "0"),
                        layer_data["defaultExt"],
                    )
                )
        return expected_files

    def _generate_aov_file_sequences(self, layer_data):
        expected_files = []
        aov_file_list = {}
        for aov in layer_data["enabledAOVs"]:
            for cam in layer_data["cameras"]:
                file_prefix = layer_data["filePrefix"]

                mappings = (
                    (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]),
                    (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]),
                    (R_SUBSTITUTE_CAMERA_TOKEN,
                     self.sanitize_camera_name(cam)),
                    (R_SUBSTITUTE_AOV_TOKEN, aov[0]),
                    (R_CLEAN_FRAME_TOKEN, ""),
                    (R_CLEAN_EXT_TOKEN, ""),
                )

                for regex, value in mappings:
                    file_prefix = re.sub(regex, value, file_prefix)

                aov_files = []
                for frame in range(
                    int(layer_data["frameStart"]),
                    int(layer_data["frameEnd"]) + 1,
                    int(layer_data["frameStep"]),
                ):
                    aov_files.append(
                        "{}.{}.{}".format(
                            file_prefix,
                            str(frame).rjust(layer_data["padding"], "0"),
                            aov[1],
                        )
                    )

                # if we have more then one renderable camera, append
                # camera name to AOV to allow per camera AOVs.
                aov_name = aov[0]
                if len(layer_data["cameras"]) > 1:
                    aov_name = "{}_{}".format(aov[0],
                                              self.sanitize_camera_name(cam))

                aov_file_list[aov_name] = aov_files
                file_prefix = layer_data["filePrefix"]

        expected_files.append(aov_file_list)
        return expected_files

    def get_files(self):
        """Return list of expected files.

        It will translate render token strings  ('<RenderPass>', etc.) to
        their values. This task is tricky as every renderer deals with this
        differently. It depends on `get_aovs()` abstract method implemented
        for every supported renderer.

        """
        layer_data = self._get_layer_data()

        expected_files = []
        if layer_data.get("enabledAOVs"):
            expected_files = self._generate_aov_file_sequences(layer_data)
        else:
            expected_files = self._generate_single_file_sequence(layer_data)

        return expected_files

    def get_renderable_cameras(self):
        """Get all renderable cameras.

        Returns:
            list: list of renderable cameras.

        """
        cam_parents = [
            cmds.listRelatives(x, ap=True)[-1] for x in cmds.ls(cameras=True)
        ]

        renderable_cameras = []
        for cam in cam_parents:
            renderable = False
            if self.maya_is_true(cmds.getAttr("{}.renderable".format(cam))):
                renderable = True
                renderable_cameras.append(cam)

        return renderable_cameras

    def maya_is_true(self, attr_val):
        """Whether a Maya attr evaluates to True.

        When querying an attribute value from an ambiguous object the
        Maya API will return a list of values, which need to be properly
        handled to evaluate properly.

        Args:
            attr_val (mixed): Maya attribute to be evaluated as bool.

        Returns:
            bool: cast Maya attribute to Pythons boolean value.

        """
        if isinstance(attr_val, types.BooleanType):
            return attr_val
        elif isinstance(attr_val, (types.ListType, types.GeneratorType)):
            return any(attr_val)
        else:
            return bool(attr_val)

    def get_layer_overrides(self, attr, layer):
        """Get overrides for attribute on given render layer.

        Args:
            attr (str): Maya attribute name.
            layer (str): Maya render layer name.

        Returns:
            Value of attribute override.

        """
        connections = cmds.listConnections(attr, plugs=True)
        if connections:
            for connection in connections:
                if connection:
                    # node_name = connection.split(".")[0]

                    attr_name = "%s.value" % ".".join(
                        connection.split(".")[:-1]
                    )
                    yield cmds.getAttr(attr_name)

    def get_render_attribute(self, attr):
        """Get attribute from render options.

        Args:
            attr (str): name of attribute to be looked up.

        Returns:
            Attribute value

        """
        return lib.get_attr_in_layer(
            "defaultRenderGlobals.{}".format(attr), layer=self.layer
        )


class ExpectedFilesArnold(AExpectedFiles):
    """Expected files for Arnold renderer.

    Attributes:
        aiDriverExtension (dict): Arnold AOV driver extension mapping.
            Is there a better way?
        renderer (str): name of renderer.

    """

    aiDriverExtension = {
        "jpeg": "jpg",
        "exr": "exr",
        "deepexr": "exr",
        "png": "png",
        "tiff": "tif",
        "mtoa_shaders": "ass",  # TODO: research what those last two should be
        "maya": "",
    }

    def __init__(self, layer, render_instance):
        """Constructor."""
        super(ExpectedFilesArnold, self).__init__(layer, render_instance)
        self.renderer = "arnold"

    def get_aovs(self):
        """Get all AOVs.

        See Also:
            :func:`AExpectedFiles.get_aovs()`

        Raises:
            :class:`AOVError`: If AOV cannot be determined.

        """
        enabled_aovs = []
        try:
            if not (
                cmds.getAttr("defaultArnoldRenderOptions.aovMode")
                and not cmds.getAttr("defaultArnoldDriver.mergeAOVs")  # noqa: W503, E501
            ):
                # AOVs are merged in mutli-channel file
                self.multipart = True
                return enabled_aovs
        except ValueError:
            # this occurs when Render Setting windows was not opened yet. In
            # such case there are no Arnold options created so query for AOVs
            # will fail. We terminate here as there are no AOVs specified then.
            # This state will most probably fail later on some Validator
            # anyway.
            return enabled_aovs

        # AOVs are set to be rendered separately. We should expect
        # <RenderPass> token in path.

        ai_aovs = [n for n in cmds.ls(type="aiAOV")]

        for aov in ai_aovs:
            enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov)))
            ai_driver = cmds.listConnections("{}.outputs".format(aov))[0]
            ai_translator = cmds.getAttr("{}.aiTranslator".format(ai_driver))
            try:
                aov_ext = self.aiDriverExtension[ai_translator]
            except KeyError:
                msg = (
                    "Unrecognized arnold " "driver format for AOV - {}"
                ).format(cmds.getAttr("{}.name".format(aov)))
                raise AOVError(msg)

            for override in self.get_layer_overrides(
                "{}.enabled".format(aov), self.layer
            ):
                enabled = self.maya_is_true(override)
            if enabled:
                # If aov RGBA is selected, arnold will translate it to `beauty`
                aov_name = cmds.getAttr("%s.name" % aov)
                if aov_name == "RGBA":
                    aov_name = "beauty"
                enabled_aovs.append((aov_name, aov_ext))
        # Append 'beauty' as this is arnolds
        # default. If <RenderPass> token is specified and no AOVs are
        # defined, this will be used.
        enabled_aovs.append(
            (u"beauty", cmds.getAttr("defaultRenderGlobals.imfPluginKey"))
        )
        return enabled_aovs


class ExpectedFilesVray(AExpectedFiles):
    """Expected files for V-Ray renderer."""

    def __init__(self, layer, render_instance):
        """Constructor."""
        super(ExpectedFilesVray, self).__init__(layer, render_instance)
        self.renderer = "vray"

    def get_renderer_prefix(self):
        """Get image prefix for V-Ray.

        This overrides :func:`AExpectedFiles.get_renderer_prefix()` as
        we must add `<aov>` token manually.

        See also:
            :func:`AExpectedFiles.get_renderer_prefix()`

        """
        prefix = super(ExpectedFilesVray, self).get_renderer_prefix()
        prefix = "{}_<aov>".format(prefix)
        return prefix

    def _get_layer_data(self):
        """Override to get vray specific extension."""
        layer_data = super(ExpectedFilesVray, self)._get_layer_data()
        default_ext = cmds.getAttr("vraySettings.imageFormatStr")
        if default_ext == "exr (multichannel)" or default_ext == "exr (deep)":
            default_ext = "exr"
        layer_data["defaultExt"] = default_ext
        layer_data["padding"] = cmds.getAttr("vraySettings.fileNamePadding")
        return layer_data

    def get_files(self):
        """Get expected files.

        This overrides :func:`AExpectedFiles.get_files()` as we
        we need to add one sequence for plain beauty if AOVs are enabled
        as vray output beauty without 'beauty' in filename.

        """
        expected_files = super(ExpectedFilesVray, self).get_files()

        layer_data = self._get_layer_data()
        # remove 'beauty' from filenames as vray doesn't output it
        update = {}
        if layer_data.get("enabledAOVs"):
            for aov, seq in expected_files[0].items():
                if aov.startswith("beauty"):
                    new_list = []
                    for f in seq:
                        new_list.append(f.replace("_beauty", ""))
                    update[aov] = new_list

            expected_files[0].update(update)
        return expected_files

    def get_aovs(self):
        """Get all AOVs.

        See Also:
            :func:`AExpectedFiles.get_aovs()`

        """
        enabled_aovs = []

        try:
            # really? do we set it in vray just by selecting multichannel exr?
            if (
                cmds.getAttr("vraySettings.imageFormatStr")
                == "exr (multichannel)"  # noqa: W503
            ):
                # AOVs are merged in mutli-channel file
                self.multipart = True
                return enabled_aovs
        except ValueError:
            # this occurs when Render Setting windows was not opened yet. In
            # such case there are no VRay options created so query for AOVs
            # will fail. We terminate here as there are no AOVs specified then.
            # This state will most probably fail later on some Validator
            # anyway.
            return enabled_aovs

        default_ext = cmds.getAttr("vraySettings.imageFormatStr")
        if default_ext == "exr (multichannel)" or default_ext == "exr (deep)":
            default_ext = "exr"

        # add beauty as default
        enabled_aovs.append(
            (u"beauty", default_ext)
        )

        # handle aovs from references
        use_ref_aovs = self.render_instance.data.get(
            "vrayUseReferencedAovs", False) or False

        # this will have list of all aovs no matter if they are coming from
        # reference or not.
        vr_aovs = cmds.ls(
            type=["VRayRenderElement", "VRayRenderElementSet"]) or []
        if not use_ref_aovs:
            ref_aovs = cmds.ls(
                type=["VRayRenderElement", "VRayRenderElementSet"],
                referencedNodes=True) or []
            # get difference
            vr_aovs = list(set(vr_aovs) - set(ref_aovs))

        for aov in vr_aovs:
            enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov)))
            for override in self.get_layer_overrides(
                "{}.enabled".format(aov), "rs_{}".format(self.layer)
            ):
                enabled = self.maya_is_true(override)

            if enabled:
                # todo: find how vray set format for AOVs
                enabled_aovs.append(
                    (self._get_vray_aov_name(aov), default_ext))

        return enabled_aovs

    def _get_vray_aov_name(self, node):
        """Get AOVs name from Vray.

        Args:
            node (str): aov node name.

        Returns:
            str: aov name.

        """
        vray_name = None
        vray_explicit_name = None
        vray_file_name = None
        for attr in cmds.listAttr(node):
            if attr.startswith("vray_filename"):
                vray_file_name = cmds.getAttr("{}.{}".format(node, attr))
            elif attr.startswith("vray_name"):
                vray_name = cmds.getAttr("{}.{}".format(node, attr))
            elif attr.startswith("vray_explicit_name"):
                vray_explicit_name = cmds.getAttr("{}.{}".format(node, attr))

            if vray_file_name is not None and vray_file_name != "":
                final_name = vray_file_name
            elif vray_explicit_name is not None and vray_explicit_name != "":
                final_name = vray_explicit_name
            elif vray_name is not None and vray_name != "":
                final_name = vray_name
            else:
                continue
            # special case for Material Select elements - these are named
            # based on the materia they are connected to.
            if "vray_mtl_mtlselect" in cmds.listAttr(node):
                connections = cmds.listConnections(
                    "{}.vray_mtl_mtlselect".format(node))
                if connections:
                    final_name += '_{}'.format(str(connections[0]))

            return final_name


class ExpectedFilesRedshift(AExpectedFiles):
    """Expected files for Redshift renderer.

    Attributes:
        ext_mapping (list): Mapping redshift extension dropdown values
            to strings.

        unmerged_aovs (list): Name of aovs that are not merged into resulting
            exr and we need them specified in expectedFiles output.

    """

    unmerged_aovs = ["Cryptomatte"]

    ext_mapping = ["iff", "exr", "tif", "png", "tga", "jpg"]

    def __init__(self, layer, render_instance):
        """Construtor."""
        super(ExpectedFilesRedshift, self).__init__(layer, render_instance)
        self.renderer = "redshift"

    def get_renderer_prefix(self):
        """Get image prefix for Redshift.

        This overrides :func:`AExpectedFiles.get_renderer_prefix()` as
        we must add `<aov>` token manually.

        See also:
            :func:`AExpectedFiles.get_renderer_prefix()`

        """
        prefix = super(ExpectedFilesRedshift, self).get_renderer_prefix()
        prefix = "{}_<aov>".format(prefix)
        return prefix

    def get_files(self):
        """Get expected files.

        This overrides :func:`AExpectedFiles.get_files()` as we
        we need to add one sequence for plain beauty if AOVs are enabled
        as vray output beauty without 'beauty' in filename.

        """
        expected_files = super(ExpectedFilesRedshift, self).get_files()

        # we need to add one sequence for plain beauty if AOVs are enabled.
        # as redshift output beauty without 'beauty' in filename.

        layer_data = self._get_layer_data()
        if layer_data.get("enabledAOVs"):
            expected_files[0][u"beauty"] = self._generate_single_file_sequence(
                layer_data
            )

        # Redshift doesn't merge Cryptomatte AOV to final exr. We need to check
        # for such condition and add it to list of expected files.

        for aov in layer_data.get("enabledAOVs"):
            if aov[0].lower() == "cryptomatte":
                aov_name = aov[0]
                expected_files.append(
                    {aov_name: self._generate_single_file_sequence(
                        layer_data, aov_name=aov_name)})

        return expected_files

    def get_aovs(self):
        """Get all AOVs.

        See Also:
            :func:`AExpectedFiles.get_aovs()`

        """
        enabled_aovs = []

        try:
            if self.maya_is_true(
                cmds.getAttr("redshiftOptions.exrForceMultilayer")
            ):
                # AOVs are merged in mutli-channel file
                self.multipart = True
                return enabled_aovs
        except ValueError:
            # this occurs when Render Setting windows was not opened yet. In
            # such case there are no Redshift options created so query for AOVs
            # will fail. We terminate here as there are no AOVs specified then.
            # This state will most probably fail later on some Validator
            # anyway.
            return enabled_aovs

        default_ext = self.ext_mapping[
            cmds.getAttr("redshiftOptions.imageFormat")
        ]
        rs_aovs = [n for n in cmds.ls(type="RedshiftAOV")]

        # todo: find out how to detect multichannel exr for redshift
        for aov in rs_aovs:
            enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov)))
            for override in self.get_layer_overrides(
                "{}.enabled".format(aov), self.layer
            ):
                enabled = self.maya_is_true(override)

            if enabled:
                # If AOVs are merged into multipart exr, append AOV only if it
                # is in the list of AOVs that renderer cannot (or will not)
                # merge into final exr.
                if self.maya_is_true(
                    cmds.getAttr("redshiftOptions.exrForceMultilayer")
                ):
                    if cmds.getAttr("%s.name" % aov) in self.unmerged_aovs:
                        enabled_aovs.append(
                            (cmds.getAttr("%s.name" % aov), default_ext)
                        )
                else:
                    enabled_aovs.append(
                        (cmds.getAttr("%s.name" % aov), default_ext)
                    )

        if self.maya_is_true(
            cmds.getAttr("redshiftOptions.exrForceMultilayer")
        ):
            # AOVs are merged in mutli-channel file
            self.multipart = True

        return enabled_aovs


class ExpectedFilesRenderman(AExpectedFiles):
    """Expected files for Renderman renderer.

    Warning:
        This is very rudimentary and needs more love and testing.
    """

    def __init__(self, layer, render_instance):
        """Constructor."""
        super(ExpectedFilesRenderman, self).__init__(layer, render_instance)
        self.renderer = "renderman"

    def get_aovs(self):
        """Get all AOVs.

        See Also:
            :func:`AExpectedFiles.get_aovs()`

        """
        enabled_aovs = []

        default_ext = "exr"
        displays = cmds.listConnections("rmanGlobals.displays")
        for aov in displays:
            aov_name = str(aov)
            if aov_name == "rmanDefaultDisplay":
                aov_name = "beauty"

            enabled = self.maya_is_true(cmds.getAttr("{}.enable".format(aov)))
            for override in self.get_layer_overrides(
                "{}.enable".format(aov), self.layer
            ):
                enabled = self.maya_is_true(override)

            if enabled:
                enabled_aovs.append((aov_name, default_ext))

        return enabled_aovs

    def get_files(self):
        """Get expected files.

        This overrides :func:`AExpectedFiles.get_files()` as we
        we need to add one sequence for plain beauty if AOVs are enabled
        as vray output beauty without 'beauty' in filename.

        In renderman we hack it with prepending path. This path would
        normally be translated from `rmanGlobals.imageOutputDir`. We skip
        this and hardcode prepend path we expect. There is no place for user
        to mess around with this settings anyway and it is enforced in
        render settings validator.
        """
        layer_data = self._get_layer_data()
        new_aovs = {}

        expected_files = super(ExpectedFilesRenderman, self).get_files()
        # we always get beauty
        for aov, files in expected_files[0].items():
            new_files = []
            for file in files:
                new_file = "{}/{}/{}".format(
                    layer_data["sceneName"], layer_data["layerName"], file
                )
                new_files.append(new_file)
            new_aovs[aov] = new_files

        return [new_aovs]


class ExpectedFilesMentalray(AExpectedFiles):
    """Skeleton unimplemented class for Mentalray renderer."""

    def __init__(self, layer, render_instance):
        """Constructor.

        Raises:
            :exc:`UnimplementedRendererException`: as it is not implemented.

        """
        raise UnimplementedRendererException("Mentalray not implemented")

    def get_aovs(self):
        """Get all AOVs.

        See Also:
            :func:`AExpectedFiles.get_aovs()`

        """
        return []


class AOVError(Exception):
    """Custom exception for determining AOVs."""

    pass


class UnsupportedRendererException(Exception):
    """Custom exception.

    Raised when requesting data from unsupported renderer.
    """

    pass


class UnimplementedRendererException(Exception):
    """Custom exception.

    Raised when requesting data from renderer that is not implemented yet.
    """

    pass
