// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html

use anyhow::Result;
use ninja_gen::action::BuildAction;
use ninja_gen::command::RunCommand;
use ninja_gen::copy::CopyFile;
use ninja_gen::copy::CopyFiles;
use ninja_gen::glob;
use ninja_gen::hashmap;
use ninja_gen::inputs;
use ninja_gen::node::CompileSass;
use ninja_gen::node::EsbuildScript;
use ninja_gen::node::TypescriptCheck;
use ninja_gen::python::python_format;
use ninja_gen::python::PythonTest;
use ninja_gen::rsync::RsyncFiles;
use ninja_gen::Build;
use ninja_gen::Utf8Path;
use ninja_gen::Utf8PathBuf;

use crate::anki_version;
use crate::python::BuildWheel;
use crate::web::copy_mathjax;

pub fn build_and_check_aqt(build: &mut Build) -> Result<()> {
    build_forms(build)?;
    build_generated_sources(build)?;
    build_data_folder(build)?;
    build_macos_helper(build)?;
    build_wheel(build)?;
    check_python(build)?;
    Ok(())
}

fn build_forms(build: &mut Build) -> Result<()> {
    let ui_files = glob!["qt/aqt/forms/*.ui"];
    let outdir = Utf8PathBuf::from("qt/_aqt/forms");
    let mut py_files = vec![];
    for path in ui_files.resolve() {
        let outpath = outdir.join(path.file_name().unwrap()).into_string();
        py_files.push(outpath.replace(".ui", "_qt5.py"));
        py_files.push(outpath.replace(".ui", "_qt6.py"));
    }
    build.add_action(
        "qt:aqt:forms",
        RunCommand {
            command: ":pyenv:bin",
            args: "$script $first_form",
            inputs: hashmap! {
                "script" => inputs!["qt/tools/build_ui.py"],
                "" => inputs![ui_files],
            },
            outputs: hashmap! {
                "first_form" => vec![py_files[0].as_str()],
                "" => py_files.iter().skip(1).map(|s| s.as_str()).collect(),
            },
        },
    )
}

/// For legacy reasons, we can not easily separate sources and generated files
/// up with a PEP420 namespace, as aqt/__init__.py exports a bunch of things.
/// To allow code to run/typecheck without having to merge source and generated
/// files into a separate folder, the generated files are exported as a separate
/// _aqt module.
fn build_generated_sources(build: &mut Build) -> Result<()> {
    build.add_action(
        "qt:aqt:hooks.py",
        RunCommand {
            command: ":pyenv:bin",
            args: "$script $out",
            inputs: hashmap! {
                "script" => inputs!["qt/tools/genhooks_gui.py"],
                "" => inputs!["pylib/anki/_vendor/stringcase.py", "pylib/tools/hookslib.py"]
            },
            outputs: hashmap! {
                "out" => vec!["qt/_aqt/hooks.py"]
            },
        },
    )?;
    build.add_action(
        "qt:aqt:sass_vars",
        RunCommand {
            command: ":pyenv:bin",
            args: "$script $root_scss $out",
            inputs: hashmap! {
                "script" => inputs!["qt/tools/extract_sass_vars.py"],
                "root_scss" => inputs![":css:_root-vars"],
            },
            outputs: hashmap! {
                "out" => vec![
                    "qt/_aqt/colors.py",
                    "qt/_aqt/props.py"
                ]
            },
        },
    )?;
    // we need to add a py.typed file to the generated sources, or mypy
    // will ignore them when used with the generated wheel
    build.add_action(
        "qt:aqt:py.typed",
        CopyFile {
            input: "qt/aqt/py.typed".into(),
            output: "qt/_aqt/py.typed",
        },
    )?;
    Ok(())
}

fn build_data_folder(build: &mut Build) -> Result<()> {
    build_css(build)?;
    build_imgs(build)?;
    build_js(build)?;
    build_pages(build)?;
    build_icons(build)?;
    copy_sveltekit(build)?;
    Ok(())
}

fn copy_sveltekit(build: &mut Build) -> Result<()> {
    build.add_action(
        "qt:aqt:data:web:sveltekit",
        RsyncFiles {
            inputs: inputs![":sveltekit:folder"],
            target_folder: "qt/_aqt/data/web/",
            strip_prefix: "$builddir/",
            extra_args: "-a --delete",
        },
    )
}

