use std::path::PathBuf;

use anyhow::Ok;
use av_decoders::DecoderImpl;
use av_scenechange::{Decoder, DetectionOptions, SceneDetectionSpeed, detect_scene_changes};
use pyo3_stub_gen::derive::gen_stub_pyclass_enum;

use crate::{
    progressbar::constuct_progress_bar,
    result::{ArgsResult, DetectResult, SmartResult, warn_check},
    vid_info::{ffms2_get_scene_len, get_ffms2_instance, is_vapoursynth_script, set_ffms2_format},
};

#[gen_stub_pyclass_enum]
#[pyo3::pyclass]
#[derive(Clone)]
pub enum ScMethod {
    Fast,
    Standard,
    Noneall,
}

pub struct SceneDetectArgs {
    pub input:              PathBuf,
    pub user_min_scene_len: Option<isize>,
    pub user_max_scene_len: Option<isize>,
    pub sc_width:           Option<usize>,
    pub sc_height:          Option<usize>,
    pub sc_method:          ScMethod,
    pub progress_template:  Option<String>,
    pub detect_flashes:     bool,
    pub lookahead_distance: usize,
}

struct VideoInfo {
    min_scene_len: Option<usize>,
    max_scene_len: Option<usize>,
}

/// main scene detection function
pub fn scene_detect(scene_detect_args: SceneDetectArgs) -> anyhow::Result<DetectResult> {
    let (mut decoder, total_frame, bitdepth, video_info) = build_decoder(&scene_detect_args)?;

    let analyze_speed = match scene_detect_args.sc_method {
        ScMethod::Fast => SceneDetectionSpeed::Fast,
        ScMethod::Standard => SceneDetectionSpeed::Standard,
        ScMethod::Noneall => SceneDetectionSpeed::None,
    };

    let options = DetectionOptions {
        analysis_speed:        analyze_speed,
        detect_flashes:        scene_detect_args.detect_flashes,
        min_scenecut_distance: video_info.min_scene_len,
        max_scenecut_distance: video_info.max_scene_len,
        lookahead_distance:    scene_detect_args.lookahead_distance,
    };

    let pb = constuct_progress_bar(
        total_frame as u64,
        0,
        scene_detect_args.progress_template,
        false,
    );

    let results = if bitdepth == 8 {
        detect_scene_changes::<u8>(
            &mut decoder,
            options,
            Some(total_frame),
            Some(&|frames, scene| {
                pb.set_position(frames as u64);
                pb.set_message(scene.to_string());
            }),
        )?
    } else {
        detect_scene_changes::<u16>(
            &mut decoder,
            options,
            Some(total_frame),
            Some(&|frames, scene| {
                pb.set_position(frames as u64);
                pb.set_message(scene.to_string());
            }),
        )?
    };

    warn_check(&results, total_frame);
    pb.finish();

    Ok(DetectResult {
        detect_result: results,
        args:          ArgsResult {
            min_scene_len:      video_info.min_scene_len,
            max_scene_len:      video_info.max_scene_len,
            lookahead_distance: scene_detect_args.lookahead_distance,
            detect_flashes:     scene_detect_args.detect_flashes,
            sc_method:          scene_detect_args.sc_method,
            total_frames:       total_frame,
        },
    })
}

/// smart scene detection function
pub fn smart_scene_detect(scene_detect_args: SceneDetectArgs) -> anyhow::Result<SmartResult> {
    let (mut decoder, total_frame, bitdepth, _) = build_decoder(&scene_detect_args)?;

    let analyze_speed = match scene_detect_args.sc_method {
        ScMethod::Fast => SceneDetectionSpeed::Fast,
        ScMethod::Standard => SceneDetectionSpeed::Standard,
        ScMethod::Noneall => SceneDetectionSpeed::None,
    };

    let min_len = scene_detect_args.user_min_scene_len.and_then(|v| usize::try_from(v).ok());
    let max_len = scene_detect_args.user_max_scene_len.and_then(|v| usize::try_from(v).ok());

    let options = DetectionOptions {
        analysis_speed:        analyze_speed,
        detect_flashes:        scene_detect_args.detect_flashes,
        min_scenecut_distance: min_len,
        max_scenecut_distance: max_len,
        lookahead_distance:    scene_detect_args.lookahead_distance,
    };

    let pb = constuct_progress_bar(
        total_frame as u64,
        0,
        scene_detect_args.progress_template,
        true,
    );

    let results = if bitdepth == 8 {
        detect_scene_changes::<u8>(
            &mut decoder,
            options,
            Some(total_frame),
            Some(&|frames, _| {
                pb.set_position(frames as u64);
            }),
        )?
    } else {
        detect_scene_changes::<u16>(
            &mut decoder,
            options,
            Some(total_frame),
            Some(&|frames, _| {
                pb.set_position(frames as u64);
            }),
        )?
    };

    warn_check(&results, total_frame);
    pb.finish();

    Ok(SmartResult {
        detect_result: results,
        args:          ArgsResult {
            min_scene_len:      min_len,
            max_scene_len:      max_len,
            lookahead_distance: scene_detect_args.lookahead_distance,
            detect_flashes:     scene_detect_args.detect_flashes,
            sc_method:          scene_detect_args.sc_method,
            total_frames:       total_frame,
        },
    })
}

fn build_decoder(args: &SceneDetectArgs) -> anyhow::Result<(Decoder, usize, u8, VideoInfo)> {
    if is_vapoursynth_script(&args.input) {
        #[cfg(not(feature = "vapoursynth"))]
        anyhow::bail!("VapourSynth support not enabled");

        #[cfg(feature = "vapoursynth")]
        {
            use std::collections::HashMap;

            use av_decoders::VapoursynthDecoder;
            use vapoursynth::prelude::Environment;

            use crate::vid_info::vs_get_scene_len;
            const OUTPUT_INDEX: i32 = 0;
            let vs_decoder = VapoursynthDecoder::from_file(&args.input, HashMap::new())?;
            let environment = Environment::from_file(
                &args.input,
                vapoursynth::prelude::EvalFlags::SetWorkingDir,
            )?;
            let (node, _) = environment.get_output(OUTPUT_INDEX)?;

            let info = node.info();
            let bitdepth = info.format.bits_per_sample();
            let numframe = info.num_frames;
            let (min_len, max_len) = vs_get_scene_len(
                &info,
                args.user_min_scene_len.as_ref(),
                args.user_max_scene_len.as_ref(),
            )?;

            let decoder = Decoder::from_decoder_impl(DecoderImpl::Vapoursynth(vs_decoder))?;

            Ok((decoder, numframe, bitdepth, VideoInfo {
                min_scene_len: min_len,
                max_scene_len: max_len,
            }))
        }
    } else {
        let mut videosource = get_ffms2_instance(args.input.as_os_str().to_string_lossy().trim());
        let numframe = videosource.video_details.total_frames.expect("Video should have frames");
        let bitdepth = videosource.video_details.bit_depth as u8;

        if args.sc_height.is_some() || args.sc_width.is_some() {
            set_ffms2_format(&mut videosource, args.sc_height, args.sc_width, bitdepth)?;
        }

        let (min_len, max_len) = ffms2_get_scene_len(
            &mut videosource,
            args.user_min_scene_len.as_ref(),
            args.user_max_scene_len.as_ref(),
        );

        let decoder = Decoder::from_decoder_impl(DecoderImpl::Ffms2(videosource))?;

        Ok((decoder, numframe, bitdepth, VideoInfo {
            min_scene_len: min_len,
            max_scene_len: max_len,
        }))
    }
}
