<filename>openpype/lib/abstract_collect_render.py
# -*- coding: utf-8 -*-
"""Collect render template.

TODO: use @dataclass when times come.

"""
from abc import abstractmethod

import attr
import six

from avalon import api
import pyblish.api

from .abstract_metaplugins import AbstractMetaContextPlugin


@attr.s
class RenderInstance(object):
    """Data collected by collectors.

    This data class later on passed to collected instances.
    Those attributes are required later on.

    """

    # metadata
    version = attr.ib()  # instance version
    time = attr.ib()  # time of instance creation (get_formatted_current_time)
    source = attr.ib()  # path to source scene file
    label = attr.ib()  # label to show in GUI
    subset = attr.ib()  # subset name
    asset = attr.ib()  # asset name (AVALON_ASSET)
    attachTo = attr.ib()  # subset name to attach render to
    setMembers = attr.ib()  # list of nodes/members producing render output
    publish = attr.ib()  # bool, True to publish instance
    name = attr.ib()  # instance name

    # format settings
    resolutionWidth = attr.ib()  # resolution width (1920)
    resolutionHeight = attr.ib()  # resolution height (1080)
    pixelAspect = attr.ib()  # pixel aspect (1.0)

    # time settings
    frameStart = attr.ib()  # start frame
    frameEnd = attr.ib()  # start end
    frameStep = attr.ib()  # frame step

    handleStart = attr.ib(default=None)  # start frame
    handleEnd = attr.ib(default=None)  # start frame

    # for software (like Harmony) where frame range cannot be set by DB
    # handles need to be propagated if exist
    ignoreFrameHandleCheck = attr.ib(default=False)

    # --------------------
    # With default values
    # metadata
    renderer = attr.ib(default="")  # renderer - can be used in Deadline
    review = attr.ib(default=False)  # generate review from instance (bool)
    priority = attr.ib(default=50)  # job priority on farm

    family = attr.ib(default="renderlayer")
    families = attr.ib(default=["renderlayer"])  # list of families

    # format settings
    multipartExr = attr.ib(default=False)  # flag for multipart exrs
    convertToScanline = attr.ib(default=False)  # flag for exr conversion

    tileRendering = attr.ib(default=False)  # bool: treat render as tiles
    tilesX = attr.ib(default=0)  # number of tiles in X
    tilesY = attr.ib(default=0)  # number of tiles in Y

    # submit_publish_job
    toBeRenderedOn = attr.ib(default=None)
    deadlineSubmissionJob = attr.ib(default=None)
    anatomyData = attr.ib(default=None)
    outputDir = attr.ib(default=None)
    context = attr.ib(default=None)

    @frameStart.validator
    def check_frame_start(self, _, value):
        """Validate if frame start is not larger then end."""
        if value > self.frameEnd:
            raise ValueError("frameStart must be smaller "
                             "or equal then frameEnd")

    @frameEnd.validator
    def check_frame_end(self, _, value):
        """Validate if frame end is not less then start."""
        if value < self.frameStart:
            raise ValueError("frameEnd must be smaller "
                             "or equal then frameStart")

    @tilesX.validator
    def check_tiles_x(self, _, value):
        """Validate if tile x isn't less then 1."""
        if not self.tileRendering:
            return
        if value < 1:
            raise ValueError("tile X size cannot be less then 1")

        if value == 1 and self.tilesY == 1:
            raise ValueError("both tiles X a Y sizes are set to 1")

    @tilesY.validator
    def check_tiles_y(self, _, value):
        """Validate if tile y isn't less then 1."""
        if not self.tileRendering:
            return
        if value < 1:
            raise ValueError("tile Y size cannot be less then 1")

        if value == 1 and self.tilesX == 1:
            raise ValueError("both tiles X a Y sizes are set to 1")


