use crate::types::*;
#[allow(unused)]
use crate::utils::num_range::*;
use clap::{ArgAction, ArgGroup, Parser, Subcommand};

#[cfg(feature = "flate2")]
fn parse_compression_level(level: &str) -> Result<u32, String> {
    let lower = level.to_ascii_lowercase();
    if lower == "none" {
        return Ok(0);
    } else if lower == "best" {
        return Ok(9);
    } else if lower == "default" {
        return Ok(6);
    } else if lower == "fast" {
        return Ok(1);
    }
    number_range(level, 0, 9)
}

#[cfg(feature = "mozjpeg")]
fn parse_jpeg_quality(quality: &str) -> Result<u8, String> {
    let lower = quality.to_ascii_lowercase();
    if lower == "best" {
        return Ok(100);
    }
    number_range(quality, 0, 100)
}

#[cfg(feature = "zstd")]
fn parse_zstd_compression_level(level: &str) -> Result<i32, String> {
    let lower = level.to_ascii_lowercase();
    if lower == "default" {
        return Ok(3);
    } else if lower == "best" {
        return Ok(22);
    }
    number_range(level, 0, 22)
}

#[cfg(feature = "webp")]
fn parse_webp_quality(quality: &str) -> Result<u8, String> {
    let lower = quality.to_ascii_lowercase();
    if lower == "best" {
        return Ok(100);
    }
    number_range(quality, 0, 100)
}

#[cfg(feature = "audio-flac")]
fn parse_flac_compression_level(level: &str) -> Result<u32, String> {
    let lower = level.to_ascii_lowercase();
    if lower == "fast" {
        return Ok(0);
    } else if lower == "best" {
        return Ok(8);
    } else if lower == "default" {
        return Ok(5);
    }
    number_range(level, 0, 8)
}

#[cfg(feature = "image-jxl")]
fn parse_jxl_distance(s: &str) -> Result<f32, String> {
    let lower = s.to_ascii_lowercase();
    if lower == "lossless" {
        return Ok(0.0);
    } else if lower == "visually-lossless" {
        return Ok(1.0);
    }
    number_range(s, 0.0, 25.0)
}

#[cfg(feature = "musica-arc")]
pub fn get_musica_game_title_value_parser() -> Vec<clap::builder::PossibleValue> {
    crate::scripts::musica::archive::paz::get_supported_games_with_title()
        .iter()
        .map(|(name, title)| {
            let mut pv = clap::builder::PossibleValue::new(*name);
            if let Some(t) = title {
                pv = pv.help(t);
                let mut alias_count = 0usize;
                for i in t.split("|") {
                    pv = pv.alias(i.trim());
                    alias_count += 1;
                }
                // alias for full title
                if alias_count > 1 {
                    pv = pv.alias(t);
                }
            }
            pv
        })
        .collect()
}

#[cfg(feature = "kirikiri-arc")]
pub fn get_xp3_game_title_value_parser() -> Vec<clap::builder::PossibleValue> {
    crate::scripts::kirikiri::archive::xp3::get_supported_games_with_title()
        .iter()
        .map(|(name, title)| {
            let mut pv = clap::builder::PossibleValue::new(*name);
            if let Some(t) = title {
                pv = pv.help(t);
                let mut alias_count = 0usize;
                for i in t.split("|") {
                    pv = pv.alias(i.trim());
                    alias_count += 1;
                }
                // alias for full title
                if alias_count > 1 {
                    pv = pv.alias(t);
                }
            }
            pv
        })
        .collect()
}

