import os
import re
import subprocess
from pathlib import Path

from kabaret import flow
from kabaret.app import resources
from kabaret.flow_contextual_dict import get_contextual_dict
from libreflow.baseflow.asset import AssetFamilyCollection, AssetTypeCollection
from libreflow.baseflow.file import (
    GenericRunAction,
    OpenWithBlenderAction,
    RenderBlenderPlayblast as BaseRenderBlenderPlayblast,
    PlayblastQuality,
    PresetSessionValue,
    RevisionsChoiceValue,
    TrackedFile,
    TrackedFolder,
)
from libreflow.baseflow.shot import Sequence
from libreflow.flows.default.flow.asset import Assets, AssetType
from libreflow.utils.b3d import wrap_python_expr
from libreflow.utils.os import remove_folder_content

from . import _version, scripts

__version__ = _version.get_versions()["version"]


#
#   KITSU
#


class CreateKitsuAssets(flow.Action):
    ICON = ("icons.libreflow", "kitsu")

    skip_existing = flow.SessionParam(False).ui(editor="bool")
    create_task_default_files = flow.SessionParam(True).ui(editor="bool")

    _assets = flow.Parent()
    _asset_family = flow.Parent(2)
    _asset_type = flow.Parent(4)

    def allow_context(self, context):
        return context

    def get_buttons(self):
        return ["Create assets", "Cancel"]

    def run(self, button):
        if button == "Cancel":
            return

        session = self.root().session()

        skip_existing = self.skip_existing.get()
        assets_data = (
            self.root().project().kitsu_api().get_assets_data(self._asset_type.name())
        )

        # Filter assets by family
        assets_data = [
            ad
            for ad in assets_data
            if ad["data"].get("family", None) is not None
            and ad["data"].get("family", None) == self._asset_family.name()
        ]

        for data in assets_data:
            name = data["name"]

            if not self._assets.has_mapped_name(name):
                session.log_info(f"[Create Kitsu Assets] Creating Asset {name}")
                a = self._assets.add(name)
            elif not skip_existing:
                a = self._assets[name]
                session.log_info(f"[Create Kitsu Assets] Updating Default Tasks {name}")
                a.ensure_tasks()
            else:
                continue

            if self.create_task_default_files.get():
                for t in a.tasks.mapped_items():
                    session.log_info(
                        f"[Create Kitsu Assets] Updating Default Files {name} {t.name()}"
                    )
                    t.create_dft_files.files.update()
                    t.create_dft_files.run(None)

        self._assets.touch()


class CreateKitsuAssetFamilies(flow.Action):
    ICON = ("icons.libreflow", "kitsu")

    skip_existing = flow.SessionParam(False).ui(editor="bool")
    create_assets = flow.SessionParam(False).ui(editor="bool")
    create_task_default_files = flow.SessionParam(False).ui(editor="bool")

    _asset_families = flow.Parent()
    _asset_type = flow.Parent(2)

    def allow_context(self, context):
        return context

    def get_buttons(self):
        return ["Create families", "Cancel"]

    def run(self, button):
        if button == "Cancel":
            return

        session = self.root().session()

        assets_data = (
            self.root().project().kitsu_api().get_assets_data(self._asset_type.name())
        )
        create_assets = self.create_assets.get()
        skip_existing = self.skip_existing.get()

        asset_family_names = [ad["data"].get("family", None) for ad in assets_data]

        for i, name in enumerate(asset_family_names):
            if name is None:
                continue

            if not self._asset_families.has_mapped_name(name):
                session.log_info(
                    f"[Create Kitsu Asset Families] Create Asset family {name} for Asset Type {self._asset_type.name()}"
                )
                af = self._asset_families.add(name)
            elif not skip_existing:
                session.log_info(
                    f"[Create Kitsu Asset Families] Asset family {name} exists for Asset Type {self._asset_type.name()}"
                )
                af = self._asset_families[name]
            else:
                continue

            if create_assets:
                af.assets.create_assets.skip_existing.set(skip_existing)
                af.assets.create_assets.create_task_default_files.set(
                    self.create_task_default_files.get()
                )
                af.assets.create_assets.run("Create assets")

        self._asset_families.touch()


