// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

// cSpell: ignore dedupe
use clap::{Parser, ValueEnum};
use i_slint_compiler::diagnostics::BuildDiagnostics;
use i_slint_compiler::*;
use itertools::Itertools;
use std::io::Cursor;
use std::io::Write;

#[cfg(all(
    feature = "jemalloc",
    not(any(
        target_os = "openbsd",
        target_os = "windows",
        all(target_arch = "aarch64", target_os = "linux")
    ))
))]
use tikv_jemallocator::Jemalloc;

#[cfg(all(
    feature = "jemalloc",
    not(any(
        target_os = "openbsd",
        target_os = "windows",
        all(target_arch = "aarch64", target_os = "linux")
    ))
))]
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum Embedding {
    /// Embed resources using absolute paths on the build system (alias: false)
    #[value(alias = "false")]
    AsAbsolutePath,
    /// Embed contents of resource files (alias: true)
    #[value(alias = "true")]
    EmbedFiles,
    /// Embed in a format optimized for the software renderer. This
    /// option falls back to `embed-files` if the renderer-software feature is not
    /// used
    #[cfg(feature = "renderer-software")]
    EmbedForSoftwareRenderer,
    /// Same as "embed-files-for-software-renderer" but use Signed Distance Field (SDF) to render fonts.
    /// This produces smaller binaries, but may result in slightly inferior visual output and slower rendering.
    #[cfg(all(feature = "renderer-software", feature = "sdf-fonts"))]
    EmbedForSoftwareRendererWithSdf,
}

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// Set the output format for generated code.
    /// Possible values: 'cpp' for C++ code or 'rust' for Rust code.
    #[arg(short = 'f', long = "format")]
    format: Option<generator::OutputFormat>,

    /// Compile in safety-critical mode using the Slint SC subset.
    #[cfg(feature = "slint-sc")]
    #[arg(long = "slint-sc")]
    slint_sc: bool,

    /// Specify include paths for imported .slint files or image resources.
    /// This is used for including external .slint files or image resources referenced by '@image-url'.
    #[arg(short = 'I', name = "include path", number_of_values = 1)]
    include_paths: Vec<std::path::PathBuf>,

    /// Define library paths in the format `<library>=<path>`.
    /// This can point to either a library directory or a .slint entry-point file.
    #[arg(short = 'L', name = "library path", number_of_values = 1)]
    library_paths: Vec<String>,

    /// Specify the path to the main .slint file to compile.
    /// Use '-' to read from stdin.
    #[arg(name = "file")]
    path: std::path::PathBuf,

    /// Set the style for the UI (e.g., 'native' or 'fluent').
    #[arg(long, name = "style name")]
    style: Option<String>,

    /// Apply a constant scale factor to embedded assets, typically for high-DPI displays.
    /// This scale factor is also applied to the window by default.
    #[arg(long, name = "scale factor")]
    scale_factor: Option<f32>,

    /// Generate a dependency file for build systems like CMake or Ninja.
    /// This file is similar to the output of `gcc -M`.
    #[arg(long = "depfile", name = "dependency file", number_of_values = 1)]
    depfile: Option<std::path::PathBuf>,

    /// Declare which resources to embed into the final output.
    #[arg(long, name = "value", value_enum)]
    embed_resources: Option<Embedding>,

    /// Set the output file for the generated code.
    /// Use '-' to output to stdout.
    #[arg(short = 'o', name = "output file", default_value = "-")]
    output: std::path::PathBuf,

    /// Set the translation domain for translatable strings.
    /// This is used to manage translation of strings in the UI.
    #[arg(long = "translation-domain")]
    translation_domain: Option<String>,

    /// Bundle translations from a specified path.
    /// Translation files should be in the gettext `.po` format and follow the directory structure:
    /// `<path>/<lang>/LC_MESSAGES/<domain>.po`.
    #[cfg(feature = "bundle-translations")]
    #[arg(long = "bundle-translations", name = "path")]
    bundle_translations: Option<std::path::PathBuf>,

    /// Disable the default to use the component name as translation context when none is specified in `@tr`
    #[cfg(feature = "bundle-translations")]
    #[arg(long = "no-default-translation-context")]
    no_default_translation_context: bool,

    /// Define the C++ namespace for generated code.
    #[cfg(feature = "cpp")]
    #[arg(long = "cpp-namespace", name = "C++ namespace")]
    cpp_namespace: Option<String>,

    /// Specify C++ files to generate.
    ///
    /// The header file (.h) is always generated in the file specified by `-o`.
    /// If `--cpp-file` is not set, all code will be generated in the header file.
    /// If set, function definitions are placed in the specified `.cpp` file.
    /// If specified multiple times, the definitions are split across multiple `.cpp` files.
    #[cfg(feature = "cpp")]
    #[arg(long = "cpp-file", name = "output .cpp file", number_of_values = 1)]
    cpp_files: Vec<std::path::PathBuf>,
}