/// Tools for export and import scripts
#[derive(Parser, Debug, Clone)]
#[clap(
    group = ArgGroup::new("encodingg").multiple(false),
    group = ArgGroup::new("output_encodingg").multiple(false),
    group = ArgGroup::new("archive_encodingg").multiple(false),
    group = ArgGroup::new("artemis_indentg").multiple(false),
    group = ArgGroup::new("ex_hibit_rld_xor_keyg").multiple(false),
    group = ArgGroup::new("ex_hibit_rld_def_xor_keyg").multiple(false),
    group = ArgGroup::new("webp_qualityg").multiple(false),
    group = ArgGroup::new("cat_system_int_encrypt_passwordg").multiple(false),
    group = ArgGroup::new("kirikiri_chat_jsong").multiple(false),
    group = ArgGroup::new("xp3-compression").multiple(false),
)]
#[command(
    version,
    about,
    long_about = "Tools for export and import scripts\nhttps://github.com/lifegpc/msg-tool"
)]
pub struct Arg {
    #[arg(short = 't', long, value_enum, global = true)]
    /// Script type
    pub script_type: Option<ScriptType>,
    #[arg(short = 'T', long, value_enum, global = true)]
    /// Output script type
    pub output_type: Option<OutputScriptType>,
    #[arg(short = 'n', long, global = true)]
    /// Disable extra extension when locating/export output script
    pub output_no_extra_ext: bool,
    #[cfg(feature = "image")]
    #[arg(short = 'i', long, value_enum, global = true)]
    /// Output image type
    pub image_type: Option<ImageOutputType>,
    #[arg(short = 'e', long, value_enum, global = true, group = "encodingg")]
    /// Script encoding
    pub encoding: Option<TextEncoding>,
    #[cfg(windows)]
    #[arg(short = 'c', long, value_enum, global = true, group = "encodingg")]
    /// Script code page
    pub code_page: Option<u32>,
    #[arg(
        short = 'E',
        long,
        value_enum,
        global = true,
        group = "output_encodingg"
    )]
    /// Output text encoding
    pub output_encoding: Option<TextEncoding>,
    #[cfg(windows)]
    #[arg(
        short = 'C',
        long,
        value_enum,
        global = true,
        group = "output_encodingg"
    )]
    /// Output code page
    pub output_code_page: Option<u32>,
    #[arg(
        short = 'a',
        long,
        value_enum,
        global = true,
        group = "archive_encodingg"
    )]
    /// Archive filename encoding
    pub archive_encoding: Option<TextEncoding>,
    #[cfg(windows)]
    #[arg(
        short = 'A',
        long,
        value_enum,
        global = true,
        group = "archive_encodingg"
    )]
    /// Archive code page
    pub archive_code_page: Option<u32>,
    #[cfg(feature = "circus")]
    #[arg(long, value_enum, global = true)]
    /// Circus Game
    pub circus_mes_type: Option<CircusMesType>,
    #[arg(short, long, action = ArgAction::SetTrue, global = true)]
    /// Search for script files in the directory recursively
    pub recursive: bool,
    #[arg(global = true, action = ArgAction::SetTrue, short, long)]
    /// Print backtrace on error
    pub backtrace: bool,
    #[cfg(feature = "escude-arc")]
    #[arg(long, action = ArgAction::SetTrue, global = true)]
    /// Whether to use fake compression for Escude archive
    pub escude_fake_compress: bool,
    #[cfg(feature = "escude")]
    #[arg(long, global = true)]
    /// The path to the Escude enum script file (enum_scr.bin)
    pub escude_enum_scr: Option<String>,
    #[cfg(feature = "escude")]
    #[arg(long, global = true)]
    /// Escude game title
    pub escude_op: Option<crate::scripts::escude::script::EscudeOp>,
    #[cfg(feature = "bgi")]
    #[arg(long, action = ArgAction::SetTrue, global = true)]
    /// Duplicate same strings when importing into BGI scripts.
    /// Enable this will cause BGI scripts to become very large.
    pub bgi_import_duplicate: bool,
    #[cfg(feature = "bgi")]
    #[arg(long, action = ArgAction::SetTrue, global = true, visible_alias = "bgi-no-append")]
    /// Disable appending new strings to the end of BGI scripts.
    /// Disable may cause BGI scripts broken.
    pub bgi_disable_append: bool,
    #[cfg(all(feature = "bgi-arc", feature = "bgi-img"))]
    #[arg(long, global = true)]
    /// Detect all files in BGI archive as SysGrp Images. By default, only files which name is `sysgrp.arc` will enabled this.
    pub bgi_is_sysgrp_arc: Option<bool>,
    #[cfg(feature = "bgi-img")]
    #[arg(long, global = true)]
    /// Whether to create scrambled SysGrp images. When in import mode, the default value depends on the original image.
    /// When in creation mode, it is not enabled by default.
    pub bgi_img_scramble: Option<bool>,
    #[cfg(feature = "bgi-img")]
    #[arg(long, global = true, default_value_t = crate::types::get_default_threads())]
    /// Workers count for decode BGI compressed images v2 in parallel. Default is half of CPU cores.
    /// Set this to 1 to disable parallel decoding. 0 means same as 1.
    pub bgi_img_workers: usize,
    #[cfg(feature = "cat-system-arc")]
    #[arg(long, global = true, group = "cat_system_int_encrypt_passwordg")]
    /// CatSystem2 engine int archive password
    pub cat_system_int_encrypt_password: Option<String>,
    #[cfg(feature = "cat-system-arc")]
    #[arg(long, global = true, group = "cat_system_int_encrypt_passwordg")]
    /// The path to the CatSystem2 engine executable file. Used to get the int archive password.
    pub cat_system_int_exe: Option<String>,
    #[cfg(feature = "cat-system-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Draw CatSystem2 image on canvas (if canvas width and height are specified in file)
    pub cat_system_image_canvas: bool,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true)]
    /// Kirikiri language index in script. If not specified, the first language will be used.
    pub kirikiri_language_index: Option<usize>,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true)]
    /// Export chat message to extra json file. (for Kirikiri SCN script.)
    /// For example, CIRCUS's comu message. Yuzusoft's phone chat message.
    pub kirikiri_export_chat: bool,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true, value_delimiter = ',')]
    /// Kirikiri chat message key. For example, CIRCUS's key is "comumode". Yuzusoft's key is "phonechat".
    /// If not specified, "comumode" will be used. Multiple keys can be specified separated by comma.
    pub kirikiri_chat_key: Option<Vec<String>>,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true, group = "kirikiri_chat_jsong")]
    /// Kirikiri chat message translation file. (Map<String, String>, key is original text, value is translated text.)
    pub kirikiri_chat_json: Option<String>,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true, group = "kirikiri_chat_jsong")]
    /// Kirikiri chat message translation directory. All json files in this directory will be merged. (Only m3t files are supported.)
    pub kirikiri_chat_dir: Option<String>,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true, value_delimiter = ',')]
    /// Kirikiri language list. First language code is code for language index 1.
    pub kirikiri_languages: Option<Vec<String>>,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true, action = ArgAction::SetTrue, visible_alias = "kr-title")]
    /// Whether to handle title in Kirikiri SCN script.
    pub kirikiri_title: bool,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true, action = ArgAction::SetTrue, visible_alias = "kr-chat-no-multilang")]
    /// Disable multi-language support for Kirikiri chat messages. (for Kirikiri SCN script.)
    pub kirikiri_chat_no_multilang: bool,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true, action = ArgAction::SetTrue, visible_alias = "kr-no-empty-lines", visible_alias = "kirikiri-no-empty-lines")]
    /// Remove empty lines in Kirikiri KS script.
    pub kirikiri_remove_empty_lines: bool,
    #[cfg(feature = "kirikiri")]
    #[arg(
        long,
        global = true,
        value_delimiter = ',',
        default_value = "nm,set_title,speaker,Talk,talk,cn,name,名前"
    )]
    /// Kirikiri name commands, used to extract names from ks script.
    pub kirikiri_name_commands: Vec<String>,
    #[cfg(feature = "kirikiri")]
    #[arg(
        long,
        global = true,
        value_delimiter = ',',
        default_value = "sel01,sel02,sel03,sel04,AddSelect,ruby,exlink,e_xlink"
    )]
    /// Kirikiri message commands, used to extract more message from ks script.
    pub kirikiri_message_commands: Vec<String>,
    #[cfg(feature = "image")]
    #[arg(short = 'f', long, global = true)]
    /// Output multiple image as `<basename>_<name>.<ext>` instead of `<basename>/<name>.<ext>`
    pub image_output_flat: bool,
    #[cfg(feature = "bgi-arc")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Whether to compress files in BGI archive when packing BGI archive.
    pub bgi_compress_file: bool,
    #[cfg(feature = "bgi-arc")]
    #[arg(long, global = true, default_value_t = 3, value_parser = crate::scripts::bgi::archive::dsc::parse_min_length)]
    /// Minimum length of match size for DSC compression. Possible values are 2-256.
    pub bgi_compress_min_len: usize,
    #[cfg(feature = "emote-img")]
    #[arg(long, global = true)]
    /// Whether to overlay PIMG images. (By default, true if all layers are not group layers.)
    pub emote_pimg_overlay: Option<bool>,
    #[cfg(feature = "artemis-arc")]
    #[arg(long, global = true)]
    /// Disable Artemis archive (.pfs) XOR encryption when packing.
    pub artemis_arc_disable_xor: bool,
    #[cfg(feature = "artemis")]
    #[arg(long, global = true, group = "artemis_indentg")]
    /// Artemis script indent size, used to format Artemis script.
    /// Default is 4 spaces.
    pub artemis_indent: Option<usize>,
    #[cfg(feature = "artemis")]
    #[arg(long, global = true, action = ArgAction::SetTrue, group = "artemis_indentg")]
    /// Disable Artemis script indent, used to format Artemis script.
    pub artemis_no_indent: bool,
    #[cfg(feature = "artemis")]
    #[arg(long, global = true, default_value_t = 100)]
    /// Max line width in Artemis script, used to format Artemis script.
    pub artemis_max_line_width: usize,
    #[cfg(feature = "artemis")]
    #[arg(long, global = true)]
    /// Specify the language of Artemis AST script.
    /// If not specified, the first language will be used.
    pub artemis_ast_lang: Option<String>,
    #[cfg(feature = "artemis")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Do not format lua code in Artemis ASB script(.asb/.iet) when exporting.
    pub artemis_asb_no_format_lua: bool,
    // Default value is from tagFilters in macro.iet
    #[cfg(feature = "artemis-panmimisoft")]
    #[arg(
        long,
        global = true,
        value_delimiter = ',',
        default_value = "背景,イベントCG,遅延背景,遅延背景予約,背景予約,遅延イベントCG,遅延イベントCG予約,イベントCG予約,遅延ポップアップ,遅延bgm_in,遅延bgm_out,遅延se_in,遅延se_out,遅延bgs_in,遅延bgs_out,立ち絵face非連動,セーブサムネイル置換終了,シネスコ,ポップアップ"
    )]
    /// Artemis Engine blacklist tag names for TXT script.
    /// This is used to ignore these tags when finding names in Artemis TXT script (ぱんみみそふと).
    pub artemis_panmimisoft_txt_blacklist_names: Vec<String>,
    #[cfg(feature = "artemis-panmimisoft")]
    #[arg(long, global = true)]
    /// Specify the language of Artemis TXT (ぱんみみそふと) script.
    /// If not specified, the first language will be used.
    pub artemis_panmimisoft_txt_lang: Option<String>,
    #[cfg(feature = "artemis-panmimisoft")]
    #[arg(long, global = true, action = ArgAction::SetTrue, requires = "artemis_panmimisoft_txt_lang")]
    /// Enable multiple language support for single language Artemis TXT (ぱんみみそふと) script.
    /// --artemis-panmimisoft-txt-lang must be set when enabling this.
    pub artemis_panmimisoft_txt_multi_lang: bool,
    #[cfg(feature = "artemis-panmimisoft")]
    #[arg(long, global = true)]
    /// The path to the tag.ini file, which contains the tags to be ignored when finding names in Artemis TXT script (ぱんみみそふと).
    pub artemis_panmimisoft_txt_tag_ini: Option<String>,
    #[cfg(feature = "cat-system")]
    #[arg(long, global = true)]
    /// CatSystem2 CSTL script language, used to extract messages from CSTL script.
    /// If not specified, the first language will be used.
    pub cat_system_cstl_lang: Option<String>,
    #[cfg(feature = "flate2")]
    #[arg(short = 'z', long, global = true, value_name = "LEVEL", value_parser = parse_compression_level, default_value_t = 6)]
    /// Zlib compression level. 0 means no compression, 9 means best compression.
    pub zlib_compression_level: u32,
    #[cfg(feature = "image")]
    #[arg(short = 'g', long, global = true, value_enum, default_value_t = PngCompressionLevel::Fast)]
    /// PNG compression level.
    pub png_compression_level: PngCompressionLevel,
    #[cfg(feature = "circus-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Keep original BPP when importing Circus CRX images.
    pub circus_crx_keep_original_bpp: bool,
    #[cfg(feature = "circus-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Use zstd compression for Circus CRX images. (CIRCUS Engine don't support this. Hook is required.)
    pub circus_crx_zstd: bool,
    #[cfg(feature = "zstd")]
    #[arg(short = 'Z', long, global = true, value_name = "LEVEL", value_parser = parse_zstd_compression_level, default_value_t = 3)]
    /// Zstd compression level. 0 means default compression level (3), 22 means best compression.
    pub zstd_compression_level: i32,
    #[cfg(feature = "circus-img")]
    #[arg(long, global = true, value_enum, default_value_t = crate::scripts::circus::image::crx::CircusCrxMode::Auto)]
    /// Circus CRX image row type mode
    pub circus_crx_mode: crate::scripts::circus::image::crx::CircusCrxMode,
    #[cfg(feature = "circus-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Draw Circus CRX images on canvas (if canvas width and height are specified in file)
    pub circus_crx_canvas: bool,
    #[arg(short = 'F', long, global = true, action = ArgAction::SetTrue)]
    /// Force all files in archive to be treated as script files.
    pub force_script: bool,
    #[cfg(feature = "ex-hibit")]
    #[arg(
        long,
        global = true,
        value_name = "HEX",
        group = "ex_hibit_rld_xor_keyg"
    )]
    /// ExHibit xor key for rld script, in hexadecimal format. (e.g. `12345678`)
    /// Use https://github.com/ZQF-ReVN/RxExHIBIT to find the key.
    pub ex_hibit_rld_xor_key: Option<String>,
    #[cfg(feature = "ex-hibit")]
    #[arg(
        long,
        global = true,
        value_name = "PATH",
        group = "ex_hibit_rld_xor_keyg"
    )]
    /// ExHibit rld xor key file, which contains the xor key in hexadecimal format. (e.g. `0x12345678`)
    pub ex_hibit_rld_xor_key_file: Option<String>,
    #[cfg(feature = "ex-hibit")]
    #[arg(
        long,
        global = true,
        value_name = "HEX",
        group = "ex_hibit_rld_def_xor_keyg"
    )]
    /// ExHibit rld def.rld xor key, in hexadecimal format. (e.g. `12345678`)
    pub ex_hibit_rld_def_xor_key: Option<String>,
    #[cfg(feature = "ex-hibit")]
    #[arg(
        long,
        global = true,
        value_name = "PATH",
        group = "ex_hibit_rld_def_xor_keyg"
    )]
    /// ExHibit rld def.rld xor key file, which contains the xor key in hexadecimal format. (e.g. `0x12345678`)
    pub ex_hibit_rld_def_xor_key_file: Option<String>,
    #[cfg(feature = "ex-hibit")]
    #[arg(long, global = true, value_name = "PATH")]
    /// Path to the ExHibit rld keys file, which contains the keys in BINARY format.
    /// Use https://github.com/ZQF-ReVN/RxExHIBIT to get this file.
    pub ex_hibit_rld_keys: Option<String>,
    #[cfg(feature = "ex-hibit")]
    #[arg(long, global = true, value_name = "PATH")]
    /// Path to the ExHibit rld def keys file, which contains the keys in BINARY format.
    pub ex_hibit_rld_def_keys: Option<String>,
    #[cfg(feature = "mozjpeg")]
    #[arg(long, global = true, default_value_t = 80, value_parser = parse_jpeg_quality)]
    /// JPEG quality for output images, 0-100. 100 means best quality.
    pub jpeg_quality: u8,
    #[cfg(feature = "webp")]
    #[arg(short = 'w', long, global = true, group = "webp_qualityg")]
    /// Use WebP lossless compression for output images.
    pub webp_lossless: bool,
    #[cfg(feature = "webp")]
    #[arg(short = 'W', long, global = true, value_name = "QUALITY", group = "webp_qualityg", value_parser = parse_webp_quality, default_value_t = 80)]
    /// WebP quality for output images, 0-100. 100 means best quality.
    pub webp_quality: u8,
    #[arg(long, global = true)]
    /// Try use YAML format instead of JSON when custom exporting.
    /// By default, this is based on output type. But can be overridden by this option.
    pub custom_yaml: Option<bool>,
    #[cfg(feature = "entis-gls")]
    #[arg(long, global = true)]
    /// Entis GLS srcxml script language, used to extract messages from srcxml script.
    /// If not specified, the first language will be used.
    pub entis_gls_srcxml_lang: Option<String>,
    #[cfg(feature = "will-plus")]
    #[arg(long, global = true)]
    /// Disable disassembly for WillPlus ws2 script.
    /// Use another parser to parse the script.
    /// Should only be used when the default parser not works well.
    pub will_plus_ws2_no_disasm: bool,
    #[cfg(feature = "lossless-audio")]
    #[arg(short = 'l', long, global = true, value_enum, default_value_t = LosslessAudioFormat::Wav)]
    /// Audio format for output lossless audio files.
    pub lossless_audio_fmt: LosslessAudioFormat,
    #[cfg(feature = "audio-flac")]
    #[arg(short = 'L', long, global = true, default_value_t = 5, value_parser = parse_flac_compression_level)]
    /// FLAC compression level for output FLAC audio files. 0 means fastest compression, 8 means best compression.
    pub flac_compression_level: u32,
    #[arg(long, global = true)]
    /// Add a mark to the end of each message for LLM translation.
    /// Only works on m3t format.
    pub llm_trans_mark: Option<String>,
    #[cfg(feature = "favorite")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Do not filter ascii strings in Favorite HCB script.
    pub favorite_hcb_no_filter_ascii: bool,
    #[cfg(feature = "image-jxl")]
    #[arg(long, global = true, action = ArgAction::SetTrue, visible_alias = "jxl-no-lossless")]
    /// Disable JXL lossless compression for output images
    pub jxl_lossy: bool,
    #[cfg(feature = "image-jxl")]
    #[arg(long, global = true, default_value_t = 1.0, value_parser = parse_jxl_distance)]
    /// JXL distance for output images. 0 means mathematically lossless compression. 1.0 means visually lossless compression.
    /// Allowed range is 0.0-25.0. Recommended range is 0.5-3.0. Default value is 1
    pub jxl_distance: f32,
    #[cfg(feature = "image-jxl")]
    #[arg(long, global = true, default_value_t = 1, visible_alias = "jxl-jobs")]
    /// Workers count for encode JXL images in parallel. Default is 1.
    /// Set this to 1 to disable parallel encoding. 0 means same as 1
    pub jxl_workers: usize,
    #[cfg(feature = "image")]
    #[arg(short = 'J', long, global = true, default_value_t = crate::types::get_default_threads(), visible_alias = "img-jobs", visible_alias = "img-workers", visible_alias = "image-jobs")]
    /// Workers count for encode images in parallel. Default is half of CPU cores.
    /// Set this to 1 to disable parallel encoding. 0 means same as 1.
    pub image_workers: usize,
    #[cfg(feature = "jieba")]
    #[arg(long, global = true)]
    /// Path to custom jieba dictionary
    pub jieba_dict: Option<String>,
    #[cfg(feature = "emote-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue, visible_alias = "psb-no-tlg")]
    /// Do not process TLG images in PSB files.
    pub psb_no_process_tlg: bool,
    #[cfg(feature = "softpal-img")]
    #[arg(long, global = true, visible_alias = "pgd-co")]
    /// Whether to use compression for Softpal Pgd images.
    /// WARN: Compress may cause image broken.
    pub pgd_compress: bool,
    #[arg(long, global = true)]
    /// Disable multiple messages section support.
    pub no_multi_message: bool,
    #[cfg(feature = "softpal")]
    #[arg(long, global = true, visible_alias = "softpal-idx")]
    /// Whether to add message index to Softpal src script when exporting.
    pub softpal_add_message_index: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true)]
    /// Disable decrypt SimpleCrypt files in Kirikiri XP3 archive when extracting.
    pub xp3_no_simple_crypt: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true)]
    /// Disable decompressing mdf files in Kirikiri XP3 archive when extracting.
    pub xp3_no_mdf_decompress: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true, default_value = "cdc:32KiB:256KiB:8MiB", value_parser = crate::scripts::kirikiri::archive::xp3::parse_segmenter_config)]
    /// Configuration for Kirikiri XP3 segmenter when creating XP3 archive.
    /// none segmenter - none
    /// fastcdc segmenter - cdc:<min>:<avg>:<max>
    /// fixed segmenter - fixed:<size>
    pub xp3_segmenter: crate::scripts::kirikiri::archive::xp3::SegmenterConfig,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true)]
    /// Disable compressing files in Kirikiri XP3 archive when creating XP3 archive.
    pub xp3_no_compress_files: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true)]
    /// Disable compressing index in Kirikiri XP3 archive when creating XP3 archive.
    pub xp3_no_compress_index: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true, default_value_t = num_cpus::get(), visible_alias = "xp3-compress-jobs")]
    /// Workers count for compress files in Kirikiri XP3 archive when creating in parallel.
    pub xp3_compress_workers: usize,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true, group = "xp3-compression")]
    /// Use zstd compression for files in Kirikiri XP3 archive when creating. (Warning: Kirikiri engine don't support this. Hook is required.)
    pub xp3_zstd: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true, group = "xp3-compression")]
    /// Use zopfli compression for files in Kirikiri XP3 archive when creating. This is very slow.
    pub xp3_zopfli: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(
        long,
        global = true,
        default_value_t = 1,
        visible_alias = "xp3-pack-jobs"
    )]
    /// Workers count for packing file in Kirikiri XP3 archive in parallel.
    /// This not works when segment is disabled.
    pub xp3_pack_workers: usize,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true)]
    /// Insert new language at the specified index in Kirikiri SCN script. If index is out of bounds, this flags will be ignored.
    pub kirikiri_language_insert: bool,
    #[cfg(feature = "musica-arc")]
    #[arg(long, global = true, value_parser = get_musica_game_title_value_parser())]
    /// Musica game title for paz archive.
    pub musica_game_title: Option<String>,
    #[cfg(feature = "musica-arc")]
    #[arg(long, global = true)]
    /// Musica xor key for paz archive.
    pub musica_xor_key: Option<u8>,
    #[cfg(feature = "musica-arc")]
    #[arg(long, global = true)]
    /// Compress files in Musica paz archive when packing paz archive.
    pub musica_compress: bool,
    #[arg(short = 'x', long, default_value_t = 0, global = true)]
    /// Exit code when some jobs failed
    pub exit_code: i32,
    #[arg(short = 'X', long, global = true)]
    /// Exit code when all jobs failed. By default, this is same as exit_code. This can override exit_code when all jobs failed.
    pub exit_code_all_failed: Option<i32>,
    #[arg(long, global = true)]
    /// Do not add quote to translated text when exporting to m3t files.
    pub m3t_no_quote: bool,
    #[arg(long, global = true)]
    /// Use original text as translated text if translated text and llm text are all empty.
    pub m3t_use_original_text: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true)]
    /// Disable adler32 checksum for Kirikiri XP3 archive when creating.
    /// This will keep compatibility with https://github.com/arcusmaximus/KirikiriTools tool.
    pub xp3_no_adler: bool,
    #[cfg(feature = "bgi")]
    #[arg(long, global = true)]
    /// Add an additional space at the end of message in BGI scripts when importing.
    /// This may help BGI engine to display the message correctly in save/load screen for some games.
    pub bgi_add_space: bool,
    #[cfg(feature = "zopfli")]
    #[arg(long, global = true, default_value_t = std::num::NonZeroU64::new(15).unwrap(), visible_alias = "zp-ic")]
    /// Maximum amount of times to rerun forward and backward pass to optimize LZ77 compression cost.
    /// Good values: 10, 15 for small files, 5 for files over several MB in size or it will be too slow.
    /// Default is 15.
    pub zopfli_iteration_count: std::num::NonZeroU64,
    #[cfg(feature = "zopfli")]
    #[arg(long, global = true, default_value_t = std::num::NonZeroU64::new(u64::MAX).unwrap(), visible_alias = "zp-iwi")]
    /// Stop after rerunning forward and backward pass this many times without finding a smaller representation of the block.
    /// Default value: practically infinite (maximum u64 value)
    pub zopfli_iterations_without_improvement: std::num::NonZeroU64,
    #[cfg(feature = "zopfli")]
    #[arg(long, global = true, default_value_t = 15, visible_alias = "zp-mbs")]
    /// Maximum amount of blocks to split into (0 for unlimited, but this can give extreme results that hurt compression on some files).
    /// Default value: 15.
    pub zopfli_maximum_block_splits: u16,
    #[cfg(feature = "entis-gls")]
    #[arg(long, global = true, action = ArgAction::SetTrue, alias = "entis-gls-csx-diasm")]
    /// Disassemble Entis GLS csx script when exporting in custom mode.
    pub entis_gls_csx_disasm: bool,
    #[cfg(feature = "entis-gls")]
    #[arg(long, global = true, default_value = "／")]
    /// The line feed character used in Entis GLS csx script.
    pub entis_gls_csx_lf: String,
    #[cfg(feature = "entis-gls")]
    #[arg(long, global = true)]
    /// Entis GLS csx script version.
    /// If not specified. Will try use lower version first.
    pub entis_gls_csx_ver: Option<crate::scripts::entis_gls::csx::CSXScriptVersion>,
    #[cfg(feature = "entis-gls")]
    #[arg(long, global = true)]
    /// Entis GLS csx script version2 full version.
    /// If not specified. Will try use higher version first.
    pub entis_gls_csx_v2_ver: Option<crate::scripts::entis_gls::csx::CSXScriptV2FullVer>,
    #[cfg(feature = "entis-gls")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Disable part labels in Entis GLS csx script when exporting.
    pub entis_gls_csx_no_part_label: bool,
    #[cfg(feature = "qlie-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Disable process ABMP10 images in ABMP10 images.
    pub qlie_abmp10_no_process_abmp10: bool,
    #[cfg(feature = "qlie-arc")]
    #[arg(long, global = true)]
    /// Path to qlie pack archive key file (pack_keyfile_kfueheish15538fa9or.key)
    pub qlie_pack_keyfile: Option<String>,
    #[cfg(feature = "qlie-arc")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Whether to compress files in Qlie pack archive.
    pub qlie_pack_compress_files: bool,
    #[cfg(feature = "qlie-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Whether to use PNG file directly for Qlie DPNG images when importing.
    /// Enable this will disable reencoding PNG files. Useful when the PNG files are already optimized by other tools.
    pub qlie_dpng_use_raw_png: bool,
    #[cfg(feature = "qlie-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Export Qlie DPNG images as PSD files.
    pub qlie_dpng_psd: bool,
    #[cfg(feature = "utils-psd")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Whether to disable compression for image data in PSD files.
    pub psd_no_compress: bool,
    #[cfg(feature = "emote-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Export Emote PIMG images as PSD files.
    pub emote_pimg_psd: bool,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true)]
    /// Whether to only extract message between Talk and Hitret command in Kirikiri KS script. Auto detect if not specified.
    pub kirikiri_ks_hitret: Option<bool>,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true)]
    /// The line feed character used in Kirikiri KS script.
    pub kirikiri_ks_lf: Option<String>,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true, value_delimiter = ',', default_value = "macCmd")]
    /// Kirikiri message tags, used to extract more message from ks script.
    pub kirikiri_message_tags: Vec<String>,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true)]
    /// Specifiy BOM type when creating new Kirikiri ks script. If not specified, detect from original script.
    pub kirikiri_ks_bom: Option<BomType>,
    #[cfg(feature = "emote-img")]
    #[arg(long, global = true, value_enum, default_value_t = crate::scripts::emote::psb::BC7Config::default())]
    /// BC7 compress configuration
    pub bc7: crate::scripts::emote::psb::BC7Config,
    #[cfg(feature = "artemis")]
    #[arg(
        long,
        global = true,
        value_delimiter = ',',
        default_value = "click,hcls,rpx"
    )]
    /// A list of Artemis ASB script end tags, used to determine a dialogue block in script.
    pub artemis_asb_end_tags: Vec<String>,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true, value_parser = get_xp3_game_title_value_parser())]
    /// Game title for Kirikiri XP3 archive. This is used to decrypt file in archives.
    pub xp3_game_title: Option<String>,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true)]
    /// Print/write debug information for Kirikiri XP3 archive when extracting archive to specifiy location.
    /// This is used to find correct configuration for unknown XP3 archives.
    pub xp3_debug_archive: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true)]
    /// Force extract encrypted files in Kirikiri XP3 archive without decryption.
    pub xp3_force_extract: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true, visible_alias = "xp3-fd")]
    /// Force decrypt files in Kirikiri xp3 archive even when flags are not set.
    /// Some encrypted files in Kirikiri XP3 archive may not set encryption flag, but still encrypted. Enable this to force decrypt these files.
    pub xp3_force_decrypt: bool,
    #[cfg(feature = "emote-img")]
    #[arg(long, global = true)]
    /// Disable diff handle when exporting Emote PIMG images to PSD files.
    /// If enabled, no group layers will be crated if both layer don't have diff_id and group_layer_id attribute.
    pub emote_pimg_psd_no_diff: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true)]
    /// The path to the file list for Kirikiri XP3 archive. This is used to recover file names from hashed values.
    /// Only works with some encyrption methods.
    pub xp3_file_list_path: Option<String>,
    #[command(subcommand)]
    /// Command
    pub command: Command,
}