class CreateKitsuAssetTypes(flow.Action):
    ICON = ("icons.libreflow", "kitsu")

    skip_existing = flow.SessionParam(False).ui(editor="bool")
    create_asset_families = flow.SessionParam(False).ui(editor="bool")
    create_assets = flow.SessionParam(False).ui(editor="bool")
    create_task_default_files = flow.SessionParam(False).ui(editor="bool")

    _asset_types = flow.Parent()

    def allow_context(self, context):
        return context

    def get_buttons(self):
        return ["Create asset types", "Cancel"]

    def run(self, button):
        if button == "Cancel":
            return

        session = self.root().session()

        asset_types_data = self.root().project().kitsu_api().get_asset_types_data()
        create_asset_families = self.create_asset_families.get()
        create_assets = self.create_assets.get()
        skip_existing = self.skip_existing.get()

        for data in asset_types_data:
            name = data["name"]

            if name == "x":
                continue

            # Ensure asset type
            if not self._asset_types.has_mapped_name(name):
                session.log_info(
                    f"[Create Kitsu Asset Types] Creating Asset Type {name}"
                )
                at = self._asset_types.add(name)
            elif not skip_existing:
                session.log_info(f"[Create Kitsu Asset Types] Asset Type {name} exists")
                at = self._asset_types[name]
            else:
                continue

            if create_asset_families:
                at.asset_families.create_asset_families.skip_existing.set(skip_existing)
                at.asset_families.create_asset_families.create_assets.set(create_assets)
                at.asset_families.create_asset_families.create_task_default_files.set(
                    self.create_task_default_files.get()
                )
                at.asset_families.create_asset_families.run("Create families")

        self._asset_types.touch()


#
#   BLENDER PLAYBLAST
#


class RenderBlenderPlayblast(BaseRenderBlenderPlayblast):

    auto_play_playblast = flow.SessionParam(True, PresetSessionValue).ui(
        tooltip="Play playblast when render is finished",
        editor="bool",
    )
    safety_margins = flow.SessionParam(False, PresetSessionValue).ui(editor="bool")
    with flow.group("Advanced"):
        resolution_percentage = flow.SessionParam(100)

    def check_default_values(self):
        super(RenderBlenderPlayblast, self).check_default_values()
        self.safety_margins.apply_preset()

    def update_presets(self):
        super(RenderBlenderPlayblast, self).update_presets()
        self.safety_margins.update_preset()

    def playblast_infos_from_revision(self, revision_name):
        filepath = self._file.path.get()
        filename = "_".join(self._file.name().split("_")[:-1])

        # Check if there is a AE compositing file
        if self._files.has_file("compositing", "aep"):
            playblast_filename = filename + "_movie_blend"
            playblast_revision_filename = (
                self._file.complete_name.get() + "_movie_blend.mp4"
            )
        else:
            playblast_filename = filename + "_movie"
            playblast_revision_filename = self._file.complete_name.get() + "_movie.mp4"

        if self.safety_margins.get() is True:
            playblast_filename = filename + "_movie_safety"
            playblast_revision_filename = (
                self._file.complete_name.get() + "_movie_safety.mp4"
            )

        playblast_filepath = os.path.join(
            self.root().project().get_root(),
            os.path.dirname(filepath),
            playblast_filename + "_mp4",
            revision_name,
            playblast_revision_filename,
        )

        return playblast_filepath, playblast_filename, self._file.path_format.get()

    def extra_argv(self):
        file_settings = get_contextual_dict(
            self._file, "settings", ["sequence", "shot"]
        )
        project_name = self.root().project().name()
        revision = self._file.get_revision(self.revision_name.get())
        do_render = self.quality.get() == "Final"

        python_expr = "import bpy\n"
        if self.safety_margins.get() is True:
            python_expr += """
if hasattr(bpy.context.scene, "camera_overscan"):
    bpy.context.scene.camera_overscan.activate = True
else:
    print("Camera Overscan add-on not found, margins will not be enabled.")\n"""

        python_expr += (
            "bpy.ops.lfs.playblast(filepath='%s', image_sequence_path='%s', do_render=%s, quality='%s', resolution_percentage=%s, do_autoplay=%s, studio='%s', project='%s', sequence='%s', scene='%s', version='%s', frame_count=%s)"
            % (
                self.output_path,
                Path(self.image_sequence_path).as_posix(),
                str(do_render),
                "PREVIEW" if self.quality.get() == "Viewport" else "FINAL",
                self.resolution_percentage.get(),
                self.auto_play_playblast.get(),
                self.root().project().get_current_site().name(),
                project_name,
                file_settings.get("sequence", "undefined")
                if "shots" in self._shot.oid()
                else file_settings.get("asset_type", "undefined"),
                file_settings.get("shot", "undefined")
                if "shots" in self._shot.oid()
                else file_settings.get("asset", "undefined"),
                self.revision_name.get(),
                self.get_shot_frame_count(),
            )
        )
        if not do_render:
            python_expr += "\nbpy.ops.wm.quit_blender()"

        args = [
            revision.get_path(),
            "--addons",
            "lfs_playblast",
            "--python-expr",
            wrap_python_expr(python_expr),
        ]

        if do_render:
            args.insert(0, "-b")

        return args

    def ensure_render_folder(self):
        """Create the folder for image sequence.

        Returns:
            TrackedFile: the folder created

        """
        folder_name = self._file.display_name.get().split(".")[0]
        folder_name += "_render"
        if self.safety_margins.get() is True:
            folder_name += "_safety"

        if not self._files.has_folder(folder_name):
            self._task.create_folder_action.folder_name.set(folder_name)
            self._task.create_folder_action.category.set("Outputs")
            self._task.create_folder_action.tracked.set(True)
            self._task.create_folder_action.run(None)
        return self._files[folder_name]


