const { dirname, isAbsolute, join, resolve } = require("path");
const { existsSync, readFileSync, writeFileSync } = require("fs");

const shelljs = require("shelljs");
const semver = require("semver");

const SnapshotGenerator = require("./snapshot-generator");
const {
    CONSTANTS,
    createDirectory,
    getJsonFile,
} = require("./utils");
const { getPackageJson } = require("../../projectHelpers");
const {
    ANDROID_PROJECT_DIR,
    ANDROID_APP_PATH,
    ANDROID_CONFIGURATIONS_PATH,
    getAndroidRuntimeVersion,
    getAndroidV8Version,
    getMksnapshotParams
} = require("../../androidProjectHelpers");

const MIN_ANDROID_RUNTIME_VERSION = "3.0.0";
const VALID_ANDROID_RUNTIME_TAGS = Object.freeze(["next", "rc"]);
const V8_VERSIONS_FILE_NAME = "v8-versions.json";
const V8_VERSIONS_URL = `https://raw.githubusercontent.com/NativeScript/android-runtime/master/${V8_VERSIONS_FILE_NAME}`;
const V8_VERSIONS_LOCAL_PATH = resolve(CONSTANTS.SNAPSHOT_TMP_DIR, V8_VERSIONS_FILE_NAME);

const resolveRelativePath = (path) => {
    if (path)
        return isAbsolute(path) ? path : resolve(process.cwd(), path);
    return null;
};

function ProjectSnapshotGenerator(options) {
    this.options = options = options || {};
    options.projectRoot = resolveRelativePath(options.projectRoot) || process.cwd();

    console.log("Project root: " + options.projectRoot);
    console.log("Snapshots build directory: " + this.getBuildPath());

    this.validateAndroidRuntimeVersion();
}
module.exports = ProjectSnapshotGenerator;

ProjectSnapshotGenerator.calculateBuildPath = function (projectRoot) {
    return join(
        ProjectSnapshotGenerator.calculateProjectPath(projectRoot),
        "snapshot-build",
        "build"
    );
}

ProjectSnapshotGenerator.prototype.getBuildPath = function () {
    return ProjectSnapshotGenerator.calculateBuildPath(this.options.projectRoot);
}

ProjectSnapshotGenerator.calculateProjectPath = function (projectRoot) {
    return join(projectRoot, ANDROID_PROJECT_DIR);
}

ProjectSnapshotGenerator.calculateConfigurationsPath = function (projectRoot) {
    return join(projectRoot, ANDROID_CONFIGURATIONS_PATH);
}

ProjectSnapshotGenerator.calculateAppPath = function (projectRoot) {
    return join(projectRoot, ANDROID_APP_PATH);
}

ProjectSnapshotGenerator.prototype.getProjectPath = function () {
    return ProjectSnapshotGenerator.calculateProjectPath(this.options.projectRoot);
}

ProjectSnapshotGenerator.cleanSnapshotArtefacts = function (projectRoot) {
    const platformPath = ProjectSnapshotGenerator.calculateProjectPath(projectRoot);

    // Remove blob files from prepared folder
    shelljs.rm("-rf", join(platformPath, "src/main/assets/snapshots"));

    // Remove prepared include.gradle configurations
    const configurationsPath = ProjectSnapshotGenerator.calculateConfigurationsPath(projectRoot);
    shelljs.rm("-rf", join(configurationsPath, SnapshotGenerator.SNAPSHOT_PACKAGE_NANE));
}

