#!/usr/bin/env node
import * as path from 'node:path';
import * as url from 'node:url';
import * as fs from 'node:fs';
import * as child_process from 'node:child_process';

// Logging
const EXIT_FAILURE = 1;
function fail(message) {
    console.error(message);
    process.exit(EXIT_FAILURE);
}
function err(message) {
    fail('ERROR: ' + message);
}
function info(message) {
    console.log('INFO: ' + message);
}

// Enums
function Enum(values) {
    for (const value of values) {
        this[value] = {name: value.toLowerCase()};
    }
}
Enum.prototype.get = function (name) {
    for (const value in this) {
        if (value.toLowerCase() === name.toLowerCase()) {
            return this[value];
        }
    }
    return null;
};
function wrap(obj) {
    return new Proxy(obj, {
        get(target, property) {
            if (property in target) {
                return target[property];
            } else {
                err('Undefined Value: ' + property);
            }
        }
    });
}
const PackageTypes = wrap(new Enum([
    'None',
    'AppImage',
    'Flatpak'
]));
const Variants = wrap(new Enum([
    'Client',
    'Server'
]));
const Architectures = wrap(new Enum([
    'AMD64',
    'ARM64',
    'ARMHF',
    'Host'
]));

// Folders
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const root = path.join(__dirname, '..');
let build = path.join(root, 'build');
let out = path.join(root, 'out');

// Positional Arguments
let argIndex = 2; // Skip First Two Arguments
const POSITIONAL_ARGUMENT_COUNT = 3;
function readArg(from, type) {
    // Check Argument Count
    if (argIndex >= process.argv.length) {
        err('Expecting ' + type);
    }
    // Read Argument
    const arg = process.argv[argIndex++];
    const value = from.get(arg);
    if (value === null) {
        err(`Invalid ${type}: ${arg}`);
    }
    // Return
    return value;
}
// Type Of Packaging
const packageType = readArg(PackageTypes, 'Package Type');
// Build Variant
const variant = readArg(Variants, 'Variant');
// Build Architecture
const architecture = readArg(Architectures, 'Architecture');
// Flatpak Builds Work Best Without Custom Toolchains
if (packageType === PackageTypes.Flatpak && architecture !== Architectures.Host) {
    err('Flatpak Builds Do Not Support Custom Toolchains');
}

// CMake Build Options
const options = new Map();

// Other Arguments
let clean = false;
let install = false;
for (; argIndex < process.argv.length; argIndex++) {
    const arg = process.argv[argIndex];
    if (arg.startsWith('-D')) {
        // Pass Build Option To CMake
        let parsedArg = arg.substring(2);
        const split = parsedArg.indexOf('=');
        if (split === -1) {
            err('Unable To Parse Build Option: ' + arg);
        }
        const name = parsedArg.substring(0, split);
        const value = parsedArg.substring(split + 1);
        if (!/^[a-zA-Z_]+$/.test(name) || name.length === 0) {
            err('Invalid Build Option Name: ' + name);
        }
        options.set(name, value);
    } else if (arg === '--clean') {
        // Remove Existing Build Directory
        clean = true;
    } else if (arg === '--install') {
        // Install To System Instead Of Output Directory
        if (packageType === PackageTypes.AppImage) {
            err('AppImages Cannot Be Installed');
        }
        install = true;
    } else {
        err('Invalid Argument: ' + arg);
    }
}

// Update Folders
function updateDir(dir) {
    if (packageType !== PackageTypes.None) {
        dir = path.join(dir, packageType.name);
    }
    return path.join(dir, variant.name, architecture.name);
}
build = updateDir(build);
let cleanOut = false;
// AppImages Are Placed Directly In ./out
if (packageType !== PackageTypes.AppImage) {
    cleanOut = true;
    out = updateDir(out);
}

// Configure Build Options
function toCmakeBool(val) {
    return val ? 'ON' : 'OFF';
}
options.set('MCPI_SERVER_MODE', toCmakeBool(variant === Variants.Server));
options.set('MCPI_IS_APPIMAGE_BUILD', toCmakeBool(packageType === PackageTypes.AppImage));
options.set('MCPI_IS_FLATPAK_BUILD', toCmakeBool(packageType === PackageTypes.Flatpak));
if (architecture !== Architectures.Host) {
    options.set('CMAKE_TOOLCHAIN_FILE', path.join(root, 'cmake', 'toolchain', architecture.name + '-toolchain.cmake'));
} else {
    options.delete('CMAKE_TOOLCHAIN_FILE');
}

// Make Build Directory
function createDir(dir, clean) {
    if (clean) {
        fs.rmSync(dir, {recursive: true, force: true});
    }
    fs.mkdirSync(dir, {recursive: true});
}
createDir(build, clean);
if (!install) {
    createDir(out, cleanOut);
}

// Run CMake
function run(command) {
    try {
        info('Running: ' + command.join(' '));
        child_process.execFileSync(command[0], command.slice(1), {cwd: build, stdio: 'inherit'});
    } catch (e) {
        err(e);
    }
}
const cmake = ['cmake', '-GNinja'];
options.forEach((value, key, map) => {
    cmake.push(`-D${key}=${value}`);
});
cmake.push(root);
run(cmake);

// Build
run(['cmake', '--build', '.']);

// Package
if (packageType !== PackageTypes.AppImage) {
    if (!install) {
        process.env.DESTDIR = out;
    }
    run(['cmake', '--install', '.']);
} else {
    run(['cmake', '--build', '.', '--target', 'package']);
    // Copy Generated Files
    const files = fs.readdirSync(build);
    for (const file of files) {
        if (file.includes('.AppImage')) {
            info('Copying: ' + file);
            const src = path.join(build, file);
            const dst = path.join(out, file);
            fs.copyFileSync(src, dst);
        }
    }
}