class ConcatenateShots(flow.Action):
    revision_name = flow.Param("", RevisionsChoiceValue)

    _file = flow.Parent()
    _files = flow.Parent(2)
    _task = flow.Parent(3)

    def needs_dialog(self):
        """Indicate whether this action requires a dialog to be displayed.

        Returns:
            bool: always True.

        """
        return True

    def get_buttons(self):
        """Return the buttons displayed in the dialog.

        Returns:
            list[str]: list of button labels, typically ['Render', 'Cancel'].

        """
        self.revision_name.revert_to_default()

        buttons = ["Render", "Cancel"]

        # if (
        #     self.root().project().get_current_site().site_type.get() == "Studio"
        #     and self.root().project().get_current_site().pool_names.get()
        # ):
        #     buttons.insert(1, "Submit job")
        return buttons

    def allow_context(self, context):
        """Check whether the given context is valid for running the action.

        Args:
            context: Context object, usually representing the current project/task.

        Returns:
            bool: Folder not empty.

        """
        return context and not self._file.is_empty()

    def ensure_movie_sequence(self):
        name = self._file.complete_name.get().replace("_render", "_movie")

        mgr = self.root().project().get_task_manager()
        dft_file = mgr.get_default_file(name, file_type="video")

        # Fallback to default mp4 container if no default file
        if dft_file is None:
            extension = "mp4"
            path_format = mgr.get_task_path_format(
                self._task.name()
            )  # get from default task
        else:
            _, extension = os.path.splitext(dft_file.file_name.get())
            extension = extension[1:]
            path_format = dft_file.path_format.get()

        mapped_name = name + "_" + extension

        if not self._files.has_mapped_name(mapped_name):
            file = self._files.add_file(
                name, extension, tracked=True, default_path_format=path_format
            )
        else:
            file = self._files[mapped_name]

        return file

    def ensure_movie_sequence_revision(self):
        file = self.ensure_movie_sequence()
        revision_name = self.revision_name.get()
        source_revision = self._file.get_revision(revision_name)

        if not file.has_revision(revision_name):
            revision = file.add_revision(revision_name)
            file.set_current_user_on_revision(revision_name)
        else:
            revision = file.get_revision(revision_name)

        revision.comment.set(source_revision.comment.get())
        file.file_type.set("Outputs")
        file.ensure_last_revision_oid()

        return revision

    @staticmethod
    def get_first_shot_name(shot_list_path):
        # Get first shot from shot list
        first_shot_file_name = None
        first_shot_path = None
        with shot_list_path.open("r") as f:
            shot_names = f.readlines()
        if not shot_names:
            return
        shot_path = shot_names[0]
        result = re.search(r"^file '(.+?)'", shot_path)
        if result is None:
            return
        return result.group(1)

    @staticmethod
    def get_frame_rate(video_path):
        result = subprocess.check_output(
            [
                "ffprobe",
                "-v",
                "error",
                "-select_streams",
                "v",
                "-show_entries",
                "stream=r_frame_rate",
                "-of",
                "csv=p=0:s=x",
                video_path,
            ]
        )
        fps, fps_base = result.decode("ascii").split("/")
        frame_rate = float(fps) / float(fps_base)
        return frame_rate

    def run(self, button):
        """Execute the render action.

        Args:
            button (str): The label of the button pressed by the user (e.g., 'Render' or 'Cancel').

        Returns:
            Any: the result of the parent run method if executed, or None if canceled.

        """
        if button == "Cancel":
            return None

        session = self.root().session()

        # Create movie file
        output_concat_path = Path(self.ensure_movie_sequence_revision().get_path())

        if output_concat_path.parent.exists():
            remove_folder_content(output_concat_path.parent)
        else:
            os.makedirs(os.path.dirname(output_concat_path), exist_ok=True)

        # Check if shots list is available
        source_revision = self._file.get_revision(self.revision_name.get())
        source_revision_path = Path(source_revision.get_path())
        shot_list_path = source_revision_path / "movies.list"

        if not os.path.isfile(shot_list_path):
            session.log_error(
                "[Concatenate Shots] Cannot found movies.list file on source revision folder"
            )
            self.message.set(
                "<font color=red>Cannot found movies.list file on source revision folder</font>"
            )
            return self.get_result(close=False)

        # Execute FFmpeg concatenate
        frame_rate = None
        first_shot_file_name = self.get_first_shot_name(shot_list_path)
        if first_shot_file_name is not None:
            first_shot_path = source_revision_path / first_shot_file_name
            frame_rate = self.get_frame_rate(first_shot_path)
        if frame_rate is None:
            frame_rate = 25
        ffmpeg_args = ["ffmpeg", "-y"]
        ffmpeg_args.extend(["-f", "concat"])
        ffmpeg_args.extend(["-safe", "0"])
        ffmpeg_args.extend(["-r", str(frame_rate)])
        ffmpeg_args.extend(["-i", shot_list_path.as_posix()])
        ffmpeg_args.extend(["-vcodec", "copy"])
        ffmpeg_args.extend(["-acodec", "copy"])
        ffmpeg_args.extend(["-r", str(frame_rate)])
        ffmpeg_args.extend([output_concat_path.as_posix()])

        os.system(" ".join(ffmpeg_args))

        return self.get_result(close=True)


