#
#            PySceneDetect: Python-Based Video Scene Detector
#   -------------------------------------------------------------------
#     [  Site:    https://scenedetect.com                           ]
#     [  Docs:    https://scenedetect.com/docs/                     ]
#     [  Github:  https://github.com/Breakthrough/PySceneDetect/    ]
#
# Copyright (C) 2014-2024 Brandon Castellano <http://www.bcastell.com>.
# PySceneDetect is licensed under the BSD 3-Clause License; see the
# included LICENSE file, or visit one of the above pages for details.
#
"""The ``scenedetect`` module comes with helper functions to simplify common use cases.
:func:`detect` can be used to perform scene detection on a video by path.  :func:`open_video`
can be used to open a video for a
:class:`SceneManager <scenedetect.scene_manager.SceneManager>`.
"""

import typing as ty
from logging import getLogger

# OpenCV is a required package, but we don't have it as an explicit dependency since we
# need to support both opencv-python and opencv-python-headless. Include some additional
# context with the exception if this is the case.
try:
    import cv2 as _
except ModuleNotFoundError as ex:
    raise ModuleNotFoundError(
        "OpenCV could not be found, try installing opencv-python:\n\npip install opencv-python",
        name="cv2",
    ) from ex

# Commonly used classes/functions exported under the `scenedetect` namespace for brevity.
# Note that order of importants is important!
from scenedetect.platform import init_logger  # noqa: I001
from scenedetect.frame_timecode import FrameTimecode
from scenedetect.video_stream import VideoStream, VideoOpenFailure
from scenedetect.video_splitter import split_video_ffmpeg, split_video_mkvmerge
from scenedetect.scene_detector import SceneDetector
from scenedetect.detectors import (
    ContentDetector,
    AdaptiveDetector,
    ThresholdDetector,
    HistogramDetector,
    HashDetector,
)
from scenedetect.backends import (
    AVAILABLE_BACKENDS,
    VideoStreamCv2,
    VideoStreamAv,
    VideoStreamMoviePy,
    VideoCaptureAdapter,
)
from scenedetect.stats_manager import StatsManager, StatsFileCorrupt
from scenedetect.scene_manager import SceneManager, save_images, SceneList, CutList, Interpolation
from scenedetect.video_manager import VideoManager  # [DEPRECATED] DO NOT USE.

# Used for module identification and when printing version & about info
# (e.g. calling `scenedetect version` or `scenedetect about`).
__version__ = "0.6.5"

init_logger()
logger = getLogger("pyscenedetect")


def open_video(
    path: str,
    framerate: ty.Optional[float] = None,
    backend: str = "opencv",
    **kwargs,
) -> VideoStream:
    """Open a video at the given path. If `backend` is specified but not available on the current
    system, OpenCV (`VideoStreamCv2`) will be used as a fallback.

    Arguments:
        path: Path to video file to open.
        framerate: Overrides detected framerate if set.
        backend: Name of specific backend to use, if possible. See
            :data:`scenedetect.backends.AVAILABLE_BACKENDS` for backends available on the current
            system. If the backend fails to open the video, OpenCV will be used as a fallback.
        kwargs: Optional named arguments to pass to the specified `backend` constructor for
            overriding backend-specific options.

    Returns:
        Backend object created with the specified video path.

    Raises:
        :class:`VideoOpenFailure`: Constructing the VideoStream fails. If multiple backends have
            been attempted, the error from the first backend will be returned.
    """
    last_error: Exception = None
    # If `backend` is available, try to open the video at `path` using it.
    if backend in AVAILABLE_BACKENDS:
        backend_type = AVAILABLE_BACKENDS[backend]
        try:
            logger.debug("Opening video with %s...", backend_type.BACKEND_NAME)
            return backend_type(path, framerate, **kwargs)
        except VideoOpenFailure as ex:
            logger.warning("Failed to open video with %s: %s", backend_type.BACKEND_NAME, str(ex))
            if backend == VideoStreamCv2.BACKEND_NAME:
                raise
            last_error = ex
    else:
        logger.warning("Backend %s not available.", backend)
    # Fallback to OpenCV if `backend` is unavailable, or specified backend failed to open `path`.
    backend_type = VideoStreamCv2
    logger.warning("Trying another backend: %s", backend_type.BACKEND_NAME)
    try:
        return backend_type(path, framerate)
    except VideoOpenFailure as ex:
        logger.debug("Failed to open video: %s", str(ex))
        if last_error is None:
            last_error = ex
    # Propagate any exceptions raised from specified backend, instead of errors from the fallback.
    assert last_error is not None
    raise last_error


def detect(
    video_path: str,
    detector: SceneDetector,
    stats_file_path: ty.Optional[str] = None,
    show_progress: bool = False,
    start_time: ty.Optional[ty.Union[str, float, int]] = None,
    end_time: ty.Optional[ty.Union[str, float, int]] = None,
    start_in_scene: bool = False,
) -> SceneList:
    """Perform scene detection on a given video `path` using the specified `detector`.

    Arguments:
        video_path: Path to input video (absolute or relative to working directory).
        detector: A `SceneDetector` instance (see :mod:`scenedetect.detectors` for a full list
            of detectors).
        stats_file_path: Path to save per-frame metrics to for statistical analysis or to
            determine a better threshold value.
        show_progress: Show a progress bar with estimated time remaining. Default is False.
        start_time: Starting point in video, in the form of a timecode ``HH:MM:SS[.nnn]`` (`str`),
            number of seconds ``123.45`` (`float`), or number of frames ``200`` (`int`).
        end_time: Starting point in video, in the form of a timecode ``HH:MM:SS[.nnn]`` (`str`),
            number of seconds ``123.45`` (`float`), or number of frames ``200`` (`int`).
        start_in_scene: Assume the video begins in a scene. This means that when detecting
            fast cuts with `ContentDetector`, if no cuts are found, the resulting scene list
            will contain a single scene spanning the entire video (instead of no scenes).
            When detecting fades with `ThresholdDetector`, the beginning portion of the video
            will always be included until the first fade-out event is detected.

    Returns:
        List of scenes as pairs of (start, end) :class:`FrameTimecode` objects.

    Raises:
        :class:`VideoOpenFailure`: `video_path` could not be opened.
        :class:`StatsFileCorrupt`: `stats_file_path` is an invalid stats file
        ValueError: `start_time` or `end_time` are incorrectly formatted.
        TypeError: `start_time` or `end_time` are invalid types.
    """
    video = open_video(video_path)
    if start_time is not None:
        start_time = video.base_timecode + start_time
        video.seek(start_time)
    if end_time is not None:
        end_time = video.base_timecode + end_time
    # To reduce memory consumption when not required, we only add a StatsManager if we
    # need to save frame metrics to disk.
    scene_manager = SceneManager(StatsManager() if stats_file_path else None)
    scene_manager.add_detector(detector)
    scene_manager.detect_scenes(
        video=video,
        show_progress=show_progress,
        end_time=end_time,
    )
    if scene_manager.stats_manager is not None:
        scene_manager.stats_manager.save_to_csv(csv_file=stats_file_path)
    return scene_manager.get_scene_list(start_in_scene=start_in_scene)