@six.add_metaclass(AbstractMetaContextPlugin)
class AbstractCollectRender(pyblish.api.ContextPlugin):
    """Gather all publishable render layers from renderSetup."""

    order = pyblish.api.CollectorOrder + 0.01
    label = "Collect Render"
    sync_workfile_version = False

    def __init__(self, *args, **kwargs):
        """Constructor."""
        super(AbstractCollectRender, self).__init__(*args, **kwargs)
        self._file_path = None
        self._asset = api.Session["AVALON_ASSET"]
        self._context = None

    def process(self, context):
        """Entry point to collector."""
        self._context = context
        for instance in context:
            # make sure workfile instance publishing is enabled
            try:
                if "workfile" in instance.data["families"]:
                    instance.data["publish"] = True
                if "renderFarm" in instance.data["families"]:
                    instance.data["remove"] = True
            except KeyError:
                # be tolerant if 'families' is missing.
                pass

        self._file_path = context.data["currentFile"].replace("\\", "/")

        render_instances = self.get_instances(context)
        for render_instance in render_instances:
            exp_files = self.get_expected_files(render_instance)
            assert exp_files, "no file names were generated, this is bug"

            # if we want to attach render to subset, check if we have AOV's
            # in expectedFiles. If so, raise error as we cannot attach AOV
            # (considered to be subset on its own) to another subset
            if render_instance.attachTo:
                assert isinstance(exp_files, list), (
                    "attaching multiple AOVs or renderable cameras to "
                    "subset is not supported"
                )

            frame_start_render = int(render_instance.frameStart)
            frame_end_render = int(render_instance.frameEnd)
            if (render_instance.ignoreFrameHandleCheck or
                    int(context.data['frameStartHandle']) == frame_start_render
                    and int(context.data['frameEndHandle']) == frame_end_render):  # noqa: W503, E501

                handle_start = context.data['handleStart']
                handle_end = context.data['handleEnd']
                frame_start = context.data['frameStart']
                frame_end = context.data['frameEnd']
                frame_start_handle = context.data['frameStartHandle']
                frame_end_handle = context.data['frameEndHandle']
            else:
                handle_start = 0
                handle_end = 0
                frame_start = frame_start_render
                frame_end = frame_end_render
                frame_start_handle = frame_start_render
                frame_end_handle = frame_end_render

            data = {
                "handleStart": handle_start,
                "handleEnd": handle_end,
                "frameStart": frame_start,
                "frameEnd": frame_end,
                "frameStartHandle": frame_start_handle,
                "frameEndHandle": frame_end_handle,
                "byFrameStep": int(render_instance.frameStep),

                "author": context.data["user"],
                # Add source to allow tracing back to the scene from
                # which was submitted originally
                "expectedFiles": exp_files,
            }
            if self.sync_workfile_version:
                data["version"] = context.data["version"]

            # add additional data
            data = self.add_additional_data(data)
            render_instance_dict = attr.asdict(render_instance)

            instance = context.create_instance(render_instance.name)
            instance.data["label"] = render_instance.label
            instance.data.update(render_instance_dict)
            instance.data.update(data)

        self.post_collecting_action()

    @abstractmethod
    def get_instances(self, context):
        """Get all renderable instances and their data.

        Args:
            context (pyblish.api.Context): Context object.

        Returns:
            list of :class:`RenderInstance`: All collected renderable instances
                (like render layers, write nodes, etc.)

        """
        pass

    @abstractmethod
    def get_expected_files(self, render_instance):
        """Get list of expected files.

        Returns:
            list: expected files. This can be either simple list of files with
                their paths, or list of dictionaries, where key is name of AOV
                for example and value is list of files for that AOV.

        Example::

            ['/path/to/file.001.exr', '/path/to/file.002.exr']

            or as dictionary:

            [
                {
                    "beauty": ['/path/to/beauty.001.exr', ...],
                    "mask": ['/path/to/mask.001.exr']
                }
            ]

        """
        pass

    def add_additional_data(self, data):
        """Add additional data to collected instance.

        This can be overridden by host implementation to add custom
        additional data.

        """
        return data

    def post_collecting_action(self):
        """Execute some code after collection is done.

        This is useful for example for restoring current render layer.

        """
        pass
