//! Rust library for CCExtractor
//!
//! Currently we are in the process of porting the 708 decoder to rust. See [decoder]

// Allow C naming style
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]

/// CCExtractor C bindings generated by bindgen
#[allow(clippy::all)]
pub mod bindings {
    include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}

pub mod args;
pub mod avc;
pub mod common;
pub mod ctorust;
pub mod decoder;
pub mod demuxer;
pub mod encoder;
pub mod es;
pub mod file_functions;
#[cfg(feature = "hardsubx_ocr")]
pub mod hardsubx;
pub mod hlist;
pub mod libccxr_exports;
pub mod parser;
pub mod track_lister;
pub mod utils;

#[cfg(windows)]
use std::os::windows::io::{FromRawHandle, RawHandle};

use args::Args;
use bindings::*;
use cfg_if::cfg_if;
use clap::{error::ErrorKind, Parser};
use common::{copy_from_rust, CType, CType2};
use decoder::Dtvcc;
use lib_ccxr::{common::Options, teletext::TeletextConfig, util::log::ExitCause};
use parser::OptionsExt;
use utils::is_true;

use env_logger::{builder, Target};
use log::{warn, LevelFilter};
#[cfg(not(test))]
use std::os::raw::c_ulong;
use std::os::raw::{c_uchar, c_void};
use std::{
    ffi::CStr,
    io::Write,
    os::raw::{c_char, c_double, c_int, c_uint},
};

// Mock data for rust unit tests
cfg_if! {
    if #[cfg(test)] {
        static mut cb_708: c_int = 0;
        static mut cb_field1: c_int = 0;
        static mut cb_field2: c_int = 0;
        static mut current_fps: c_double = 30.0;
        static mut usercolor_rgb: [c_int; 8] = [0; 8];
        static mut FILEBUFFERSIZE: c_int = 0;
        static mut MPEG_CLOCK_FREQ: c_int = 90000;

        static mut frames_since_ref_time: c_int = 0;
        static mut total_frames_count: c_uint = 0;
        static mut fts_at_gop_start: i64 = 0;
        static mut gop_rollover: c_int = 0;
        static mut pts_big_change: c_uint = 0;

        static mut tlt_config: ccx_s_teletext_config = unsafe { std::mem::zeroed() };
        static mut ccx_options: ccx_s_options = unsafe { std::mem::zeroed() };
        static mut gop_time: gop_time_code = unsafe { std::mem::zeroed() };
        static mut first_gop_time: gop_time_code = unsafe { std::mem::zeroed() };
        static mut ccx_common_timing_settings: ccx_common_timing_settings_t = unsafe { std::mem::zeroed() };
        static mut capitalization_list: word_list = unsafe { std::mem::zeroed() };
        static mut profane: word_list = unsafe { std::mem::zeroed() };

        unsafe extern "C" fn version(_location: *const c_char) {}
        unsafe extern "C" fn set_binary_mode() {}
        fn process_hdcc(_enc_ctx: *mut encoder_ctx, _ctx: *mut lib_cc_decode, _sub: *mut cc_subtitle){}
        fn store_hdcc(
            _enc_ctx: *mut encoder_ctx,
            _ctx: *mut lib_cc_decode,
            _cc_data: *mut c_uchar,
            _cc_count: c_int,
            _sequence_number: c_int,
            _current_fts_now: LLONG,
            _sub: *mut cc_subtitle,
        ){}
        fn anchor_hdcc(_ctx: *mut lib_cc_decode, _seq: c_int){}
        fn do_cb(
            _ctx: *mut lib_cc_decode,
            _cc_block: *mut c_uchar,
            _sub: *mut cc_subtitle,
        ) -> c_int{0}
        fn decode_vbi(
            _dec_ctx: *mut lib_cc_decode,
            _field: u8,
            _buffer: *mut c_uchar,
            _len: usize,
            _sub: *mut cc_subtitle,
        ) -> c_int{0}
        #[allow(dead_code)]
        fn print_file_report(_ctx: *mut lib_ccx_ctx){}
        #[cfg(feature = "enable_ffmpeg")]
        fn init_ffmpeg(path: *const c_char){}
        pub fn start_tcp_srv(_port: *const c_char, _pwd: *const c_char) -> c_int{0}
        pub fn start_upd_srv(_src: *const c_char, _addr: *const c_char, _port: c_uint) -> c_int{0}
        pub fn net_udp_read(
            _socket: c_int,
            _buffer: *mut c_void,
            _length: usize,
            _src_str: *const c_char,
            _addr_str: *const c_char,
        ) -> c_int{0}
        pub fn net_tcp_read(_socket: c_int, _buffer: *mut c_void, _length: usize) -> c_int{0}
        pub fn ccx_probe_mxf(_ctx: *mut ccx_demuxer) -> c_int{0}
        pub fn ccx_mxf_init(_demux: *mut ccx_demuxer) -> *mut MXFContext{std::ptr::null_mut()}
        #[allow(clashing_extern_declarations)]
        pub fn ccx_gxf_probe(_buf: *const c_uchar, _len: c_int) -> c_int{0}
        pub fn ccx_gxf_init(_arg: *mut ccx_demuxer) -> *mut ccx_gxf{std::ptr::null_mut()}
    }
}