fn build_css(build: &mut Build) -> Result<()> {
    let scss_files = build.expand_inputs(inputs![glob!["qt/aqt/data/web/css/*.scss"]]);
    let out_dir = Utf8Path::new("qt/_aqt/data/web/css");
    for scss in scss_files {
        let stem = Utf8Path::new(&scss).file_stem().unwrap();
        let mut out_path = out_dir.join(stem);
        out_path.set_extension("css");

        build.add_action(
            "qt:aqt:data:web:css",
            CompileSass {
                input: scss.into(),
                output: out_path.as_str(),
                deps: inputs![":sass"],
                load_paths: vec![".", "node_modules"],
            },
        )?;
    }
    let other_ts_css = build.inputs_with_suffix(
        inputs![":ts:editor", ":ts:editable", ":ts:reviewer:reviewer.css"],
        ".css",
    );
    build.add_action(
        "qt:aqt:data:web:css",
        CopyFiles {
            inputs: other_ts_css.into(),
            output_folder: "qt/_aqt/data/web/css",
        },
    )
}

fn build_imgs(build: &mut Build) -> Result<()> {
    build.add_action(
        "qt:aqt:data:web:imgs",
        CopyFiles {
            inputs: inputs![glob!["qt/aqt/data/web/imgs/*"]],
            output_folder: "qt/_aqt/data/web/imgs",
        },
    )
}

fn build_js(build: &mut Build) -> Result<()> {
    for ts_file in &["deckbrowser", "webview", "toolbar", "reviewer-bottom"] {
        build.add_action(
            "qt:aqt:data:web:js",
            EsbuildScript {
                script: "ts/transform_ts.mjs".into(),
                entrypoint: format!("qt/aqt/data/web/js/{ts_file}.ts").into(),
                deps: inputs![],
                output_stem: &format!("qt/_aqt/data/web/js/{ts_file}"),
                extra_exts: &[],
            },
        )?;
    }
    let files = inputs![glob!["qt/aqt/data/web/js/*"]];
    build.add_action(
        "check:typescript:aqt",
        TypescriptCheck {
            tsconfig: "qt/aqt/data/web/js/tsconfig.json".into(),
            inputs: files,
        },
    )?;
    let files_from_ts = build.inputs_with_suffix(
        inputs![":ts:editor", ":ts:reviewer:reviewer.js", ":ts:mathjax"],
        ".js",
    );
    build.add_action(
        "qt:aqt:data:web:js",
        CopyFiles {
            inputs: files_from_ts.into(),
            output_folder: "qt/_aqt/data/web/js",
        },
    )?;
    build_vendor_js(build)
}

fn build_vendor_js(build: &mut Build) -> Result<()> {
    build.add_action("qt:aqt:data:web:js:vendor:mathjax", copy_mathjax())?;
    build.add_action(
        "qt:aqt:data:web:js:vendor",
        CopyFiles {
            inputs: inputs![
                ":node_modules:jquery",
                ":node_modules:jquery-ui",
                ":node_modules:bootstrap-dist",
                "qt/aqt/data/web/js/vendor/plot.js"
            ],
            output_folder: "qt/_aqt/data/web/js/vendor",
        },
    )
}

fn build_pages(build: &mut Build) -> Result<()> {
    build.add_action(
        "qt:aqt:data:web:pages",
        CopyFiles {
            inputs: inputs![":ts:pages"],
            output_folder: "qt/_aqt/data/web/pages",
        },
    )?;
    Ok(())
}

fn build_icons(build: &mut Build) -> Result<()> {
    build_themed_icons(build)?;
    build.add_action(
        "qt:aqt:data:qt:icons:mdi_unthemed",
        CopyFiles {
            inputs: inputs![":node_modules:mdi_unthemed"],
            output_folder: "qt/_aqt/data/qt/icons",
        },
    )?;
    build.add_action(
        "qt:aqt:data:qt:icons:from_src",
        CopyFiles {
            inputs: inputs![glob!["qt/aqt/data/qt/icons/*.{png,svg}"]],
            output_folder: "qt/_aqt/data/qt/icons",
        },
    )?;
    build.add_action(
        "qt:aqt:data:qt:icons",
        RunCommand {
            command: ":pyenv:bin",
            args: "$script $out $in",
            inputs: hashmap! {
                "script" => inputs!["qt/tools/build_qrc.py"],
                "in" => inputs![
                    ":qt:aqt:data:qt:icons:mdi_unthemed",
                    ":qt:aqt:data:qt:icons:mdi_themed",
                    ":qt:aqt:data:qt:icons:from_src",
                ]
            },
            outputs: hashmap! {
                "out" => vec!["qt/_aqt/data/qt/icons.qrc"]
            },
        },
    )?;
    Ok(())
}

