import glob
import json
import os
import traceback

import unreal


def _log(message: str) -> None:
    unreal.log(f"[HyperTwistMoviePipelineReplayExecutor] {message}")


def _log_error(message: str) -> None:
    unreal.log_error(f"[HyperTwistMoviePipelineReplayExecutor] {message}")


def _get_parameter(parameters, key: str, default: str = "") -> str:
    value = parameters.get(key, default)
    return value.strip() if isinstance(value, str) else default


def _get_int_parameter(parameters, key: str, default: int) -> int:
    value = _get_parameter(parameters, key, "")
    if value == "":
        return default

    try:
        return int(value)
    except ValueError:
        return default


def _resolve_render_success(results, matching_files) -> bool:
    success_value = getattr(results, "success", None)
    if success_value is None:
        return len(matching_files) > 0

    try:
        return bool(success_value)
    except Exception:
        return len(matching_files) > 0


@unreal.uclass()
class HyperTwistMoviePipelineReplayExecutor(unreal.MoviePipelinePythonHostExecutor):
    active_movie_pipeline = unreal.uproperty(unreal.MoviePipeline)
    output_directory = unreal.uproperty(str)
    output_base_name = unreal.uproperty(str)
    output_kind = unreal.uproperty(str)
    report_path = unreal.uproperty(str)
    replay_path = unreal.uproperty(str)
    render_map_path = unreal.uproperty(str)

    def _post_init(self):
        self.active_movie_pipeline = None
        self.output_directory = ""
        self.output_base_name = ""
        self.output_kind = "video"
        self.report_path = ""
        self.replay_path = ""
        self.render_map_path = ""

    @unreal.ufunction(override=True)
    def execute_delayed(self, in_pipeline_queue):
        try:
            (_cmd_tokens, _cmd_switches, cmd_parameters) = (
                unreal.SystemLibrary.parse_command_line(unreal.SystemLibrary.get_command_line())
            )
            level_sequence_path = _get_parameter(cmd_parameters, "LevelSequence")
            if level_sequence_path == "":
                raise RuntimeError("Missing -LevelSequence= argument for replay export render.")

            self.output_directory = _get_parameter(
                cmd_parameters, "HyperTwistOutputDirectory", "C:/HyperTwist/media-export"
            )
            self.output_base_name = _get_parameter(
                cmd_parameters, "HyperTwistOutputBaseName", "classic-cube-replay"
            )
            self.output_kind = _get_parameter(
                cmd_parameters, "HyperTwistOutputKind", "video"
            ).lower()
            self.report_path = _get_parameter(cmd_parameters, "HyperTwistExportReportPath")
            self.replay_path = _get_parameter(cmd_parameters, "HyperTwistReplayPath")
            self.render_map_path = _get_parameter(cmd_parameters, "HyperTwistRenderMapPath")

            output_width = max(
                16, _get_int_parameter(cmd_parameters, "HyperTwistOutputWidth", 1280)
            )
            output_height = max(
                16, _get_int_parameter(cmd_parameters, "HyperTwistOutputHeight", 720)
            )
            output_frame_rate = max(
                1, _get_int_parameter(cmd_parameters, "HyperTwistOutputFrameRate", 30)
            )
            custom_start_frame = max(
                0, _get_int_parameter(cmd_parameters, "HyperTwistCustomStartFrame", 0)
            )
            custom_end_frame = max(
                custom_start_frame + 1,
                _get_int_parameter(
                    cmd_parameters, "HyperTwistCustomEndFrame", custom_start_frame + 1
                ),
            )

            pipeline_queue = (
                in_pipeline_queue
                if in_pipeline_queue is not None
                else unreal.new_object(unreal.MoviePipelineQueue, outer=self)
            )
            self.pipeline_queue = pipeline_queue
            new_job = pipeline_queue.allocate_new_job(unreal.MoviePipelineExecutorJob)
            if self.render_map_path:
                new_job.map = unreal.SoftObjectPath(self.render_map_path)
            new_job.sequence = unreal.SoftObjectPath(level_sequence_path)

            job_config = new_job.get_configuration()
            output_setting = job_config.find_or_add_setting_by_class(
                unreal.MoviePipelineOutputSetting
            )
            output_setting.output_directory = unreal.DirectoryPath(self.output_directory)
            output_setting.file_name_format = self.output_base_name
            output_setting.output_resolution = unreal.IntPoint(output_width, output_height)
            output_setting.use_custom_frame_rate = True
            output_setting.output_frame_rate = unreal.FrameRate(
                numerator=output_frame_rate, denominator=1
            )
            output_setting.override_existing_output = True
            output_setting.use_custom_playback_range = True
            output_setting.custom_start_frame = custom_start_frame
            output_setting.custom_end_frame = custom_end_frame
            output_setting.zero_pad_frame_numbers = 4
            output_setting.flush_disk_writes_per_shot = True

            job_config.find_or_add_setting_by_class(unreal.MoviePipelineDeferredPassBase)

            if self.output_kind == "still":
                job_config.find_or_add_setting_by_class(
                    unreal.MoviePipelineImageSequenceOutput_PNG
                )
            else:
                mp4_output = job_config.find_or_add_setting_by_class(
                    unreal.MoviePipelineMP4EncoderOutput
                )
                mp4_output.include_audio = False
                mp4_output.constant_rate_factor = 18
                mp4_output.encoding_rate_control = (
                    unreal.MoviePipelineMP4EncodeRateControlMode.QUALITY
                )

            job_config.initialize_transient_settings()

            target_world = self.get_last_loaded_world()
            if target_world is None:
                raise RuntimeError(
                    "Movie render pipeline could not resolve the last loaded world."
                )

            self.active_movie_pipeline = unreal.new_object(
                unreal.MoviePipeline,
                outer=target_world,
            )
            self.active_movie_pipeline.on_movie_pipeline_work_finished_delegate.add_function_unique(
                self, "on_movie_pipeline_finished"
            )

            _log(
                "Starting replay export render "
                f"(kind={self.output_kind}, map={self.render_map_path or '<current>'}, "
                f"sequence={level_sequence_path}, "
                f"frames={custom_start_frame}-{custom_end_frame}, output={self.output_directory})"
            )
            self.active_movie_pipeline.initialize(new_job)
        except Exception as error:
            _log_error(
                f"Replay export render setup failed: {error}\n{traceback.format_exc()}"
            )
            self.active_movie_pipeline = None
            self.on_executor_finished_impl()

    @unreal.ufunction(override=True)
    def on_begin_frame(self):
        super(HyperTwistMoviePipelineReplayExecutor, self).on_begin_frame()

    @unreal.ufunction(override=True)
    def on_map_load(self, in_world):
        pass

    @unreal.ufunction(override=True)
    def is_rendering(self):
        return self.active_movie_pipeline is not None

    @unreal.ufunction(ret=None, params=[unreal.MoviePipelineOutputData])
    def on_movie_pipeline_finished(self, results):
        extension = ".png" if self.output_kind == "still" else ".mp4"
        pattern = os.path.join(self.output_directory, "**", f"{self.output_base_name}*{extension}")
        matching_files = sorted(glob.glob(pattern, recursive=True))
        success = _resolve_render_success(results, matching_files)

        report = {
            "reportVersion": "ht-classic-cube-mrq-export/v1",
            "success": success,
            "outputKind": self.output_kind,
            "outputDirectory": self.output_directory,
            "outputBaseName": self.output_base_name,
            "replayPath": self.replay_path,
            "files": matching_files,
        }

        if self.report_path:
            report_directory = os.path.dirname(self.report_path)
            if report_directory:
                os.makedirs(report_directory, exist_ok=True)
            with open(self.report_path, "w", encoding="utf-8") as handle:
                json.dump(report, handle, indent=2)

        _log(
            f"Finished replay export render (success={success}, files={len(matching_files)})"
        )
        self.active_movie_pipeline = None
        self.on_executor_finished_impl()