// External C symbols (only when not testing)
#[cfg(not(test))]
extern "C" {
    static mut cb_708: c_int;
    static mut cb_field1: c_int;
    static mut cb_field2: c_int;
    static mut current_fps: c_double;
    static mut usercolor_rgb: [c_int; 8];
    static mut FILEBUFFERSIZE: c_int;
    static mut terminate_asap: c_int;
    static mut net_activity_gui: c_ulong;
    static mut MPEG_CLOCK_FREQ: c_int;
    static mut tlt_config: ccx_s_teletext_config;
    static mut ccx_options: ccx_s_options;
    static mut frames_since_ref_time: c_int;
    static mut total_frames_count: c_uint;
    static mut gop_time: gop_time_code;
    static mut first_gop_time: gop_time_code;
    static mut fts_at_gop_start: i64;
    static mut gop_rollover: c_int;
    static mut ccx_common_timing_settings: ccx_common_timing_settings_t;
    static mut capitalization_list: word_list;
    static mut profane: word_list;
    static mut pts_big_change: c_uint;

    fn version(location: *const c_char);
    fn set_binary_mode();
    fn process_hdcc(enc_ctx: *mut encoder_ctx, ctx: *mut lib_cc_decode, sub: *mut cc_subtitle);
    fn store_hdcc(
        enc_ctx: *mut encoder_ctx,
        ctx: *mut lib_cc_decode,
        cc_data: *mut c_uchar,
        cc_count: c_int,
        sequence_number: c_int,
        current_fts_now: LLONG,
        sub: *mut cc_subtitle,
    );
    fn anchor_hdcc(ctx: *mut lib_cc_decode, seq: c_int);
    fn do_cb(ctx: *mut lib_cc_decode, cc_block: *mut c_uchar, sub: *mut cc_subtitle) -> c_int;
    fn decode_vbi(
        dec_ctx: *mut lib_cc_decode,
        field: u8,
        buffer: *mut c_uchar,
        len: usize,
        sub: *mut cc_subtitle,
    ) -> c_int;
    fn print_file_report(ctx: *mut lib_ccx_ctx);
    #[allow(dead_code)]
    #[cfg(feature = "enable_ffmpeg")]
    fn init_ffmpeg(path: *const c_char);
    pub fn start_tcp_srv(port: *const c_char, pwd: *const c_char) -> c_int;
    pub fn start_upd_srv(src: *const c_char, addr: *const c_char, port: c_uint) -> c_int;
    pub fn net_udp_read(
        socket: c_int,
        buffer: *mut c_void,
        length: usize,
        src_str: *const c_char,
        addr_str: *const c_char,
    ) -> c_int;
    pub fn net_tcp_read(socket: c_int, buffer: *mut c_void, length: usize) -> c_int;
    pub fn ccx_probe_mxf(ctx: *mut ccx_demuxer) -> c_int;
    pub fn ccx_mxf_init(demux: *mut ccx_demuxer) -> *mut MXFContext;
    #[allow(clashing_extern_declarations)]
    pub fn ccx_gxf_probe(buf: *const c_uchar, len: c_int) -> c_int;
    pub fn ccx_gxf_init(arg: *mut ccx_demuxer) -> *mut ccx_gxf;
}