class SequenceRenderPlayblast(OpenWithBlenderAction):
    """Execute the rendering of the sequence in a separate shot."""

    revision_name = flow.Param("", RevisionsChoiceValue)
    quality = flow.Param("Final", PlayblastQuality)
    safety_margins = flow.SessionParam(False, PresetSessionValue).ui(editor="bool")
    concat_shots = flow.SessionParam(True, PresetSessionValue).ui(
        tooltip="Render the entire sequence",
        editor="bool",
    )
    with flow.group("Advanced"):
        resolution_percentage = flow.SessionParam(100)

    _file = flow.Parent()
    _files = flow.Parent(2)
    _task = flow.Parent(3)
    _sequence = flow.Parent(5)

    def __init__(self, parent, name):
        super().__init__(parent, name)
        self.output_path = None
        self.output_concat_path = None
        self.file_settings = get_contextual_dict(self._file, "settings")

    def get_run_label(self):
        """Get the label to display for the run action.

        Returns:
            str: label shown on the run button.

        """
        return "Sequence render playblast - {seq} {file} {rev}".format(
            seq=self.file_settings.get("sequence", "undefined"),
            file=self.file_settings.get("file_display_name", "undefined"),
            rev=self.revision_name.get(),
        )

    def check_default_values(self):
        """Check the default values."""
        self.revision_name.revert_to_default()
        self.safety_margins.apply_preset()
        self.concat_shots.apply_preset()

    def update_presets(self):
        self.safety_margins.update_preset()
        self.concat_shots.update_preset()

    def needs_dialog(self):
        """Indicate whether this action requires a dialog to be displayed.

        Returns:
            bool: always True.

        """
        msg = self.runner_configured()
        if msg is not None:
            name, _ = self.runner_name_and_tags()
            error_txt = '<div style="color: red">Error:</div>'
            self.root().session().log_error(msg)
            self.message.set(
                (
                    f'<h3 style="font-weight: 400">{error_txt} Application {name} not configured (see terminal).'
                )
            )
            self._buttons.set(["Cancel"])

        self.check_default_values()

        return True

    def get_buttons(self):
        """Return the buttons displayed in the dialog.

        Returns:
            list[str]: list of button labels, typically ['Render', 'Cancel'].

        """
        self.revision_name.revert_to_default()

        buttons = ["Render", "Cancel"]

        # if (
        #     self.root().project().get_current_site().site_type.get() == "Studio"
        #     and self.root().project().get_current_site().pool_names.get()
        # ):
        #     buttons.insert(1, "Submit job")
        return buttons

    def allow_context(self, context):
        """Check whether the given context is valid for running the action.

        Args:
            context: Context object, usually representing the current project/task.

        Returns:
            bool: Blender file and not empty.

        """
        return (
            context
            and not self._file.is_empty()
            and self._file.format.get() in self.supported_extensions()
        )

    def ensure_render_sequence_folder(self):
        """Create the folder for movies.

        Returns:
            TrackedFolder: the folder created

        """
        folder_name = self._file.display_name.get().split(".")[0]
        folder_name += "_sequence_render"
        if self.safety_margins.get() is True:
            folder_name += "_safety"

        if not self._files.has_folder(folder_name):
            self._task.create_folder_action.folder_name.set(folder_name)
            self._task.create_folder_action.category.set("Outputs")
            self._task.create_folder_action.tracked.set(True)
            self._task.create_folder_action.run(None)

        return self._files[folder_name]

    def ensure_render_sequence_folder_revision(self):
        """Get the revision of the movies folder.

        Returns:
            Revision: revision of the movies folder

        """
        folder = self.ensure_render_sequence_folder()
        revision_name = self.revision_name.get()
        source_revision = self._file.get_revision(revision_name)

        if not folder.has_revision(revision_name):
            revision = folder.add_revision(revision_name)
            folder.set_current_user_on_revision(revision_name)
        else:
            revision = folder.get_revision(revision_name)

        revision.comment.set(source_revision.comment.get())
        folder.ensure_last_revision_oid()

        return revision

    def ensure_movie_sequence(self):
        name = f"{self._file.complete_name.get()}_sequence_movie"
        if self.safety_margins.get() is True:
            name += "_safety"

        mgr = self.root().project().get_task_manager()
        dft_file = mgr.get_default_file(name, file_type="video")

        # Fallback to default mp4 container if no default file
        if dft_file is None:
            extension = "mp4"
            path_format = mgr.get_task_path_format(
                self._task.name()
            )  # get from default task
        else:
            _, extension = os.path.splitext(dft_file.file_name.get())
            extension = extension[1:]
            path_format = dft_file.path_format.get()

        mapped_name = name + "_" + extension

        if not self._files.has_mapped_name(mapped_name):
            file = self._files.add_file(
                name, extension, tracked=True, default_path_format=path_format
            )
        else:
            file = self._files[mapped_name]

        return file

    def ensure_movie_sequence_revision(self):
        file = self.ensure_movie_sequence()
        revision_name = self.revision_name.get()
        source_revision = self._file.get_revision(revision_name)

        if not file.has_revision(revision_name):
            revision = file.add_revision(revision_name)
            file.set_current_user_on_revision(revision_name)
        else:
            revision = file.get_revision(revision_name)

        revision.comment.set(source_revision.comment.get())
        file.file_type.set("Outputs")
        file.ensure_last_revision_oid()

        return revision

    def extra_argv(self):
        """Build the list of command-line arguments required for render in Blender.

        Returns:
            list[str]: command-line arguments for the Blender render process.

        """
        segmenter_script_path = resources.get("scripts", "segmenter_playblast.py")
        with open(segmenter_script_path, "r") as f:
            python_expr = f.read()
        project_name = self.root().project().name()
        revision = self._file.get_revision(self.revision_name.get())
        do_render = self.quality.get() == "Final"  # Viewport or final rendering
        python_expr += """batch_render_playblasts(
            do_render={do_render},
            file_path=r"{file_path}",
            quality="{quality}",
            resolution_percentage={resolution_percentage},
            studio="{studio}",
            project="{project}",
            sequence="{sequence}",
            version="{version}",
            do_mark_images=True,
            video_codec='H264',
            safety_margins={safety_margins},
            concat_shots={concat_shots},
            concat_movie_path=r"{concat_movie_path}")
        """.format(
            do_render=str(do_render),
            file_path=self.output_path.as_posix(),
            quality="PREVIEW" if self.quality.get() == "Viewport" else "FINAL",
            resolution_percentage=self.resolution_percentage.get(),
            studio=self.root().project().get_current_site().name(),
            project=project_name,
            sequence=self._sequence.name(),
            version=self.revision_name.get(),
            safety_margins=self.safety_margins.get(),
            concat_shots=self.concat_shots.get(),
            concat_movie_path=self.output_concat_path.as_posix()
            if self.output_concat_path is not None
            else None,
        )
        if not do_render:
            python_expr += "\nbpy.ops.wm.quit_blender()"

        args = [
            revision.get_path(),
            "--python-expr",
            wrap_python_expr(python_expr),
        ]
        if do_render:
            args.insert(0, "-b")

        return args

    def run(self, button):
        """Execute the render action.

        Args:
            button (str): The label of the button pressed by the user (e.g., 'Render' or 'Cancel').

        Returns:
            Any: the result of the parent run method if executed, or None if canceled.

        """
        if button == "Cancel":
            return None

        self.update_presets()

        # Fetch which file extension to use
        mgr = self.root().project().get_task_manager()
        file_name = (
            rf"(sequence_movie{'_safety' if self.safety_margins.get() is True else ''})"
        )
        dft_file = mgr.get_default_file(file_name, file_type="video")

        if dft_file is None:
            extension = ".mp4"
        else:
            _, extension = os.path.splitext(dft_file.file_name.get())

        # Define output for render folder
        output_suffix_name = f"{self._file.complete_name.get()}_movie{'_safety' if self.safety_margins.get() is True else ''}{extension}"
        self.output_path = (
            Path(self.ensure_render_sequence_folder_revision().get_path())
            / output_suffix_name
        )

        if self.output_path.parent.exists():
            remove_folder_content(self.output_path.parent)

        # For sequence movie
        if self.concat_shots.get():
            self.output_concat_path = Path(
                self.ensure_movie_sequence_revision().get_path()
            )

            if self.output_concat_path.parent.exists():
                remove_folder_content(self.output_concat_path.parent)
            else:
                os.makedirs(os.path.dirname(self.output_concat_path), exist_ok=True)

        result = GenericRunAction.run(self, button)

        return result