ProjectSnapshotGenerator.installSnapshotArtefacts = function (projectRoot) {
    const buildPath = ProjectSnapshotGenerator.calculateBuildPath(projectRoot);
    const platformPath = ProjectSnapshotGenerator.calculateProjectPath(projectRoot);

    const appPath = ProjectSnapshotGenerator.calculateAppPath(projectRoot);
    const configurationsPath = ProjectSnapshotGenerator.calculateConfigurationsPath(projectRoot);
    const configDestinationPath = join(configurationsPath, SnapshotGenerator.SNAPSHOT_PACKAGE_NANE);

    // Remove build folder to make sure that the apk will be fully rebuild
    shelljs.rm("-rf", join(platformPath, "build"));

    // Copy include.gradle to the specified destination in the platforms folder
    shelljs.mkdir("-p", configDestinationPath);
    shelljs.cp(join(buildPath, "include.gradle"), join(configDestinationPath, "include.gradle"));

    if (shelljs.test("-e", join(buildPath, "ndk-build/libs"))) {
        // useLibs = true
        const libsDestinationPath = join(platformPath, "src", SnapshotGenerator.SNAPSHOT_PACKAGE_NANE, "jniLibs");

        // Copy the libs to the specified destination in the platforms folder
        shelljs.mkdir("-p", libsDestinationPath);
        shelljs.cp("-R", join(buildPath, "ndk-build/libs") + "/", libsDestinationPath);
    } else {
        // useLibs = false
        const blobsSrcPath = join(buildPath, "snapshots/blobs");
        const blobsDestinationPath = resolve(appPath, "../snapshots");
        const appPackageJsonPath = join(appPath, "package.json");

        // Copy the blobs in the prepared app folder
        shelljs.cp("-R", blobsSrcPath + "/", resolve(appPath, "../snapshots"));

        /*
        Rename TNSSnapshot.blob files to snapshot.blob files. The xxd tool uses the file name for the name of the static array. This is why the *.blob files are initially named  TNSSnapshot.blob. After the xxd step, they must be renamed to snapshot.blob, because this is the filename that the Android runtime is looking for.
        */
        shelljs.exec("find " + blobsDestinationPath + " -name '*.blob' -execdir mv {} snapshot.blob ';'");

        // Update the package.json file
        const appPackageJson = shelljs.test("-e", appPackageJsonPath) ? JSON.parse(readFileSync(appPackageJsonPath, 'utf8')) : {};
        appPackageJson["android"] = appPackageJson["android"] || {};
        appPackageJson["android"]["heapSnapshotBlob"] = "../snapshots";
        writeFileSync(appPackageJsonPath, JSON.stringify(appPackageJson, null, 2));
    }
}

const versionIsPrerelease = version => version.indexOf("-") > -1;
const v8VersionsFileExists = () => existsSync(V8_VERSIONS_LOCAL_PATH);
const saveV8VersionsFile = versionsMap =>
    writeFileSync(V8_VERSIONS_LOCAL_PATH, JSON.stringify(versionsMap));
const readV8VersionsFile = () => JSON.parse(readFileSync(V8_VERSIONS_LOCAL_PATH));
const fetchV8VersionsFile = () =>
    new Promise((resolve, reject) => {
        getJsonFile(V8_VERSIONS_URL)
            .then(versionsMap => {
                createDirectory(dirname(V8_VERSIONS_LOCAL_PATH));
                saveV8VersionsFile(versionsMap);
                return resolve(versionsMap);
            })
            .catch(reject);
    });

const findV8Version = (runtimeVersion, v8VersionsMap) => {
    const runtimeRange = Object.keys(v8VersionsMap)
        .find(range => semver.satisfies(runtimeVersion, range));

    return v8VersionsMap[runtimeRange];
}

const getV8VersionsMap = runtimeVersion =>
    new Promise((resolve, reject) => {
        if (!v8VersionsFileExists() || versionIsPrerelease(runtimeVersion)) {
            fetchV8VersionsFile()
                .then(versionsMap => resolve({ versionsMap, latest: true }))
                .catch(reject);
        } else {
            const versionsMap = readV8VersionsFile();
            return resolve({ versionsMap, latest: false });
        }
    });