/// Initialize env logger with custom format, using stderr as target
/// This ensures debug output doesn't pollute stdout when using --stdout option
///
/// # Safety
///
/// This function is safe to call from any context. It initializes the global
/// logger, so it should only be called once during program startup. Subsequent
/// calls will have no effect (env_logger silently ignores duplicate init).
#[no_mangle]
pub extern "C" fn ccxr_init_logger() {
    builder()
        .format(|buf, record| writeln!(buf, "[CEA-708] {}", record.args()))
        .filter_level(LevelFilter::Debug)
        .target(Target::Stderr)
        .init();
}

/// Process cc_data
///
/// # Safety
/// dec_ctx should not be a null pointer
/// data should point to cc_data of length cc_count
#[no_mangle]
extern "C" fn ccxr_process_cc_data(
    dec_ctx: *mut lib_cc_decode,
    data: *const ::std::os::raw::c_uchar,
    cc_count: c_int,
) -> c_int {
    // Null pointer and bounds checks
    if dec_ctx.is_null() || data.is_null() || cc_count <= 0 {
        return -1;
    }

    let dec_ctx = unsafe { &mut *dec_ctx };

    // Check dtvcc pointer before dereferencing
    if dec_ctx.dtvcc.is_null() {
        return -1;
    }

    let mut ret = -1;
    let mut cc_data: Vec<u8> = (0..cc_count * 3)
        .map(|x| unsafe { *data.add(x as usize) })
        .collect();
    let dtvcc_ctx = unsafe { &mut *dec_ctx.dtvcc };
    let mut dtvcc = Dtvcc::new(dtvcc_ctx);
    for cc_block in cc_data.chunks_exact_mut(3) {
        if !validate_cc_pair(cc_block) {
            continue;
        }
        let success = do_cb_dtvcc(dec_ctx, &mut dtvcc, cc_block);
        if success {
            ret = 0;
        }
    }
    ret
}

/// Returns `true` if cc_block pair is valid
///
/// For CEA-708 data, only cc_valid is checked
/// For CEA-608 data, parity is also checked
pub fn validate_cc_pair(cc_block: &mut [u8]) -> bool {
    let cc_valid = (cc_block[0] & 4) >> 2;
    let cc_type = cc_block[0] & 3;
    if cc_valid == 0 {
        return false;
    }
    if cc_type == 0 || cc_type == 1 {
        // For CEA-608 data we verify parity.
        if verify_parity(cc_block[2]) {
            // If the second byte doesn't pass parity, ignore pair
            return false;
        }
        if verify_parity(cc_block[1]) {
            // If the first byte doesn't pass parity,
            // we replace it with a solid blank and process the pair.
            cc_block[1] = 0x7F;
        }
    }
    true
}

/// Returns `true` if data has odd parity
///
/// CC uses odd parity (i.e., # of 1's in byte is odd.)
pub fn verify_parity(data: u8) -> bool {
    if data.count_ones() & 1 == 1 {
        return true;
    }
    false
}