fn build_themed_icons(build: &mut Build) -> Result<()> {
    let themed_icons_with_extra = hashmap! {
        "chevron-up" => &["FG_DISABLED"],
        "chevron-down" => &["FG_DISABLED"],
        "drag-vertical" => &["FG_SUBTLE"],
        "drag-horizontal" => &["FG_SUBTLE"],
        "check" => &["FG_DISABLED"],
        "circle-medium" => &["FG_DISABLED"],
        "minus-thick" => &["FG_DISABLED"],
    };
    for icon_path in build.expand_inputs(inputs![":node_modules:mdi_themed"]) {
        let path = Utf8Path::new(&icon_path);
        let stem = path.file_stem().unwrap();
        let mut colors = vec!["FG"];
        if let Some(&extra) = themed_icons_with_extra.get(stem) {
            colors.extend(extra);
        }
        build.add_action(
            "qt:aqt:data:qt:icons:mdi_themed",
            BuildThemedIcon {
                src_icon: path,
                colors,
            },
        )?;
    }
    Ok(())
}

struct BuildThemedIcon<'a> {
    src_icon: &'a Utf8Path,
    colors: Vec<&'a str>,
}

impl BuildAction for BuildThemedIcon<'_> {
    fn command(&self) -> &str {
        "$pyenv_bin $script $in $colors $out"
    }

    fn files(&mut self, build: &mut impl ninja_gen::build::FilesHandle) {
        let stem = self.src_icon.file_stem().unwrap();
        // eg foo-light.svg, foo-dark.svg, foo-FG_SUBTLE-light.svg,
        // foo-FG_SUBTLE-dark.svg
        let outputs: Vec<_> = self
            .colors
            .iter()
            .flat_map(|&color| {
                let variant = if color == "FG" {
                    "".into()
                } else {
                    format!("-{color}")
                };
                [
                    format!("qt/_aqt/data/qt/icons/{stem}{variant}-light.svg"),
                    format!("qt/_aqt/data/qt/icons/{stem}{variant}-dark.svg"),
                ]
            })
            .collect();

        build.add_inputs("pyenv_bin", inputs![":pyenv:bin"]);
        build.add_inputs("script", inputs!["qt/tools/color_svg.py"]);
        build.add_inputs("in", inputs![self.src_icon.as_str()]);
        build.add_inputs("", inputs![":qt:aqt:sass_vars"]);
        build.add_variable("colors", self.colors.join(":"));
        build.add_outputs("out", outputs);
    }
}

fn build_macos_helper(build: &mut Build) -> Result<()> {
    if cfg!(target_os = "macos") {
        build.add_action(
            "qt:aqt:data:lib:libankihelper",
            RunCommand {
                command: ":pyenv:bin",
                args: "$script $out $in",
                inputs: hashmap! {
                    "script" => inputs!["qt/mac/helper_build.py"],
                    "in" => inputs![glob!["qt/mac/*.swift"]],
                    "" => inputs!["out/env"],
                },
                outputs: hashmap! {
                    "out" => vec!["qt/_aqt/data/lib/libankihelper.dylib"],
                },
            },
        )?;
    }
    Ok(())
}

fn build_wheel(build: &mut Build) -> Result<()> {
    build.add_action(
        "wheels:aqt",
        BuildWheel {
            name: "aqt",
            version: anki_version(),
            src_folder: "qt/aqt",
            gen_folder: "$builddir/qt/_aqt",
            platform: None,
            deps: inputs![":qt:aqt", glob!("qt/aqt/**"), "python/requirements.aqt.in"],
        },
    )
}

fn check_python(build: &mut Build) -> Result<()> {
    python_format(
        build,
        "qt",
        inputs![glob!("qt/**/*.py", "qt/bundle/PyOxidizer/**")],
    )?;

    build.add_action(
        "check:pytest:aqt",
        PythonTest {
            folder: "qt/tests",
            python_path: &["pylib", "$builddir/pylib", "$builddir/qt"],
            deps: inputs![":pylib:anki", ":qt:aqt", glob!["qt/tests/**"]],
        },
    )
}