#[derive(Parser, Debug, Clone)]
#[clap(group = ArgGroup::new("patched_encodingg").multiple(false), group = ArgGroup::new("patched_archive_encodingg").multiple(false))]
pub struct ImportArgs {
    /// Input script file or directory
    pub input: String,
    /// Text file or directory
    pub output: String,
    /// Patched script file or directory
    pub patched: String,
    #[arg(short = 'p', long, group = "patched_encodingg")]
    /// Patched script encoding
    pub patched_encoding: Option<TextEncoding>,
    #[cfg(windows)]
    #[arg(short = 'P', long, group = "patched_encodingg")]
    /// Patched script code page
    pub patched_code_page: Option<u32>,
    #[arg(
        long,
        value_enum,
        group = "patched_archive_encodingg",
        visible_alias = "pa"
    )]
    /// Patched archive filename encoding
    pub patched_archive_encoding: Option<TextEncoding>,
    #[cfg(windows)]
    #[arg(
        long,
        value_enum,
        group = "patched_archive_encodingg",
        visible_alias = "PA"
    )]
    /// Patched archive code page
    pub patched_archive_code_page: Option<u32>,
    #[arg(long)]
    /// Patched script format type
    pub patched_format: Option<FormatType>,
    #[arg(long)]
    /// Fixed length of one line in patched script (for fixed format)
    pub patched_fixed_length: Option<usize>,
    #[arg(long, action = ArgAction::SetTrue)]
    /// Keep original line breaks in patched script (for fixed format)
    pub patched_keep_original: bool,
    #[arg(long, action = ArgAction::SetTrue)]
    /// Break words in patched script (for fixed format)
    pub patched_break_words: bool,
    #[arg(long, action = ArgAction::SetTrue)]
    /// Insert fullwidth space at the start of line in patched script (for fixed format)
    pub patched_insert_fullwidth_space_at_line_start: bool,
    #[arg(long, action = ArgAction::SetTrue)]
    /// If a line break occurs in the middle of some symbols, bring the sentence to next line (for fixed format)
    pub patched_break_with_sentence: bool,
    #[cfg(feature = "jieba")]
    #[arg(long, action = ArgAction::SetTrue)]
    /// Whether to disable break Chinese words at the end of the line.
    pub patched_no_break_chinese_words: bool,
    #[arg(long, action = ArgAction::SetTrue)]
    /// Do not remove space at the start of the line
    pub patched_no_remove_space_at_line_start: bool,
    #[arg(long)]
    /// Name table file
    pub name_csv: Option<String>,
    #[arg(long)]
    /// Replacement table file
    pub replacement_json: Option<String>,
    #[arg(long, action = ArgAction::SetTrue)]
    pub warn_when_output_file_not_found: bool,
    #[arg(long)]
    /// Output dependency file path. This file will contain a list of all files used during import.
    pub dep_file: Option<String>,
    #[arg(short = 'j', long, default_value_t = 1)]
    /// Workers count for import scripts in parallel.
    pub jobs: usize,
}

