/*
 * self.js
 * Copyright (C) 2015 Kovid Goyal <kovid at kovidgoyal.net>
 *
 * Distributed under terms of the BSD license.
 */

import path from 'path';
import crypto from 'crypto';
import fs from 'fs';
import vm from 'vm';
import zlib from 'zlib';
import { createRequire } from 'module';
import { fileURLToPath } from 'url';
import { create_compiler } from './compiler.mjs';

const require = createRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);

async function compile_baselib(RapydScript, src_path) {
    var items = (await fs.promises.readdir(src_path)).filter(function(name) {
        return name.slice(0, 'baselib-'.length) === 'baselib-' && name.slice(-4) == '.pyj';
    });
    var ans = {'pretty': '', 'ugly': ''};

    items.sort();
    for (const fname of items) {
        var name = fname.slice('baselib-'.length, -4), ast;
        var raw = await fs.promises.readFile(path.join(src_path, fname), 'utf-8');
        try {
            ast = await RapydScript.parse(raw, {filename:fname, basedir:src_path});
        } catch (e) {
            if (!(e instanceof RapydScript.SyntaxError)) throw e;
            console.error(e.toString());
            process.exit(1);
        }
        [true, false].forEach(function (beautify) {
            var output = new RapydScript.OutputStream({
                beautify: beautify, write_name: false, private_scope:false, omit_baselib: true,
            });
            ast.print(output);
            ans[(beautify) ? 'pretty' : 'ugly'] += output.get();
        });
    }
    return ans;
}

async function check_for_changes(base_path, src_path, signatures) {
    // Check if any of the files involved in the build process have changed,
    // as compared to the saved sha1 hashes from the last build
    var saved_hashes = {}, hashes = {}, sources = {};
    var compiler_changed = false, compiler_hash, source_hash;
    try {
        saved_hashes = JSON.parse(await fs.promises.readFile(signatures, 'utf-8'));
    } catch (e) {
        if (e.code != 'ENOENT') throw (e);
    }

    var src_file_names = [];

    async function process_dir(p) {
        var entries = await fs.promises.readdir(p);
        for (const name of entries) {
            var fp = path.join(p, name);
            if (name.substr(-4) === '.pyj') src_file_names.push(path.relative(src_path, fp));
            else if (name != 'lib') {
                var stat = await fs.promises.stat(fp);
                if (stat.isDirectory()) await process_dir(fp);
            }
        }
    }
    await process_dir(src_path);

    compiler_hash = crypto.createHash('sha1');
    source_hash = crypto.createHash('sha1');
    for (const fname of src_file_names) {
        var src = path.join(src_path, fname);
        sources[src] = await fs.promises.readFile(src, 'utf-8');
        compiler_hash.update(sources[src]);
        source_hash.update(sources[src]);
        var h = crypto.createHash('sha1');
        h.update(sources[src]);
        hashes[fname.split('.')[0]] = h.digest('hex');
    }
    var compiler_files = [__filename, path.join(base_path, 'tools', 'compiler.mjs')];
    for (const fpath of compiler_files) {
        compiler_hash.update(await fs.promises.readFile(fpath, 'utf-8'));
    }
    hashes['#compiler#'] = compiler_hash.digest('hex');
    hashes['#compiled_with#'] = saved_hashes['#compiler#'] || 'unknown';
    source_hash = source_hash.digest('hex');
    if (hashes['#compiler#'] != saved_hashes['#compiler#']) {
        console.log('There are changes to the source files of the compiler, rebuiliding');
        compiler_changed = true;
    } else if (hashes['#compiled_with#'] != saved_hashes['#compiled_with#']) {
        console.log('Re-building compiler with updated version of itself');
        compiler_changed = true;
    }

    return [source_hash, compiler_changed, sources, hashes];
}


async function compile(src_path, lib_path, sources, source_hash, profile) {
    var file = path.join(src_path, 'compiler.pyj');
    var t1 = new Date().getTime();
    var RapydScript = await create_compiler();
    var output_options, profiler, cpu_profile;
    var compiled_baselib = await compile_baselib(RapydScript, src_path);
    var out_path = path.join(path.dirname(lib_path), 'dev');
    try {
        await fs.promises.mkdir(out_path);
    } catch (e) {
        if (e.code != 'EEXIST') throw e;
    }
    output_options = {'beautify': true, 'baselib_plain': compiled_baselib.pretty};

    var raw = sources[file], toplevel;

	async function parse_file(code, file) {
		return await RapydScript.parse(code, {
			filename: file,
			basedir: path.dirname(file),
			libdir: path.join(src_path, 'lib'),
		});
	}

    try {
        if (profile) {
            profiler = require('v8-profiler');
            profiler.startProfiling();
        }
        toplevel = await parse_file(raw, file);
        if (profile) {
            cpu_profile = profiler.stopProfiling();
            await fs.promises.writeFile('self.cpuprofile', JSON.stringify(cpu_profile), 'utf-8');
        }
    } catch (e) {
        if (!(e instanceof RapydScript.SyntaxError)) throw e;
        console.error(e.toString());
        process.exit(1);
    }
    var output = new RapydScript.OutputStream(output_options);
    toplevel.print(output);
    output = output.get().replace('__COMPILER_VERSION__', source_hash);
    await fs.promises.writeFile(path.join(out_path, 'compiler.js'), output, "utf8");
    await fs.promises.writeFile(path.join(out_path, 'baselib-plain-pretty.js'), compiled_baselib.pretty, 'utf-8');
    await fs.promises.writeFile(path.join(out_path, 'baselib-plain-ugly.js'), compiled_baselib.ugly, 'utf-8');
    console.log('Compiler built in', (new Date().getTime() - t1)/1000, 'seconds\n');
    return output;
}

async function run_single_compile(base_path, src_path, lib_path, profile) {
    var out_path = path.join(path.dirname(lib_path), 'dev');
    var signatures = path.join(out_path, 'signatures.json');
    var temp = await check_for_changes(base_path, src_path, signatures);
    var source_hash = temp[0], compiler_changed = temp[1], sources = temp[2], hashes = temp[3];

    if (compiler_changed) {
        await compile(src_path, lib_path, sources, source_hash, profile);
        await fs.promises.writeFile(signatures, JSON.stringify(hashes, null, 4));
    } else console.log('Compiler is built with the up-to-date version of itself');
    return compiler_changed;
}

async function write_stdlib_modules(src_path, out_path) {
    var lib_dir = path.join(src_path, 'lib');
    var entries = await fs.promises.readdir(lib_dir);
    var modules = entries
        .filter(function (name) { return name.slice(-4) === '.pyj'; })
        .map(function (name) { return name.slice(0, -4); })
        .sort();
    await fs.promises.writeFile(
        path.join(out_path, 'stdlib_modules.json'),
        JSON.stringify(modules),
        'utf-8'
    );
}

export default async function compile_self(base_path, src_path, lib_path, complete, profile) {
    var changed;
    do {
        changed = await run_single_compile(base_path, src_path, lib_path, profile);
        lib_path = path.join(path.dirname(lib_path), 'dev');
    } while (changed && complete);
    await write_stdlib_modules(src_path, lib_path);
}
