/*
Copyright 2020-2021 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import path from "path";
import childProcess from "child_process";
import mkdirp from "mkdirp";
import fsExtra from "fs-extra";

import HakEnv from "../../scripts/hak/hakEnv";
import { DependencyInfo } from "../../scripts/hak/dep";

export default async function (hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
    if (hakEnv.isWin()) {
        await buildOpenSslWin(hakEnv, moduleInfo);
        await buildSqlCipherWin(hakEnv, moduleInfo);
    } else if (hakEnv.wantsStaticSqlCipherUnix()) {
        await buildSqlCipherUnix(hakEnv, moduleInfo);
    }
    await buildMatrixSeshat(hakEnv, moduleInfo);
}

async function buildOpenSslWin(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
    const version = moduleInfo.cfg.dependencies.openssl;
    const openSslDir = path.join(moduleInfo.moduleTargetDotHakDir, `openssl-${version}`);

    const openSslArch = hakEnv.getTargetArch() === "x64" ? "VC-WIN64A" : "VC-WIN32";

    console.log("Building openssl in " + openSslDir);
    await new Promise<void>((resolve, reject) => {
        const proc = childProcess.spawn(
            "perl",
            [
                "Configure",
                "--prefix=" + moduleInfo.depPrefix,
                // sqlcipher only uses about a tiny part of openssl. We link statically
                // so will only pull in the symbols we use, but we may as well turn off
                // as much as possible to save on build time.
                "no-afalgeng",
                "no-capieng",
                "no-cms",
                "no-ct",
                "no-deprecated",
                "no-dgram",
                "no-dso",
                "no-ec",
                "no-ec2m",
                "no-gost",
                "no-nextprotoneg",
                "no-ocsp",
                "no-sock",
                "no-srp",
                "no-srtp",
                "no-tests",
                "no-ssl",
                "no-tls",
                "no-dtls",
                "no-shared",
                "no-aria",
                "no-camellia",
                "no-cast",
                "no-chacha",
                "no-cmac",
                "no-des",
                "no-dh",
                "no-dsa",
                "no-ecdh",
                "no-ecdsa",
                "no-idea",
                "no-md4",
                "no-mdc2",
                "no-ocb",
                "no-poly1305",
                "no-rc2",
                "no-rc4",
                "no-rmd160",
                "no-scrypt",
                "no-seed",
                "no-siphash",
                "no-sm2",
                "no-sm3",
                "no-sm4",
                "no-whirlpool",
                openSslArch,
            ],
            {
                cwd: openSslDir,
                stdio: "inherit",
            },
        );
        proc.on("exit", (code) => {
            code ? reject(code) : resolve();
        });
    });

    await new Promise<void>((resolve, reject) => {
        const proc = childProcess.spawn("nmake", ["build_libs"], {
            cwd: openSslDir,
            stdio: "inherit",
        });
        proc.on("exit", (code) => {
            code ? reject(code) : resolve();
        });
    });

    await new Promise<void>((resolve, reject) => {
        const proc = childProcess.spawn("nmake", ["install_dev"], {
            cwd: openSslDir,
            stdio: "inherit",
        });
        proc.on("exit", (code) => {
            code ? reject(code) : resolve();
        });
    });
}

async function buildSqlCipherWin(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
    const version = moduleInfo.cfg.dependencies.sqlcipher;
    const sqlCipherDir = path.join(moduleInfo.moduleTargetDotHakDir, `sqlcipher-${version}`);
    const buildDir = path.join(sqlCipherDir, "bld");

    await mkdirp(buildDir);

    await new Promise<void>((resolve, reject) => {
        const proc = childProcess.spawn("nmake", ["/f", path.join("..", "Makefile.msc"), "libsqlite3.lib", "TOP=.."], {
            cwd: buildDir,
            stdio: "inherit",
            env: Object.assign({}, process.env, {
                CCOPTS: "-DSQLITE_HAS_CODEC -I" + path.join(moduleInfo.depPrefix, "include"),
                LTLIBPATHS: "/LIBPATH:" + path.join(moduleInfo.depPrefix, "lib"),
                LTLIBS: "libcrypto.lib",
            }),
        });
        proc.on("exit", (code) => {
            code ? reject(code) : resolve();
        });
    });

    await fsExtra.copy(path.join(buildDir, "libsqlite3.lib"), path.join(moduleInfo.depPrefix, "lib", "sqlcipher.lib"));

    await fsExtra.copy(path.join(buildDir, "sqlite3.h"), path.join(moduleInfo.depPrefix, "include", "sqlcipher.h"));
}

async function buildSqlCipherUnix(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
    const version = moduleInfo.cfg.dependencies.sqlcipher;
    const sqlCipherDir = path.join(moduleInfo.moduleTargetDotHakDir, `sqlcipher-${version}`);

    const args = [
        "--prefix=" + moduleInfo.depPrefix + "",
        "--enable-tempstore=yes",
        "--enable-shared=no",
        "--enable-tcl=no",
    ];

    if (hakEnv.isMac()) {
        args.push("--with-crypto-lib=commoncrypto");
    }

    if (hakEnv.wantsStaticSqlCipherUnix()) {
        args.push("--enable-tcl=no");

        if (hakEnv.isLinux()) {
            args.push("--with-pic=yes");
        }
    }

    if (!hakEnv.isHost()) {
        // In the nonsense world of `configure`, it is assumed you are building
        // a compiler like `gcc`, so the `host` option actually means the target
        // the build output runs on.
        args.push(`--host=${hakEnv.getTargetId()}`);
    }

    const cflags = ["-DSQLITE_HAS_CODEC"];

    if (!hakEnv.isHost()) {
        // `clang` uses more logical option naming.
        cflags.push(`--target=${hakEnv.getTargetId()}`);
    }

    if (cflags.length) {
        args.push(`CFLAGS=${cflags.join(" ")}`);
    }

    const ldflags: string[] = [];

    if (hakEnv.isMac()) {
        ldflags.push("-framework Security");
        ldflags.push("-framework Foundation");
    }

    if (ldflags.length) {
        args.push(`LDFLAGS=${ldflags.join(" ")}`);
    }

    await new Promise<void>((resolve, reject) => {
        const proc = childProcess.spawn(path.join(sqlCipherDir, "configure"), args, {
            cwd: sqlCipherDir,
            stdio: "inherit",
        });
        proc.on("exit", (code) => {
            code ? reject(code) : resolve();
        });
    });

    await new Promise<void>((resolve, reject) => {
        const proc = childProcess.spawn("make", [], {
            cwd: sqlCipherDir,
            stdio: "inherit",
        });
        proc.on("exit", (code) => {
            code ? reject(code) : resolve();
        });
    });

    await new Promise<void>((resolve, reject) => {
        const proc = childProcess.spawn("make", ["install"], {
            cwd: sqlCipherDir,
            stdio: "inherit",
        });
        proc.on("exit", (code) => {
            code ? reject(code) : resolve();
        });
    });
}

async function buildMatrixSeshat(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
    // seshat now uses n-api so we shouldn't need to specify a node version to
    // build against, but it does seems to still need something in here, so leaving
    // it for now: we should confirm how much of this it still actually needs.
    const env = hakEnv.makeGypEnv();

    if (!hakEnv.isLinux() || hakEnv.wantsStaticSqlCipherUnix()) {
        Object.assign(env, {
            SQLCIPHER_STATIC: 1,
            SQLCIPHER_LIB_DIR: path.join(moduleInfo.depPrefix, "lib"),
            SQLCIPHER_INCLUDE_DIR: path.join(moduleInfo.depPrefix, "include"),
        });
    }

    if (hakEnv.isLinux() && hakEnv.wantsStaticSqlCipherUnix()) {
        // Ensure Element uses the statically-linked seshat build, and prevent other applications
        // from attempting to use this one. Detailed explanation:
        //
        // RUSTFLAGS
        //     An environment variable containing a list of arguments to pass to rustc.
        // -Clink-arg=VALUE
        //     A rustc argument to pass a single argument to the linker.
        // -Wl,
        //     gcc syntax to pass an argument (from gcc) to the linker (ld).
        // -Bsymbolic:
        //     Prefer local/statically linked symbols over those in the environment.
        //     Prevent overriding native libraries by LD_PRELOAD etc.
        // --exclude-libs ALL
        //     Prevent symbols from being exported by any archive libraries.
        //     Reduces output filesize and prevents being dynamically linked against.
        env.RUSTFLAGS = "-Clink-arg=-Wl,-Bsymbolic -Clink-arg=-Wl,--exclude-libs,ALL";
    }

    if (hakEnv.isWin()) {
        env.RUSTFLAGS = "-Ctarget-feature=+crt-static -Clink-args=libcrypto.lib";
        // Note that in general, you can specify targets in Rust without having to have
        // the matching toolchain, however for this, cargo gets confused when building
        // the build scripts since they run on the host, but vcvarsall.bat sets the c
        // compiler in the path to be the one for the target, so we just use the matching
        // toolchain for the target architecture which makes everything happy.
        env.RUSTUP_TOOLCHAIN = `stable-${hakEnv.getTargetId()}`;
    }

    if (!hakEnv.isHost()) {
        env.CARGO_BUILD_TARGET = hakEnv.getTargetId();
    }

    console.log("Running neon with env", env);
    await new Promise<void>((resolve, reject) => {
        const proc = childProcess.spawn(
            path.join(moduleInfo.nodeModuleBinDir, "neon" + (hakEnv.isWin() ? ".cmd" : "")),
            ["build", "--release"],
            {
                cwd: moduleInfo.moduleBuildDir,
                env,
                stdio: "inherit",
            },
        );
        proc.on("exit", (code) => {
            code ? reject(code) : resolve();
        });
    });
}