fn main() -> std::io::Result<()> {
    proc_macro2::fallback::force(); // avoid a abort if panic=abort is set
    let args = Cli::parse();
    let mut diag = BuildDiagnostics::default();
    let syntax_node = parser::parse_file(&args.path, &mut diag);
    //println!("{:#?}", syntax_node);
    if diag.has_errors() {
        diag.print();
        std::process::exit(-1);
    }

    #[cfg(feature = "slint-sc")]
    if args.slint_sc {
        let reject = |cond: bool, flag: &str| {
            if cond {
                eprintln!("--slint-sc cannot be used together with {flag}");
                std::process::exit(1);
            }
        };
        reject(args.format.is_some(), "--format");
        reject(args.style.is_some(), "--style");
        // An import resolves relative to the importing file only.
        reject(!args.include_paths.is_empty(), "-I");
        reject(!args.library_paths.is_empty(), "-L");
        reject(args.scale_factor.is_some(), "--scale-factor");
        reject(args.embed_resources.is_some(), "--embed-resources");
        reject(args.translation_domain.is_some(), "--translation-domain");
        #[cfg(feature = "bundle-translations")]
        reject(args.bundle_translations.is_some(), "--bundle-translations");
        #[cfg(feature = "bundle-translations")]
        reject(args.no_default_translation_context, "--no-default-translation-context");
        #[cfg(feature = "cpp")]
        reject(args.cpp_namespace.is_some(), "--cpp-namespace");
        #[cfg(feature = "cpp")]
        reject(!args.cpp_files.is_empty(), "--cpp-file");
    }

    #[allow(unused_mut)]
    let mut format = args.format.clone().unwrap_or_else(|| {
        #[cfg(feature = "slint-sc")]
        if args.slint_sc {
            return generator::OutputFormat::SlintSc;
        }
        match std::path::Path::new(&args.output).extension().and_then(|ext| ext.to_str()) {
            #[cfg(feature = "rust")]
            Some("rs") => generator::OutputFormat::Rust,
            #[cfg(feature = "python")]
            Some("py") => generator::OutputFormat::Python,
            #[cfg(feature = "cpp")]
            _ => generator::OutputFormat::Cpp(Default::default()),
            #[cfg(not(feature = "cpp"))]
            _ => {
                eprintln!(
                    "Cannot guess output format from file extension. Use --format to specify."
                );
                std::process::exit(1);
            }
        }
    });

    #[cfg(feature = "cpp")]
    if args.cpp_namespace.is_some() {
        if !matches!(format, generator::OutputFormat::Cpp(..)) {
            eprintln!("C++ namespace option was set. Output format will be C++.");
        }
        format = generator::OutputFormat::Cpp(generator::cpp::Config {
            namespace: args.cpp_namespace,
            ..Default::default()
        });
    }

    #[cfg(feature = "cpp")]
    if !args.cpp_files.is_empty() {
        match &mut format {
            generator::OutputFormat::Cpp(config) => {
                config.cpp_files = args.cpp_files;

                if args.output == std::path::Path::new("-") {
                    eprintln!("--cpp-file can only be used together with -o");
                    std::process::exit(1);
                }

                config.header_include = args.output.to_string_lossy().to_string();
            }
            _ => {
                eprintln!("C++ files option was set but the output format is not C++ - ignoring");
            }
        }
    }

    let mut compiler_config = CompilerConfiguration::new(format.clone());
    compiler_config.translation_domain = args.translation_domain;
    #[cfg(feature = "bundle-translations")]
    if args.no_default_translation_context {
        compiler_config.default_translation_context =
            i_slint_compiler::DefaultTranslationContext::None;
    }

    // Override defaults from command line:
    if let Some(embed) = args.embed_resources {
        compiler_config.embed_resources = match embed {
            Embedding::AsAbsolutePath => EmbedResourcesKind::OnlyBuiltinResources,
            Embedding::EmbedFiles => EmbedResourcesKind::EmbedAllResources,
            #[cfg(feature = "renderer-software")]
            Embedding::EmbedForSoftwareRenderer => EmbedResourcesKind::EmbedTextures,
            #[cfg(all(feature = "renderer-software", feature = "sdf-fonts"))]
            Embedding::EmbedForSoftwareRendererWithSdf => {
                compiler_config.use_sdf_fonts = true;
                EmbedResourcesKind::EmbedTextures
            }
        };
    }

    compiler_config.include_paths = args.include_paths;
    compiler_config.library_paths = args
        .library_paths
        .iter()
        .filter_map(|entry| entry.split('=').collect_tuple().map(|(k, v)| (k.into(), v.into())))
        .collect();
    if let Some(style) = args.style {
        compiler_config.style = Some(style);
    }
    if let Some(constant_scale_factor) = args.scale_factor {
        compiler_config.const_scale_factor = Some(constant_scale_factor);
    }
    #[cfg(feature = "bundle-translations")]
    if let Some(path) = args.bundle_translations {
        compiler_config.translation_path_bundle = Some(path);
    }
    let syntax_node = syntax_node.expect("diags contained no compilation errors");
    let (doc, diag, loader) =
        spin_on::spin_on(compile_syntax_node(syntax_node, diag, compiler_config));

    let diag = diag.check_and_exit_on_error();

    if args.output == std::path::Path::new("-") {
        generator::generate(format, &mut std::io::stdout(), None, &doc, &loader.compiler_config)?;
    } else {
        let mut cursor = Cursor::new(Vec::new());
        generator::generate(
            format,
            &mut cursor,
            Some(&args.output),
            &doc,
            &loader.compiler_config,
        )?;
        // Important: Write without unnecessary mtime modification to avoid
        // build systems to always detect the generated file as modified.
        fileaccess::write_file_if_changed(&args.output, &cursor.into_inner())?;
    }

    if let Some(depfile) = args.depfile {
        let mut cursor = Cursor::new(Vec::new());
        write!(cursor, "{}: {}", args.output.display(), args.path.display())?;
        for x in &diag.all_loaded_files {
            if x.is_absolute() {
                write!(cursor, " {}", x.display())?;
            }
        }
        // A variable font is stored once per weight, so dedupe here.
        let embedded = doc.embedded_file_resources.borrow();
        let resources: std::collections::BTreeSet<&str> = embedded
            .iter()
            .filter_map(|er| er.path.as_deref())
            .filter(|resource| !resource.starts_with("builtin:/"))
            .collect();
        for resource in resources {
            write!(cursor, " {resource}")?;
        }
        writeln!(cursor)?;
        fileaccess::write_file_if_changed(&depfile, &cursor.into_inner())?;
    }
    diag.print_warnings_and_exit_on_error();
    Ok(())
}