def render_blender_playblast(parent):
    if isinstance(parent, TrackedFile):
        r = flow.Child(RenderBlenderPlayblast)
        r.name = "render_blender_playblast"
        r.index = None
        return r


def sequence_render_playblast(parent):
    if isinstance(parent, TrackedFile) and isinstance(
        parent._task.get_parent(), Sequence
    ):
        r = flow.Child(SequenceRenderPlayblast)
        r.name = "sequence_render_playblast"
        r.index = None
        return r


def concatenate_shots(parent):
    if isinstance(parent, TrackedFolder) and isinstance(
        parent._task.get_parent(), Sequence
    ):
        r = flow.Child(ConcatenateShots)
        r.name = "concatenate_shots"
        r.index = None
        return r


def hide_assets_map(parent):
    if isinstance(parent, AssetType):
        assets_rel = flow.Child(Assets).ui(
            expanded=True, hidden=True, default_height=600
        )
        assets_rel.name = "assets"

        asset_families_rel = flow.Child(AssetFamilyCollection).ui(
            expanded=True, show_filter=True
        )
        asset_families_rel.name = "asset_families"
        return [assets_rel, asset_families_rel]


def create_kitsu_assets(parent):
    if isinstance(parent, AssetTypeCollection):
        r = flow.Child(CreateKitsuAssetTypes)
        r.name = "create_asset_types"  # override base action
        r.index = None
        return (r, 1)
    elif isinstance(parent, AssetFamilyCollection):
        r = flow.Child(CreateKitsuAssetFamilies)
        r.name = "create_asset_families"
        r.index = None
        return (r, 1)
    elif isinstance(parent, Assets):
        r = flow.Child(CreateKitsuAssets)
        r.name = "create_assets"
        r.index = None
        return (r, 1)


def install_extensions(session):
    return {
        "lpc": [
            create_kitsu_assets,
            hide_assets_map,
            render_blender_playblast,
            sequence_render_playblast,
            concatenate_shots,
        ]
    }