#[derive(Subcommand, Debug, Clone)]
/// Commands
pub enum Command {
    /// Extract from script
    Export {
        /// Input script file or directory
        input: String,
        /// Output file or directory
        output: Option<String>,
    },
    /// Import to script
    Import(ImportArgs),
    /// Pack files to archive
    Pack {
        /// Input directory
        input: String,
        /// Output archive file
        output: Option<String>,
        #[arg(long)]
        /// Use \ as path separator instead of / in archive
        backslash: bool,
    },
    /// Unpack archive to directory
    Unpack {
        /// Input archive file
        input: String,
        /// Output directory
        output: Option<String>,
    },
    /// Create a new script file
    Create {
        /// Input script
        input: String,
        /// Output script file
        output: Option<String>,
    },
    /// Pack files to archive (version 2)
    PackV2 {
        #[arg(short = 'o', long)]
        /// Output archive file
        output: Option<String>,
        /// Path to input files/directories
        input: Vec<String>,
        #[arg(long)]
        /// Use \ as path separator instead of / in archive
        backslash: bool,
        #[arg(long)]
        /// Do not create directory entries in archive. This means all files are stored in a flat structure.
        no_dir: bool,
        #[arg(long)]
        /// Output dependency file path. This file will contain a list of all files packed in the archive.
        dep_file: Option<String>,
    },
    /// Convert output script to another format
    Convert {
        /// Input script format type
        input_type: OutputScriptType,
        /// Output script format type
        output_type: OutputScriptType,
        /// Input script file
        input: String,
        /// Output script file
        output: Option<String>,
    },
}