ProjectSnapshotGenerator.prototype.getV8Version = function (generationOptions) {
    return new Promise((resolve, reject) => {
        const maybeV8Version = generationOptions.v8Version;
        if (maybeV8Version) {
            return resolve(maybeV8Version);
        }

        // try to get the V8 Version from the settings.json file in android runtime folder
        const runtimeV8Version = getAndroidV8Version(this.options.projectRoot);
        if(runtimeV8Version) {
            return resolve(runtimeV8Version);
        }

        const runtimeVersion = getAndroidRuntimeVersion(this.options.projectRoot);
        getV8VersionsMap(runtimeVersion)
            .then(({ versionsMap, latest }) => {
                const v8Version = findV8Version(runtimeVersion, versionsMap);

                if (!v8Version && !latest) {
                    fetchV8VersionsFile().then(latestVersionsMap => {
                        const version = findV8Version(runtimeVersion, latestVersionsMap)
                        return resolve(version);
                    })
                    .catch(reject);
                } else {
                    return resolve(v8Version);
                }
            })
            .catch(reject);
    });
}

ProjectSnapshotGenerator.prototype.validateAndroidRuntimeVersion = function () {
    const currentRuntimeVersion = getAndroidRuntimeVersion(this.options.projectRoot);

    if (!currentRuntimeVersion || !this.getProjectPath()) {
        throw new Error("In order to generate a V8 snapshot you must have the \"android\" platform installed - to do so please run \"tns platform add android\".");
    }

    if (!VALID_ANDROID_RUNTIME_TAGS.includes(currentRuntimeVersion) &&
        !semver.gte(currentRuntimeVersion, MIN_ANDROID_RUNTIME_VERSION)) {

        throw new Error("In order to support heap snapshots, you must have at least tns-android@" + MIN_ANDROID_RUNTIME_VERSION +
            " installed. Current Android Runtime version is: " + currentRuntimeVersion + ".");
    }
}

ProjectSnapshotGenerator.prototype.generate = function (generationOptions) {
    generationOptions = generationOptions || {};

    console.log("Running snapshot generation with the following arguments: ");
    console.log(JSON.stringify(generationOptions, null, '\t'));

    // Clean build folder
    shelljs.rm("-rf", this.getBuildPath());
    shelljs.mkdir("-p", this.getBuildPath());

    const snapshotToolsPath = resolveRelativePath(generationOptions.snapshotToolsPath) || CONSTANTS.SNAPSHOT_TMP_DIR;
    const androidNdkPath = generationOptions.androidNdkPath || process.env.ANDROID_NDK_HOME;

    console.log("Snapshot tools path: " + snapshotToolsPath);

    // Generate snapshots
    const generator = new SnapshotGenerator({ buildPath: this.getBuildPath() });

    const noV8VersionFoundMessage = `Cannot find suitable v8 version!`;
    let shouldRethrow = false;

    const mksnapshotParams = getMksnapshotParams(this.options.projectRoot);

    return this.getV8Version(generationOptions).then(v8Version => {
        shouldRethrow = true;
        if (!v8Version) {
            throw new Error(noV8VersionFoundMessage);
        }

        const options = {
            snapshotToolsPath,
            targetArchs: generationOptions.targetArchs || ["arm", "arm64", "ia32"],
            v8Version: generationOptions.v8Version || v8Version,
            preprocessedInputFile: generationOptions.preprocessedInputFile,
            useLibs: generationOptions.useLibs || false,
            inputFiles: generationOptions.inputFiles || [join(this.options.projectRoot, "__snapshot.js")],
            androidNdkPath,
            mksnapshotParams: mksnapshotParams
        };

        return generator.generate(options).then(() => {
            console.log("Snapshots build finished succesfully!");

            if (generationOptions.install) {
                ProjectSnapshotGenerator.cleanSnapshotArtefacts(this.options.projectRoot);
                ProjectSnapshotGenerator.installSnapshotArtefacts(this.options.projectRoot);
                console.log(generationOptions.useLibs ?
                    "Snapshot is included in the app as dynamically linked library (.so file)." :
                    "Snapshot is included in the app as binary .blob file. The more space-efficient option is to embed it in a dynamically linked library (.so file).");
            }
        });
    }).catch(error => {
        throw shouldRethrow ?
            error :
            new Error(`${noV8VersionFoundMessage} Original error: ${error.message || error}`);
    });
}
