/// Parameters are sorted by logical order, which is different from UI order
///
/// Order:
///   100 noaudio
///   120 output
///  1000 default params (in order of addition)
///  1600 trim
///  2000 hwaccel
///  4000 abitrate, vbitrate
mod macros;
mod parameter;

mod audio_bitrate;
mod audio_crystalizer;
mod audio_pitch;
mod audio_volume;
mod disable_audio;
mod finalizer;
mod hardware_acceleration;
mod output_format;
mod speed_factor;
mod trim;
mod video_bitrate;
mod video_frame_rate;
mod video_scale;

pub(crate) use audio_bitrate::*;
pub(crate) use audio_crystalizer::*;
pub(crate) use audio_pitch::*;
pub(crate) use audio_volume::*;
pub(crate) use disable_audio::*;
pub(crate) use finalizer::*;
pub(crate) use hardware_acceleration::*;
pub(crate) use output_format::*;
pub(crate) use parameter::{Parameter, ParameterData, PresetParameter, SelectOption};
pub(crate) use speed_factor::*;
pub(crate) use trim::*;
pub(crate) use video_bitrate::*;
pub(crate) use video_frame_rate::*;
pub(crate) use video_scale::*;

use crate::{
    info::Info,
    params::macros::select_option,
    visitors::{ParameterVisitor, PresetApplier, PresetSaver, VisitorContext},
};
use tracing::{Level, debug, enabled};

/// Create parameters based on given info and apply the preset
pub(crate) fn create_params(info: &Info, preset: Option<&str>, source_ext: &str) -> Vec<Parameter> {
    let mut params: Vec<Parameter> = Vec::new();
    if info.has_non_empty_duration() {
        params.push(Trim::new_parameter());
    }
    if info.has_audio() && info.has_video() {
        params.push(DisableAudio::new_parameter());
    }
    if info.has_audio() {
        params.push(AudioBitrate::new_parameter());
        params.push(AudioCrystalizer::new_parameter());
        params.push(AudioVolume::new_parameter());
        params.push(AudioPitch::new_parameter());
    }
    if info.has_non_empty_duration() {
        params.push(SpeedFactor::new_parameter());
    }
    if info.has_video() {
        params.push(VideoBitrate::new_parameter());
        params.push(VideoFrameRate::new_parameter());
        params.push(VideoScale::new_parameter());
        params.push(HardwareAcceleration::new_parameter());
    }
    params.push(OutputFormat::new_parameter(info, source_ext));
    if let Some(preset_value) = preset {
        let ctx = VisitorContext::new(info);
        apply_preset(ctx, &mut params, preset_value);
    }
    recheck_params(&mut params);
    params
}

pub(crate) fn apply_preset(ctx: VisitorContext, params: &mut [Parameter], preset: &str) {
    let mut preset_applier = PresetApplier::new(ctx, preset);
    apply_visitor(&mut preset_applier, params);
}

pub(crate) fn save_preset(ctx: VisitorContext, params: &mut [Parameter]) -> String {
    let mut preset_saver = PresetSaver::new(ctx);
    apply_visitor(&mut preset_saver, params);
    preset_saver.collect()
}

/// Recheck and disable conflicting parameters after each option change
pub(crate) fn recheck_params(params: &mut [Parameter]) {
    let result_is_audio = if let Some(result_format) = get_output_format(params) {
        OutputFormat::is_audio(&result_format.value)
    } else {
        false
    };

    let audio_is_disabled = if let Some(disable_audio) = params
        .iter()
        .filter(|param| param.id == DisableAudio::ID)
        .find_map(|param| match &param.data {
            ParameterData::Toggle { value } => Some(value),
            _ => None,
        }) {
        !result_is_audio && *disable_audio
    } else {
        false
    };

    for param in params {
        if matches!(
            param.id,
            DisableAudio::ID
                | VideoScale::ID
                | VideoBitrate::ID
                | VideoFrameRate::ID
                | HardwareAcceleration::ID
        ) {
            param.enabled = !result_is_audio;
        }

        if matches!(
            param.id,
            AudioBitrate::ID | AudioCrystalizer::ID | AudioPitch::ID | AudioVolume::ID
        ) {
            param.enabled = !audio_is_disabled;
        }

        if param.id == OutputFormat::ID {
            OutputFormat::toggle_audio_formats(param, !audio_is_disabled);
        }
    }
}

pub(crate) fn get_output_format(params: &[Parameter]) -> Option<&SelectOption> {
    params
        .iter()
        .filter(|param| param.id == OutputFormat::ID)
        .find_map(|param| select_option!(&param.data))
}

pub(crate) fn apply_visitor(visitor: &mut dyn ParameterVisitor, params: &mut [Parameter]) {
    // Apply visitor based on logical order, which is slightly different from UI order
    let mut sorted_params: Vec<&mut Parameter> =
        params.iter_mut().filter(|param| param.enabled).collect();
    sorted_params.sort_by_key(|param| param.order);
    if enabled!(Level::DEBUG) {
        let visitor_type = std::any::type_name_of_val(visitor);
        let chain = sorted_params
            .iter()
            .map(|p| format!("{}({})", p.id, p.order))
            .collect::<Vec<_>>()
            .join(" -> ");
        debug!(visitor = visitor_type, %chain, "params chain");
    }
    for param in sorted_params {
        match param.id {
            Trim::ID => visitor.visit_trim(&mut param.data),
            DisableAudio::ID => visitor.visit_disable_audio(&mut param.data),
            AudioVolume::ID => visitor.visit_audio_volume(&mut param.data),
            AudioBitrate::ID => visitor.visit_audio_bitrate(&mut param.data),
            AudioCrystalizer::ID => visitor.visit_audio_crystalizer(&mut param.data),
            AudioPitch::ID => visitor.visit_audio_pitch(&mut param.data),
            SpeedFactor::ID => visitor.visit_speed_factor(&mut param.data),
            VideoBitrate::ID => visitor.visit_video_bitrate(&mut param.data),
            VideoFrameRate::ID => visitor.visit_video_frame_rate(&mut param.data),
            VideoScale::ID => visitor.visit_video_scale(&mut param.data),
            HardwareAcceleration::ID => visitor.visit_hardware_acceleration(&mut param.data),
            OutputFormat::ID => visitor.visit_output_format(&mut param.data),
            _ => {}
        }
    }
    visitor.visit_last();
}