pub fn parse_args() -> Arg {
    Arg::parse()
}

#[cfg(feature = "ex-hibit")]
pub fn load_ex_hibit_rld_xor_key(arg: &Arg) -> anyhow::Result<Option<u32>> {
    if let Some(key) = &arg.ex_hibit_rld_xor_key {
        if key.starts_with("0x") {
            return Ok(Some(u32::from_str_radix(&key[2..], 16)?));
        } else {
            return Ok(Some(u32::from_str_radix(key, 16)?));
        }
    }
    if let Some(file) = &arg.ex_hibit_rld_xor_key_file {
        let key = std::fs::read_to_string(file)?.trim().to_string();
        if key.starts_with("0x") {
            return Ok(Some(u32::from_str_radix(&key[2..], 16)?));
        } else {
            return Ok(Some(u32::from_str_radix(&key, 16)?));
        }
    }
    Ok(None)
}

#[cfg(feature = "ex-hibit")]
pub fn load_ex_hibit_rld_def_xor_key(arg: &crate::args::Arg) -> anyhow::Result<Option<u32>> {
    if let Some(key) = &arg.ex_hibit_rld_def_xor_key {
        if key.starts_with("0x") {
            return Ok(Some(u32::from_str_radix(&key[2..], 16)?));
        } else {
            return Ok(Some(u32::from_str_radix(key, 16)?));
        }
    }
    if let Some(file) = &arg.ex_hibit_rld_def_xor_key_file {
        let key = std::fs::read_to_string(file)?.trim().to_string();
        if key.starts_with("0x") {
            return Ok(Some(u32::from_str_radix(&key[2..], 16)?));
        } else {
            return Ok(Some(u32::from_str_radix(&key, 16)?));
        }
    }
    Ok(None)
}