/// Process CC data according to its type
pub fn do_cb_dtvcc(ctx: &mut lib_cc_decode, dtvcc: &mut Dtvcc, cc_block: &[u8]) -> bool {
    let cc_valid = (cc_block[0] & 4) >> 2;
    let cc_type = cc_block[0] & 3;
    let mut timeok = true;

    if ctx.write_format != ccx_output_format::CCX_OF_DVDRAW
        && ctx.write_format != ccx_output_format::CCX_OF_RAW
        && (cc_block[0] == 0xFA || cc_block[0] == 0xFC || cc_block[0] == 0xFD)
        && (cc_block[1] & 0x7F) == 0
        && (cc_block[2] & 0x7F) == 0
    {
        return true;
    }

    if cc_valid == 1 || cc_type == 3 {
        ctx.cc_stats[cc_type as usize] += 1;
        match cc_type {
            // Type 0 and 1 are for CEA-608 data. Handled by C code, do nothing
            0 | 1 => {}
            // Type 2 and 3 are for CEA-708 data.
            2 | 3 => {
                let current_time = if ctx.timing.is_null() {
                    0
                } else {
                    unsafe { (*ctx.timing).get_fts(ctx.current_field as u8) }
                };
                ctx.current_field = 3;

                // Check whether current time is within start and end bounds
                if is_true(ctx.extraction_start.set)
                    && current_time < ctx.extraction_start.time_in_ms
                {
                    timeok = false;
                }
                if is_true(ctx.extraction_end.set) && current_time > ctx.extraction_end.time_in_ms {
                    timeok = false;
                    ctx.processed_enough = 1;
                }

                if timeok && ctx.write_format != ccx_output_format::CCX_OF_RAW {
                    dtvcc.process_cc_data(cc_valid, cc_type, cc_block[1], cc_block[2]);
                }
                unsafe { cb_708 += 1 }
            }
            _ => warn!("Invalid cc_type"),
        }
    }
    true
}

/// Close a Windows handle by wrapping it in a File and dropping it.
///
/// # Safety
///
/// - `handle` must be a valid Windows file handle or null
/// - If non-null, the handle must not be used after this call
/// - The handle must have been obtained from a valid file operation
/// - Passing an invalid (non-null) handle results in undefined behavior
#[cfg(windows)]
#[no_mangle]
extern "C" fn ccxr_close_handle(handle: RawHandle) {
    use std::fs::File;

    if handle.is_null() {
        return;
    }
    unsafe {
        // File will close automatically (due to Drop) once it goes out of scope
        let _file = File::from_raw_handle(handle);
    }
}

/// Normalize legacy single-dash long options to double-dash format.
///
/// Old versions of ccextractor accepted `-quiet`, `-stdout`, etc. but clap
/// requires `--quiet`, `--stdout`. This function converts single-dash long
/// options to double-dash for backward compatibility.
///
/// # Rules
/// - Single-dash options with multiple characters (e.g., `-quiet`) are converted to `--quiet`
/// - Double-dash options (e.g., `--quiet`) are left unchanged
/// - Single-letter short options (e.g., `-o`) are left unchanged
/// - Non-option arguments (e.g., `file.ts`) are left unchanged
/// - Numeric options (e.g., `-1`, `-12`) are left unchanged (these are valid short options)
fn normalize_legacy_option(arg: String) -> String {
    // Check if it's a single-dash option with multiple characters (e.g., -quiet)
    // but not a short option with a value (e.g., -o filename)
    // Single-letter options like -o, -s should be left unchanged
    // Numeric options like -1, -12 should also be left unchanged
    if arg.starts_with('-')
        && !arg.starts_with("--")
        && arg.len() > 2
        && arg.chars().nth(1).is_some_and(|c| c.is_ascii_alphabetic())
    {
        // Convert -option to --option
        format!("-{}", arg)
    } else {
        arg
    }
}

