#!/usr/bin/env perl

use strict;
use warnings;
use feature 'say';
use File::Copy qw(copy);
use File::Path qw(rmtree make_path);
use Cwd        qw(abs_path);
use FindBin;
use lib "$FindBin::Bin/Modules";
use Util;

# ========================================================================================= #
# Project root
# ========================================================================================= #

my $root = abs_path("$FindBin::Bin/..");
chdir($root) or die "Cannot chdir to $root: $!\n";

# ========================================================================================= #
# Environment set by "nix develop"
# ========================================================================================= #

sub require_env {
    my ($name) = @_;
    return $ENV{$name}
      // die "$name is not set (run from 'nix develop' shell)\n";
}

my $wasi_root = require_env('WASI_ROOT');
my $wamr_root = require_env('WAMR_ROOT');
my $cross_cc  = require_env('CROSS_CC');
my $linux_cc  = require_env('LINUX_CC');

my $use_aot_in_text  = ( $ENV{WAMR_USE_AOT_IN_TEXT}  // 'false' ) eq 'true';
my $use_mmap_in_text = ( $ENV{WAMR_USE_MMAP_IN_TEXT} // 'false' ) eq 'true';
my $use_xip          = ( $ENV{WAMR_USE_XIP}          // 'false' ) eq 'true';
my $use_allocator    = ( $ENV{WAMR_USE_ALLOCATOR}    // 'false' ) eq 'true';
my $use_global_heap_in_text =
  ( $ENV{WAMR_USE_GLOBAL_HEAP_IN_TEXT} // 'false' ) eq 'true';
my $use_runtime_pool_in_text =
  ( $ENV{WAMR_USE_RUNTIME_POOL_IN_TEXT} // 'false' ) eq 'true';
my $use_linear_pool_in_text =
  ( $ENV{WAMR_USE_LINEAR_POOL_IN_TEXT} // 'false' ) eq 'true';

# ========================================================================================= #
# WAMR cmake configuration
# ========================================================================================= #

# Flags common to all platforms
my @wamr_cmake_base = (
    '-DCMAKE_BUILD_TYPE=Debug',    '-DWAMR_BUILD_TARGET=X86_32',
    '-DWAMR_BUILD_AOT=1',          '-DWAMR_BUILD_WAMR_COMPILER=0',
    '-DWAMR_BUILD_INTERP=1',       '-DWAMR_BUILD_FAST_INTERP=0',
    '-DWAMR_BUILD_JIT=0',          '-DWAMR_BUILD_FAST_JIT=0',
    '-DWAMR_BUILD_LIBC_BUILTIN=1', '-DWAMR_BUILD_LIBC_WASI=0',
    '-DWAMR_BUILD_SIMD=0',
);

my @wamr_cmake_baremetal = (
    '-DWAMR_BUILD_PLATFORM=baremetal',
    '-DCMAKE_SYSTEM_NAME=Generic',
    '-DCMAKE_SYSTEM_PROCESSOR=i386',
    '-DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY',
);

my @wamr_cmake_linux = ( '-DWAMR_BUILD_PLATFORM=linux', );

# Variant defines passed as CMAKE_C_FLAGS
my @variant_cflags =
  ( '-Wno-error=implicit-function-declaration', '-O0', '-ggdb3' );
push @variant_cflags,
  $use_mmap_in_text ? '-DWAMR_MMAP_IN_TEXT=1' : '-DWAMR_MMAP_IN_TEXT=0';
push @variant_cflags, '-DWASM_MEM_ALLOC_WITH_USAGE=1' if $use_allocator;
my $cmake_c_flags = join( ' ', @variant_cflags );

# Variant options passed as cmake variables
my @variant_cmake_flags;
push @variant_cmake_flags, '-DWAMR_BUILD_ALLOC_WITH_USAGE=1' if $use_allocator;

# Defines forwarded to the host compilation so wasm_host.c sees the same
# WASM_MEM_ALLOC_WITH_USAGE value as libiwasm.a was built with.
my @host_variant_cflags;
push @host_variant_cflags, '-DWASM_MEM_ALLOC_WITH_USAGE=1' if $use_allocator;
push @host_variant_cflags,
  $use_global_heap_in_text
  ? '-DWAMR_GLOBAL_HEAP_IN_TEXT=1'
  : '-DWAMR_GLOBAL_HEAP_IN_TEXT=0';
push @host_variant_cflags,
  $use_runtime_pool_in_text
  ? '-DWAMR_RUNTIME_POOL_IN_TEXT=1'
  : '-DWAMR_RUNTIME_POOL_IN_TEXT=0';
push @host_variant_cflags,
  $use_linear_pool_in_text
  ? '-DWAMR_LINEAR_POOL_IN_TEXT=1'
  : '-DWAMR_LINEAR_POOL_IN_TEXT=0';

# ========================================================================================= #
# Compiler / linker flags
# ========================================================================================= #

my @wasi_cflags = (
    '--target=wasm32',          "--sysroot=$wasi_root/share/wasi-sysroot",
    '-z',                       'stack-size=4096',
    '-O0',                      '-nostdlib',
    '-Wl,--no-entry',           '-Wl,--export=wasm_module',
    '-Wl,--no-gc-sections',     '-Wl,--initial-memory=65536',
    '-Wl,--export=__heap_base', '-Wl,--export=__data_end',
);

my @cross_cflags =
  qw(-O0 -m32 -ffunction-sections -fdata-sections -ffreestanding -fpermissive -ggdb3);
my @linux_cflags =
  qw(-O0 -m32 -ffunction-sections -fdata-sections -fpermissive -ggdb3);
my @linux_baremetal_cflags =
  qw(-O0 -m32 -ffunction-sections -fdata-sections -ffreestanding -ggdb3);

# libiwasm.a is passed as a direct file path (no -L/-liwasm)
my @cross_ldflags_base = (
    '-Wl,--build-id=none', '-static', '-nostdlib', '-m32',
    '-lc', '-lgcc', '-lm'
);
my @linux_ldflags_base     = ( '-Wl,--build-id=none', '-m32', '-lm' );
my @baremetal_ldflags_base = (
    '-Wl,--build-id=none', '-static', '-nostdlib', '-m32',
    '-lc', '-lgcc', '-lm'
);
my @cross_ldflags_nowasm = (
    '-Wl,--build-id=none', '-static', '-nostdlib', '-m32',
    '-lc', '-lgcc', '-lm'
);
my @linux_ldflags_nowasm = ( '-Wl,--build-id=none', '-m32', '-lm' );

my @wamr_inc_baremetal = (
    "-I$wamr_root/core/iwasm/include",
    "-I$wamr_root/core/shared/utils",
    "-I$wamr_root/core/shared/platform/baremetal",
);
my @wamr_inc_linux = (
    "-I$wamr_root/core/iwasm/include",
    "-I$wamr_root/core/shared/utils",
    "-I$wamr_root/core/shared/platform/linux",
);

my @cross_wamrcflags = ( '--target=i386', '--cpu=generic', '--opt-level=0' );
push @cross_wamrcflags, '--xip' if $use_xip;
my @linux_wamrcflags = ( '--target=i386', '--cpu=generic', '--opt-level=0' );

# ========================================================================================= #
# Entry point
# ========================================================================================= #

die "Usage: compile.pl <module> <target> <mode>\n"
  . "  target: fail | linux | linux-baremetal\n"
  . "  mode:   aot  | interp | c\n"
  unless @ARGV == 3;

my ( $module, $target, $mode ) = @ARGV;

build( $module, $target, $mode );

# ========================================================================================= #
# Build
# ========================================================================================= #

sub build {
    my ( $module, $target, $mode ) = @_;
    my $bd = "build-$module";

    rmtree($bd);
    make_path($bd);
    copy_auxiliary( $module, $bd );

    if ( $mode eq 'aot' || $mode eq 'interp' ) {
        build_libiwasm( $module, $bd, $target );
        compile_wasm_module( $module, $bd );
        if ( $mode eq 'aot' ) {
            compile_wasm_aot( $module, $bd, $target );
            make_aot_array( $module, $bd );
        }
        else {
            make_interp_array( $module, $bd );
        }
        prepare_wasm_host( $module, $bd, $mode );
        compile_wasm_host( $module, $bd, $target );
        compile_startup( $module, $bd, $target );
        compile_syscalls( $module, $bd, $target );
        link_wasm( $module, $bd, $target );
    }
    elsif ( $mode eq 'c' ) {
        compile_c_module( $module, $bd, $target );
        compile_c_host( $module, $bd, $target );
        compile_startup( $module, $bd, $target );
        compile_syscalls( $module, $bd, $target );
        link_c( $module, $bd, $target );
    }
    else {
        die "Unknown mode '$mode'; expected: aot, interp, c\n";
    }

    build_iso( $module, $bd )
      if $target eq 'fail' || $target eq 'linux-baremetal';
}

# ========================================================================================= #
# Steps
# ========================================================================================= #

sub build_libiwasm {
    my ( $module, $bd, $target ) = @_;
    my $cmake_bd = "$bd/wamr-build";
    make_path($cmake_bd);

    my ( $cmake_cc, @sys_flags ) =
        ( $target eq 'linux' )
      ? ( $linux_cc, @wamr_cmake_linux )
      : ( $cross_cc, @wamr_cmake_baremetal );

    Util::run(
        'cmake',              '-S',
        $wamr_root,           '-B',
        $cmake_bd,            "-DCMAKE_C_COMPILER=$cmake_cc",
        @wamr_cmake_base,     @sys_flags,
        @variant_cmake_flags, "-DCMAKE_C_FLAGS=$cmake_c_flags",
    );
    Util::run( 'cmake', '--build', $cmake_bd );
    copy( "$cmake_bd/libiwasm.a", "$bd/libiwasm.a" )
      or die "failed to copy $cmake_bd/libiwasm.a: $!";
    rmtree($cmake_bd) or die "failed to remove $cmake_bd: $!";
}

sub copy_auxiliary {
    my ( $module, $bd ) = @_;
    my @files = (
        [ 'flake.nix',                       "$bd/flake.nix" ],
        [ 'scripts/build.pl',                "$bd/build.pl" ],
        [ 'scripts/compile.pl',              "$bd/compile.pl" ],
        [ 'scripts/runner.pl',               "$bd/runner.pl" ],
        [ 'targets/lib.h',                   "$bd/lib.h" ],
        [ 'targets/linker.ld',               "$bd/linker.ld" ],
        [ 'targets/startup.s',               "$bd/startup.s" ],
        [ 'targets/syscalls.c',              "$bd/syscalls.c" ],
        [ "targets/wasm-module/$module.cpp", "$bd/wasm-module.cpp" ],
    );
    for my $pair (@files) {
        copy( $pair->[0], $pair->[1] )
          or die "failed to copy $pair->[0] -> $pair->[1]: $!";
    }

    # Copy tacle benchs if module is from tacle-bench
    if ( Util::module_is_tacle($module) ) {
        my ( undef, $src_dir ) = Util::find_tacle_sources($module);
        my $dst_dir = "$bd/tacle-src";
        make_path($dst_dir);
        for my $file ( glob("$src_dir/*.c"), glob("$src_dir/*.h") ) {
            my $base = ( File::Spec->splitpath($file) )[2];
            copy( $file, "$dst_dir/$base" )
              or die "failed to copy $file to $dst_dir/$base: $!";
        }
    }
}

sub compile_wasm_module {
    my ( $module, $bd ) = @_;
    if ( Util::module_is_tacle($module) ) {
        my ( $srcs, $inc ) = Util::find_tacle_sources($module);
        Util::run(
            "$wasi_root/bin/clang",       @wasi_cflags,
            "-Dmain=tacle_main_bullshit", "-DTARGET_WASM",
            "-I$inc",                     "targets/wasm-module/$module.cpp",
            @$srcs,                       '-o',
            "$bd/wasm_module.wasm"
        );
    }
    else {
        Util::run( "$wasi_root/bin/clang", @wasi_cflags,
            "-DTARGET_WASM", "targets/wasm-module/$module.cpp",
            '-o',            "$bd/wasm_module.wasm" );

    }
}

sub compile_wasm_aot {
    my ( $module, $bd, $target ) = @_;
    my @flags = ( $target eq 'linux' ) ? @linux_wamrcflags : @cross_wamrcflags;
    Util::run( 'wamrc', @flags, '-o', "$bd/wasm_module.aot",
        "$bd/wasm_module.wasm" );
}

sub make_aot_array {
    my ( $module, $bd ) = @_;
    open( my $xxd, '-|', 'xxd', '-i', "$bd/wasm_module.aot" )
      or die "failed to run xxd: $!";
    my $content = do { local $/; <$xxd> };
    close($xxd);

    $content =
      qq{__attribute__((section(".text.wamr_aot"), aligned(4096)))\n}
      . $content
      if $use_aot_in_text;

    Util::write_file( "$bd/wasm_aot_array.c", $content );
}

sub make_interp_array {
    my ( $module, $bd ) = @_;
    open( my $xxd, '-|', 'xxd', '-i', "$bd/wasm_module.wasm" )
      or die "Cannot run xxd: $!\n";
    my $content = do { local $/; <$xxd> };
    close($xxd);
    Util::write_file( "$bd/wasm_interp_array.c", $content );
}

sub prepare_wasm_host {
    my ( $module, $bd, $mode ) = @_;
    my $template = 'targets/wasm-host/wasm_host.c';

    my $mod_c = ( $module =~ s/-/_/gr );
    my ( $array_file, $array_sym, $len_sym ) =
      ( $mode eq 'aot' )
      ? (
        'wasm_aot_array.c',
        "build_${mod_c}_wasm_module_aot",
        "build_${mod_c}_wasm_module_aot_len"
      )
      : (
        'wasm_interp_array.c',
        "build_${mod_c}_wasm_module_wasm",
        "build_${mod_c}_wasm_module_wasm_len"
      );

    my $content = Util::read_file($template);
    $content =~ s/__WASM_ARRAY_FILE__/$array_file/g;
    $content =~ s/__WASM_ARRAY__/$array_sym/g;
    $content =~ s/__WASM_ARRAY_LEN__/$len_sym/g;
    Util::write_file( "$bd/module_host.c", $content );
}

sub compile_wasm_host {
    my ( $module, $bd, $target ) = @_;
    if ( $target eq 'fail' ) {
        Util::run(
            $cross_cc,           '-I./targets/wasm-host',
            @cross_cflags,       @host_variant_cflags,
            @wamr_inc_baremetal, '-DTARGET_FAIL',
            '-c',                "$bd/module_host.c",
            '-o',                "$bd/system.o"
        );
    }
    elsif ( $target eq 'linux' ) {
        Util::run(
            $linux_cc,       '-I./targets/wasm-host',
            @linux_cflags,   @host_variant_cflags,
            @wamr_inc_linux, '-DTARGET_LINUX',
            '-c',            "$bd/module_host.c",
            '-o',            "$bd/system.o"
        );
    }
    elsif ( $target eq 'linux-baremetal' ) {
        Util::run(
            $cross_cc,               '-I./targets/wasm-host',
            @linux_baremetal_cflags, @host_variant_cflags,
            @wamr_inc_baremetal,     '-DTARGET_LINUX_BAREMETAL',
            '-c',                    "$bd/module_host.c",
            '-o',                    "$bd/system.o"
        );
    }
    else {
        die "Unknown target '$target'\n";
    }
}

sub compile_c_module {
    my ( $module, $bd, $target ) = @_;
    my ( $cc, @flags ) =
        ( $target eq 'linux' ) ? ( $linux_cc, @linux_cflags, '-DTARGET_LINUX' )
      : ( $target eq 'linux-baremetal' )
      ? ( $cross_cc, @linux_baremetal_cflags, '-DTARGET_LINUX_BAREMETAL' )
      : ( $cross_cc, @cross_cflags, '-DTARGET_FAIL' );

    if ( Util::module_is_tacle($module) ) {
        my ( $srcs, $inc ) = Util::find_tacle_sources($module);

        # Compile the wrapper separately, so the main can be renamed
        Util::run( $cc, @flags, "-I$inc", '-c',
            "targets/wasm-module/$module.cpp",
            '-o', "$bd/c_module.o" );

        my @objs;
        for my $src (@$srcs) {
            ( my $obj = $src ) =~ s{.*/}{};
            $obj =~ s/\.c$/.o/;
            $obj = "$bd/tacle_$obj";
            Util::run( $cc, @flags, "-I$inc", '-Dmain=tacle_main_bullshit',
                '-c', $src, '-o', $obj );
            push @objs, $obj;
        }
    }
    else {
        Util::run( $cc, @flags, '-c', "targets/wasm-module/$module.cpp",
            '-o', "$bd/c_module.o" );
    }
}

sub compile_c_host {
    my ( $module, $bd, $target ) = @_;
    my ( $cc, @flags ) =
        ( $target eq 'linux' ) ? ( $linux_cc, @linux_cflags, '-DTARGET_LINUX' )
      : ( $target eq 'linux-baremetal' )
      ? ( $cross_cc, @linux_baremetal_cflags, '-DTARGET_LINUX_BAREMETAL' )
      : ( $cross_cc, @cross_cflags, '-DTARGET_FAIL' );
    Util::run( $cc, @flags, '-c', 'targets/c-host/c_host.c', '-o',
        "$bd/c_host.o" );
    copy( 'targets/c-host/c_host.c', "$bd/module_host.c" );
}

sub compile_startup {
    my ( $module, $bd, $target ) = @_;
    return unless $target eq 'fail' || $target eq 'linux-baremetal';
    Util::run( $cross_cc, 'targets/startup.s', '-I./targets/wasm-host',
        @cross_cflags, '-c', '-o', "$bd/startup.o" );
}

sub compile_syscalls {
    my ( $module, $bd, $target ) = @_;
    if ( $target eq 'fail' ) {
        Util::run( $cross_cc, 'targets/syscalls.c',
            '-I./targets/wasm-host', @cross_cflags,
            '-c', '-o', "$bd/syscalls.o" );
    }
    elsif ( $target eq 'linux-baremetal' ) {
        Util::run( $cross_cc, 'targets/syscalls.c', @linux_baremetal_cflags,
            '-c', '-o', "$bd/syscalls.o" );
    }

    # Linux needs neither startup nor syscall stubs
}

sub link_wasm {
    my ( $module, $bd, $target ) = @_;
    if ( $target eq 'fail' ) {
        Util::run(
            $cross_cc,           '-Wl,-T',
            'targets/linker.ld', "$bd/system.o",
            "$bd/startup.o",     "$bd/syscalls.o",
            "$bd/libiwasm.a",    @cross_ldflags_base,
            '-o',                "$bd/system.elf"
        );
    }
    elsif ( $target eq 'linux' ) {
        Util::run( $linux_cc, "$bd/system.o", "$bd/libiwasm.a",
            @linux_ldflags_base, '-o', "$bd/system.elf" );
    }
    elsif ( $target eq 'linux-baremetal' ) {
        Util::run(
            $cross_cc,           '-Wl,-T',
            'targets/linker.ld', "$bd/system.o",
            "$bd/startup.o",     "$bd/syscalls.o",
            "$bd/libiwasm.a",    @baremetal_ldflags_base,
            '-o',                "$bd/system.elf"
        );
    }
    else {
        die "Unknown target '$target'\n";
    }
}

sub link_c {
    my ( $module, $bd, $target ) = @_;
    my @tacle_objs =
      Util::module_is_tacle($module) ? glob("$bd/tacle_*.o") : ();
    if ( $target eq 'fail' ) {
        Util::run(
            $cross_cc,             '-Wl,-T',
            'targets/linker.ld',   "$bd/c_host.o",
            "$bd/startup.o",       "$bd/syscalls.o",
            "$bd/c_module.o",      @tacle_objs,
            @cross_ldflags_nowasm, '-o',
            "$bd/system.elf"
        );
    }
    elsif ( $target eq 'linux' ) {
        Util::run( $linux_cc, "$bd/c_host.o", "$bd/c_module.o", @tacle_objs,
            @linux_ldflags_nowasm, '-o', "$bd/system.elf" );
    }
    elsif ( $target eq 'linux-baremetal' ) {
        Util::run(
            $cross_cc,               '-Wl,-T',
            'targets/linker.ld',     "$bd/c_host.o",
            "$bd/startup.o",         "$bd/syscalls.o",
            "$bd/c_module.o",        @tacle_objs,
            @baremetal_ldflags_base, '-o',
            "$bd/system.elf"
        );
    }
    else {
        die "C mode is not supported for target '$target'\n";
    }
}

sub build_iso {
    my ( $module, $bd ) = @_;
    make_path("$bd/grub/boot/grub");
    copy( 'targets/grub.cfg', "$bd/grub/boot/grub/grub.cfg" )
      or die "failed to copy grub.cfg: $!\n";
    copy( "$bd/system.elf", "$bd/grub/boot/system.elf" )
      or die "failed to copy system.elf: $!\n";
    Util::run( 'grub-mkrescue', '-o', "$bd/system.iso", "$bd/grub" );
}