#[cfg(feature = "cat-system-arc")]
pub fn get_cat_system_int_encrypt_password(arg: &Arg) -> anyhow::Result<Option<String>> {
    if let Some(exe) = &arg.cat_system_int_exe {
        return Ok(Some(
            crate::scripts::cat_system::archive::int::get_password_from_exe(exe)?,
        ));
    }
    if let Some(password) = &arg.cat_system_int_encrypt_password {
        return Ok(Some(password.clone()));
    }
    Ok(None)
}

#[cfg(feature = "artemis-panmimisoft")]
pub fn get_artemis_panmimisoft_txt_blacklist_names(
    arg: &Arg,
) -> anyhow::Result<std::collections::HashSet<String>> {
    match &arg.artemis_panmimisoft_txt_tag_ini {
        Some(path) => {
            let mut set = crate::scripts::artemis::panmimisoft::txt::read_tags_from_ini(path)?;
            for name in &arg.artemis_panmimisoft_txt_blacklist_names {
                set.insert(name.clone());
            }
            Ok(set)
        }
        None => Ok(arg
            .artemis_panmimisoft_txt_blacklist_names
            .iter()
            .cloned()
            .collect()),
    }
}

#[cfg(feature = "kirikiri")]
pub fn load_kirikiri_chat_json(
    arg: &Arg,
) -> anyhow::Result<
    Option<
        std::sync::Arc<
            std::collections::HashMap<String, std::collections::HashMap<String, (String, usize)>>,
        >,
    >,