/// # Safety
/// Safe if argv is a valid pointer
///
/// Parse parameters from argv and argc
#[no_mangle]
pub unsafe extern "C" fn ccxr_parse_parameters(argc: c_int, argv: *mut *mut c_char) -> c_int {
    // Null pointer and bounds checks
    if argv.is_null() || argc <= 0 {
        return ExitCause::NoInputFiles.exit_code();
    }

    // Convert argv to Vec<String> and pass it to parse_parameters
    // Use to_string_lossy() to handle invalid UTF-8 gracefully instead of panicking
    let args = std::slice::from_raw_parts(argv, argc as usize)
        .iter()
        .map(|&arg| CStr::from_ptr(arg).to_string_lossy().into_owned())
        .collect::<Vec<String>>();

    if args.len() <= 1 {
        return ExitCause::NoInputFiles.exit_code();
    }

    // Backward compatibility: Convert single-dash long options to double-dash
    // Old versions of ccextractor accepted -quiet, -stdout, etc. but clap requires --quiet, --stdout
    // This allows scripts using the old syntax to continue working
    let args: Vec<String> = args.into_iter().map(normalize_legacy_option).collect();

    let args: Args = match Args::try_parse_from(args) {
        Ok(args) => args,
        Err(e) => {
            // Not all errors are actual errors, some are just help or version
            // So handle them accordingly
            match e.kind() {
                ErrorKind::DisplayHelp => {
                    // Print the help string
                    println!("{e}");
                    return ExitCause::WithHelp.exit_code();
                }
                ErrorKind::DisplayVersion => {
                    version(*argv);
                    return ExitCause::WithHelp.exit_code();
                }
                ErrorKind::UnknownArgument => {
                    println!("Unknown Argument");
                    println!("{e}");
                    return ExitCause::MalformedParameter.exit_code();
                }
                _ => {
                    println!("{e}");
                    return ExitCause::Failure.exit_code();
                }
            }
        }
    };

    let mut _capitalization_list: Vec<String> = Vec::new();
    let mut _profane: Vec<String> = Vec::new();

    let mut opt = Options::default();
    let mut _tlt_config = TeletextConfig::default();

    opt.parse_parameters(
        &args,
        &mut _tlt_config,
        &mut _capitalization_list,
        &mut _profane,
    );

    // Handle --list-tracks mode: list tracks and exit early
    if opt.list_tracks_only {
        use std::path::Path;
        use track_lister::list_tracks;

        let files = match &opt.inputfile {
            Some(f) if !f.is_empty() => f,
            _ => {
                eprintln!("Error: No input files specified for --list-tracks");
                return ExitCause::NoInputFiles.exit_code();
            }
        };

        let mut had_errors = false;
        for file in files {
            if let Err(e) = list_tracks(Path::new(file)) {
                eprintln!("Error listing tracks for '{}': {}", file, e);
                had_errors = true;
            }
        }

        // Exit with appropriate code - we don't want to continue to C processing
        return if had_errors {
            ExitCause::Failure.exit_code()
        } else {
            ExitCause::WithHelp.exit_code() // Reuse this code to indicate successful early exit
        };
    }

    tlt_config = _tlt_config.to_ctype(&opt);

    // Convert the rust struct (CcxOptions) to C struct (ccx_s_options), so that it can be used by the C code
    copy_from_rust(&raw mut ccx_options, opt);

    if !_capitalization_list.is_empty() {
        capitalization_list = _capitalization_list.to_ctype();
    }
    if !_profane.is_empty() {
        profane = _profane.to_ctype();
    }

    ExitCause::Ok.exit_code()
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_verify_parity() {
        // Odd parity
        assert!(verify_parity(0b1010001));

        // Even parity
        assert!(!verify_parity(0b1000001));
    }

    #[test]
    fn test_validate_cc_pair() {
        // Valid CEA-708 data
        let mut cc_block = [0x97, 0x1F, 0x3C];
        assert!(validate_cc_pair(&mut cc_block));

        // Invalid CEA-708 data
        let mut cc_block = [0x93, 0x1F, 0x3C];
        assert!(!validate_cc_pair(&mut cc_block));

        // Valid CEA-608 data
        let mut cc_block = [0x15, 0x2F, 0x7D];
        assert!(validate_cc_pair(&mut cc_block));
        // Check for replaced bit when 1st byte doesn't pass parity
        assert_eq!(cc_block[1], 0x7F);

        // Invalid CEA-608 data
        let mut cc_block = [0x15, 0x2F, 0x5E];
        assert!(!validate_cc_pair(&mut cc_block));
    }

    #[test]
    fn test_do_cb() {
        let mut dtvcc_ctx = crate::decoder::test::initialize_dtvcc_ctx();
        let mut dtvcc = Dtvcc::new(&mut dtvcc_ctx);
        let mut decoder_ctx = lib_cc_decode::default();
        let cc_block = [0x97, 0x1F, 0x3C];

        assert!(do_cb_dtvcc(&mut decoder_ctx, &mut dtvcc, &cc_block));
        assert_eq!(decoder_ctx.current_field, 3);
        assert_eq!(decoder_ctx.cc_stats[3], 1);
        assert_eq!(decoder_ctx.processed_enough, 0);
        assert_eq!(unsafe { cb_708 }, 11);
    }

    #[test]
    fn test_normalize_legacy_option_single_dash_long() {
        // Single-dash long options should be converted to double-dash
        assert_eq!(
            normalize_legacy_option("-quiet".to_string()),
            "--quiet".to_string()
        );
        assert_eq!(
            normalize_legacy_option("-stdout".to_string()),
            "--stdout".to_string()
        );
        assert_eq!(
            normalize_legacy_option("-autoprogram".to_string()),
            "--autoprogram".to_string()
        );
        assert_eq!(
            normalize_legacy_option("-goptime".to_string()),
            "--goptime".to_string()
        );
    }

    #[test]
    fn test_normalize_legacy_option_double_dash() {
        // Double-dash options should remain unchanged
        assert_eq!(
            normalize_legacy_option("--quiet".to_string()),
            "--quiet".to_string()
        );
        assert_eq!(
            normalize_legacy_option("--stdout".to_string()),
            "--stdout".to_string()
        );
        assert_eq!(
            normalize_legacy_option("--autoprogram".to_string()),
            "--autoprogram".to_string()
        );
    }

    #[test]
    fn test_normalize_legacy_option_short_options() {
        // Single-letter short options should remain unchanged
        assert_eq!(normalize_legacy_option("-o".to_string()), "-o".to_string());
        assert_eq!(normalize_legacy_option("-s".to_string()), "-s".to_string());
    }

    #[test]
    fn test_normalize_legacy_option_numeric_options() {
        // Numeric options should remain unchanged (these are valid ccextractor options)
        assert_eq!(normalize_legacy_option("-1".to_string()), "-1".to_string());
        assert_eq!(normalize_legacy_option("-2".to_string()), "-2".to_string());
        assert_eq!(
            normalize_legacy_option("-12".to_string()),
            "-12".to_string()
        );
    }

    #[test]
    fn test_normalize_legacy_option_non_options() {
        // Non-option arguments should remain unchanged
        assert_eq!(
            normalize_legacy_option("file.ts".to_string()),
            "file.ts".to_string()
        );
        assert_eq!(
            normalize_legacy_option("/path/to/file.ts".to_string()),
            "/path/to/file.ts".to_string()
        );
        assert_eq!(
            normalize_legacy_option("ccextractor".to_string()),
            "ccextractor".to_string()
        );
    }

    #[test]
    fn test_normalize_legacy_option_edge_cases() {
        // Empty string
        assert_eq!(normalize_legacy_option("".to_string()), "".to_string());
        // Just a dash
        assert_eq!(normalize_legacy_option("-".to_string()), "-".to_string());
        // Double dash alone (end of options marker)
        assert_eq!(normalize_legacy_option("--".to_string()), "--".to_string());
    }
}
