#
#            PySceneDetect: Python-Based Video Scene Detector
#   -------------------------------------------------------------------
#     [  Site:    https://scenedetect.com                           ]
#     [  Docs:    https://scenedetect.com/docs/                     ]
#     [  Github:  https://github.com/Breakthrough/PySceneDetect/    ]
#
# Copyright (C) 2016 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>`.
"""

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 _  # availability check; raise a friendlier error if missing
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 as init_logger  # noqa: I001
from scenedetect.common import (
    FrameTimecode as FrameTimecode,
    FrameRate as FrameRate,
    SceneList as SceneList,
    CutList as CutList,
    CropRegion as CropRegion,
    TimecodePair as TimecodePair,
    TimecodeLike as TimecodeLike,
    Interpolation as Interpolation,
)
from scenedetect.platform import StrPath as StrPath
from scenedetect.video_stream import VideoStream as VideoStream
from scenedetect.video_stream import VideoOpenFailure as VideoOpenFailure
from scenedetect.output import (
    save_images as save_images,
    split_video_ffmpeg as split_video_ffmpeg,
    split_video_mkvmerge as split_video_mkvmerge,
    is_ffmpeg_available as is_ffmpeg_available,
    is_mkvmerge_available as is_mkvmerge_available,
    write_scene_list as write_scene_list,
    write_scene_list_html as write_scene_list_html,
    PathFormatter as PathFormatter,
    VideoMetadata as VideoMetadata,
    SceneMetadata as SceneMetadata,
)
from scenedetect.detector import SceneDetector as SceneDetector
from scenedetect.detectors import (
    ContentDetector as ContentDetector,
    AdaptiveDetector as AdaptiveDetector,
    ThresholdDetector as ThresholdDetector,
    HistogramDetector as HistogramDetector,
    HashDetector as HashDetector,
)
from scenedetect.backends import (
    AVAILABLE_BACKENDS as AVAILABLE_BACKENDS,
    VideoStreamCv2 as VideoStreamCv2,
    VideoStreamAv as VideoStreamAv,
    VideoStreamMoviePy as VideoStreamMoviePy,
    VideoCaptureAdapter as VideoCaptureAdapter,
    VideoStreamConcat as VideoStreamConcat,
    SourceSpan as SourceSpan,
)
from scenedetect.stats_manager import StatsManager as StatsManager
from scenedetect.stats_manager import StatsFileCorrupt as StatsFileCorrupt
from scenedetect.scene_manager import SceneManager

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

init_logger()
logger = getLogger("pyscenedetect")


def open_video(
    path: "StrPath | list[StrPath] | tuple[StrPath, ...]",
    frame_rate: FrameRate | None = None,
    backend: str = "opencv",
    framerate: float | None = None,
    **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. May also be a list of paths, in which case the videos
            are concatenated and treated as a single continuous stream
            (see :class:`VideoStreamConcat <scenedetect.backends.concat.VideoStreamConcat>`).
        frame_rate: Overrides detected frame rate if set. Takes precedence over `framerate`.
        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.
        framerate: [DEPRECATED] Use `frame_rate` instead. Retained as a deprecated alias for
            backwards compatibility; ignored when `frame_rate` is provided.
        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.
    """
    # TODO(https://scenedetect.com/issue/548): emit DeprecationWarning when `framerate=` is
    # used, once internal callers and downstream users have had a release to migrate.
    if frame_rate is None:
        frame_rate = framerate
    # A list of paths is opened as a single concatenated stream. VideoStreamConcat handles
    # backend selection/fallback internally, so this must come before the lookup below.
    if isinstance(path, (list, tuple)):
        return VideoStreamConcat(path, frame_rate, backend=backend, **kwargs)
    last_error: Exception | None = 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, frame_rate, **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, frame_rate)
    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: "StrPath | list[StrPath] | tuple[StrPath, ...]",
    detector: SceneDetector,
    stats_file_path: StrPath | None = None,
    show_progress: bool = False,
    start_time: TimecodeLike | None = None,
    end_time: TimecodeLike | None = None,
    start_in_scene: bool = False,
    backend: str = "opencv",
) -> 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). May also
            be a list of paths, in which case the videos are concatenated and treated as a
            single continuous stream.
        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.
        backend: Name of the backend to use for video decoding. See
            :data:`scenedetect.backends.AVAILABLE_BACKENDS` for backends available on the
            current system. Defaults to OpenCV; falls back to OpenCV if the requested backend
            is unavailable or fails to open the video.

    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, backend=backend)
    if start_time is not None:
        video.seek(FrameTimecode(start_time, video.frame_rate))
    end_timecode = FrameTimecode(end_time, video.frame_rate) if end_time is not None else None
    # 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_timecode,
    )
    if scene_manager.stats_manager is not None and stats_file_path 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)