> {
    if let Some(path) = &arg.kirikiri_chat_json {
        return Ok(Some(std::sync::Arc::new(
            crate::scripts::kirikiri::read_kirikiri_comu_json(path)?
                .into_iter()
                .map(|(k, v)| {
                    let v: std::collections::HashMap<_, _> =
                        v.into_iter().map(|(k, v)| (k, (v, 1))).collect();
                    (k, v)
                })
                .collect(),
        )));
    }
    if let Some(dir) = &arg.kirikiri_chat_dir {
        let mut outt = arg.output_type.unwrap_or(OutputScriptType::M3t);
        if !matches!(
            outt,
            OutputScriptType::M3t
                | OutputScriptType::M3ta
                | OutputScriptType::M3tTxt
                | OutputScriptType::Po
                | OutputScriptType::Pot
        ) {
            outt = OutputScriptType::M3t;
        }
        let files = crate::utils::files::find_ext_files(dir, arg.recursive, &[outt.as_ref()])?;
        if !files.is_empty() {
            let mut map = std::collections::HashMap::new();
            let mut global: std::collections::HashMap<
                String,
                (String, std::collections::HashSet<String>),
            > = std::collections::HashMap::new();
            for file in files {
                let f = crate::utils::files::read_file(&file)?;
                let data = crate::utils::encoding::decode_to_string(
                    crate::get_output_encoding(arg),
                    &f,
                    true,
                )?;
                let m3t = if outt.is_m3t() {
                    crate::output_scripts::m3t::M3tParser::new(
                        &data,
                        arg.llm_trans_mark.as_ref().map(|s| s.as_str()),
                        arg.m3t_use_original_text,
                    )
                    .parse_as_vec()?
                } else {
                    crate::output_scripts::po::PoParser::new(
                        &data,
                        arg.llm_trans_mark.as_ref().map(|s| s.as_str()),
                    )
                    .parse_as_vec()?
                };
                let current_key = std::path::Path::new(&file)
                    .file_stem()
                    .and_then(|s| s.to_str())
                    .unwrap_or("unknown")
                    .to_string();
                let mut entry: std::collections::HashMap<
                    String,
                    (String, std::collections::HashSet<String>),
                > = std::collections::HashMap::new();
                for (k, v) in m3t {
                    if v.is_empty() {
                        continue;
                    }
                    let k = k.replace("\\[", "[");
                    let v = v.replace("\\[", "[");
                    if let Some((_, count)) = entry.get_mut(&k) {
                        count.insert(v.clone());
                    } else {
                        entry.insert(
                            k.clone(),
                            (v.clone(), std::collections::HashSet::from_iter([v.clone()])),
                        );
                    }
                    if let Some((_, count)) = global.get_mut(&k) {
                        count.insert(v.clone());
                    } else {
                        global.insert(
                            k,
                            (v.clone(), std::collections::HashSet::from_iter([v.clone()])),
                        );
                    }
                }
                map.insert(current_key, entry);
            }
            map.insert("global".to_string(), global);
            return Ok(Some(std::sync::Arc::new(
                map.into_iter()
                    .map(|(k, v)| {
                        let v: std::collections::HashMap<_, _> =
                            v.into_iter().map(|(k, (v, s))| (k, (v, s.len()))).collect();
                        (k, v)
                    })
                    .collect(),
            )));
        }
    }
    Ok(None)
}
