use 5.020;
use strict;
use warnings;
use ExtUtils::MakeMaker;
use Config;
use Getopt::Long;
use Getopt::Long 'GetOptions';
use File::Path 'mkpath', 'rmtree';

use FindBin;
use lib "$FindBin::Bin/lib";
use SPVM::Builder::Util::API;
use SPVM::Builder::Config::DLL;
use SPVM::Builder::CompileInfo;
use SPVM::Builder::LinkInfo;

########## [SPVM Only Module Loading Start] ##########
use SPVM::Builder::Util;
########## [SPVM Only Module Loading End] ##########

GetOptions(
  # Compiler and linker flags
  'ccflag=s' => \my @ccflags,
  'define=s' => \my @defines,
  'ldflag=s' => \my @ldflags,
  'debug' => \my $debug,
  'build-type=s' => \my $build_type,
  'asan-on-linux' => \my $asan_on_linux,
  
  # Build target settings
  'no-build-spvm-modules' => \my $no_build_spvm_modules,
  
  # For cpan/cpanm to run Makefile.PL
  'meta' => \my $meta,
  
  # Paralle settings
  'parallel-make' => \my $paralle_make,
  'parallel-test' => \my $paralle_test,
  'jobs=i' => \my $jobs,
  
  ########## [SPVM Only Command Line Options Start] ##########
  'no-precompile-tests' => \my $no_precompile_tests,
  ########## [SPVM Only Line Options Options End] ##########
);

my $build_dir = '.spvm_build';
mkpath $build_dir;

my $config_global_file = '.spvm_build/global.config';
&generate_config_global_file($config_global_file, {
  ccflags       => \@ccflags,
  defines       => \@defines,
  ldflags       => \@ldflags,
  debug         => $debug,
  build_type    => $build_type,
  asan_on_linux => $asan_on_linux,
});

my $gnu_make = SPVM::Builder::Util::API::search_gnu_make_command();

if ($meta) {
  $no_build_spvm_modules = 1;
}

my $asan_logs_dir = "$FindBin::Bin/.spvm_build/asan_logs";
if ($asan_on_linux) {
  rmtree $asan_logs_dir;
  mkpath $asan_logs_dir;
}

# Jobs count for pallalel build and tests
unless (defined $jobs) {
  my $cpus = SPVM::Builder::Util::API::get_cpu_count();
  $jobs = $cpus + 2;
}
# Limit the maximum number of parallel jobs to 16 to prevent Windows from exhausting OS resources, such as process handles and kernel memory, ensuring stable performance during JIT builds.
if ($jobs > 16) {
  $jobs = 16;
}

########## [SPVM Only Build Loigc Start] ##########

# Check supported environment
{
  # SPVM requires a 64-bit address space (pointer size must be 8 bytes)
  # This ensures support for large memory maps required by coroutines.
  my $ptrsize = $Config{ptrsize};
  if ($ptrsize < 8) {
    warn "SPVM doesn't support 32-bit address spaces (the pointer size is $ptrsize bytes). A 64-bit environment is required.\n";
    die "OS unsupported\n";
  }
  
  # SPVM requires GNU Make for the order-only dependency syntax '|' and the 'override' syntax.
  unless ($gnu_make) {
    warn "Building SPVM needs GNU Make.\n";
    die "OS unsupported\n";
  }
}

# Always true in SPVM
$paralle_make = 1;
$paralle_test = 1;

if ($meta) {
  $no_build_spvm_modules = 1;
}

# If you edit yacc/spvm_yacc.y, spvm_yacc.c must be re-generated by the following command before "perl Makefile.PL"
#   yacc/bison.sh

# Updates src/spvm_precompile_header_content.c
SPVM::Builder::Util::build_precompile_header_content_c_source();

my $config = SPVM::Builder::Util::API::create_default_config;
my $config_global = SPVM::Builder::Config::Util::load_config_global($config_global_file);
$config->global($config_global);
$config->global->apply_build_rules($config);

my $compile_info = SPVM::Builder::CompileInfo->new(config => $config);

my $link_info = SPVM::Builder::LinkInfo->new(config => $config);

# CCFLAGS
my $ccflags = '';
$ccflags .= " $Config{ccflags}";
$ccflags .= ' ' . join(' ', @{$compile_info->create_ccflags});

# LDDLFLAGS
my $ldflags = $Config{lddlflags};
$ldflags =~ s/(?:^|\s)-O[0-9s]\b//g; # Remove -O[0-9s] to ensure clean debug session
$ldflags =~ s/(?:^|\s)-s(?:\s|$)/ /g; # Remove strip flag (-s) more robustly
$ldflags =~ s/-shared//g;
$ldflags =~ s/-bundle//g;
$ldflags .= ' ' . join(' ', @{$link_info->create_ldflags});

# C - Source files
my $spvm_core_source_file_names = SPVM::Builder::Util::get_spvm_core_source_file_names();
my @spvm_core_source_files = map { "lib/SPVM/Builder/src/$_" } @$spvm_core_source_file_names;
my @csource_files = ('SPVM.c', @spvm_core_source_files);

# H - Header files(This is only used to resolve dependencies)
my @spvm_header_files = glob 'lib/SPVM/Builder/include/*.h';
my @header_files = ('ppport.h', @spvm_header_files);

########## [SPVM Only Build Loigc End] ##########

my $object_map = {};
for my $c_file (@csource_files) {
    my $obj_file = $c_file;
    $obj_file =~ s/\.c$/\$(OBJ_EXT)/;
    $object_map->{$obj_file} = $c_file;
}

WriteMakefile(
  NAME => 'SPVM',
  VERSION_FROM => 'lib/SPVM.pm',
  LICENSE => 'mit',
  ($] >= 5.005 ?     ## Add these new keywords supported since 5.005
    (ABSTRACT_FROM => 'lib/SPVM.pm', # retrieve abstract from module
     AUTHOR => 'Yuki Kimoto<kimoto.yuki@gmail.com>') : ()),
  test => {TESTS => 't/*.t t/*/*.t t/*/*/*.t t/*/*/*/*.t'},
  clean => {FILES => ".spvm_build SPVM.o lib/SPVM/Builder/src/*.o t/03_precompile"},
  META_MERGE => {
    'meta-spec' => { version => 2 },
    resources => {
      repository => {
        type => 'git',
        url => 'https://github.com/yuki-kimoto/SPVM.git',
        web => 'https://github.com/yuki-kimoto/SPVM',
      },
      bugtracker => {
        web => "https://github.com/yuki-kimoto/SPVM/issues",
      },
    },
  },
  MAKE => $gnu_make,
  macro => {
    $paralle_make ? (MAKEFLAGS => "-j$jobs") : (),
    
    # Inject eval "\$ENV{HARNESS_OPTIONS}='j$jobs'" at the beginning of the command line for parallel tests overriding TEST_VERBOSE
    # Constraints for Windows shell compatibility (pwsh/cmd.exe):
    # 1. The entire command is wrapped in double quotes.
    # 2. No backslashes (\) to avoid escape hell.
    # 3. No dollar signs ($) to prevent pwsh variable expansion.
    # 4. No nested parentheses to avoid pwsh tokenization errors.
    # 5. Only '();. are used for maximum safety.
    # 6. Minimal symbols to avoid edge cases in shell parsing.
    $paralle_test ? ('override TEST_VERBOSE' => "scalar (eval chr(36) . q(ENV{HARNESS_OPTIONS}='j$jobs'), 0)") : (),
    $asan_on_linux ? ('override FULLPERL' => qq|LD_PRELOAD=\$\$($Config{cc} -print-file-name=libasan.so) ASAN_OPTIONS="log_path=$asan_logs_dir/asan.log:exitcode=0" $^X|) : (),
  },
  NORECURS => 1,
  CONFIGURE_REQUIRES => {
  },
  PREREQ_PM => {
    'ExtUtils::CBuilder' => '0.280236',
    'Time::Piece' => '1.12',
    'JSON::PP' => '2.27105',
  },
  TEST_REQUIRES => {
    'Test::More' => '0.92',
  },
  ########## [SPVM Only WriteMakefile Options Start] ##########
  CCFLAGS => $ccflags,
  OPTIMIZE => ' ',
  LDDLFLAGS => $ldflags,
  C => [@csource_files],
  H => [@header_files],
  OBJECT => '$(O_FILES)', # link all the C files too
  EXE_FILES => ['script/spvm', 'script/spvmcc', 'script/spvmdist', 'script/spvmdeps'],
  ########## [SPVM Only WriteMakefile Options End] ##########
);

package MY {
  
  ########## [SPVM Only Module Loading Start] ##########
  use File::Find 'find';
  ########## [SPVM Only Module Loading End] ##########
  
  # Add Build shared library make rule
  sub postamble {
    
    # The content of Makefile
    my $make_rule = '';
    
    ########## [SPVM Only Compilation Start] ##########
    for my $spvm_core_source_file (@spvm_core_source_files) {
      my $spvm_core_object_file = $spvm_core_source_file;
      $spvm_core_object_file =~ s/\.c$/\$(OBJ_EXT)/;
      
      $make_rule .= "$spvm_core_object_file : $spvm_core_source_file\n";
      
      my $spvm_core_ccflags = $compile_info->create_ccflags;
      
      my $spvm_core_cc = $compile_info->config->cc;
      
      $make_rule .= "\t$spvm_core_cc -c @$spvm_core_ccflags -o $spvm_core_object_file $spvm_core_source_file\n\n";
    }
    ########## [SPVM Only Compilation End] ##########
    
    unless ($no_build_spvm_modules) {
      my $options = {};
      $options->{dependent_files} = ['SPVM.xs'];
      
      $options->{order_only_dependent_files} = ['$(INST_DYNAMIC)', 'pm_to_blib'];
      
      $options->{config_global_file} = $config_global_file;
      
      # Test create_make_rule for backward compatibility and practical testing
      $make_rule .= SPVM::Builder::Util::API::create_make_rule_native('Fn', $options);
      $make_rule .= SPVM::Builder::Util::API::create_make_rule_precompile('Fn', $options);
      
      # Native classes (excluding Fn)
      $options->{native_classes} = [
        'Format',
        'Sync::Mutex',
        'Native',
        'Native::Compiler',
        'Native::ClassFile',
        'Native::Runtime',
        'Native::Env',
        'Native::Stack',
        'Native::BasicType',
        'Native::ClassVar',
        'Native::Field',
        'Native::Method',
        'Native::Arg',
        'Native::MethodCall',
        'Native::API',
        'Native::Constant',
      ];

      # Precompile classes (excluding Fn)
      $options->{precompile_classes} = [
        'Format',
        'Sort',
        'Packer',
      ];
      
      # Specify text files containing additional classes for practical tests
      # These files contain newline-separated class names like 'Array'
      $options->{native_classes_file} = 'native_classes.txt';
      $options->{precompile_classes_file} = 'precompile_classes.txt';
      
      # Generate a single parallel make rule for the rest
      $make_rule .= SPVM::Builder::Util::API::create_make_rule_parallel($options);
    }
    
    ########## [SPVM Only Test Generation Start] ##########
    unless ($no_precompile_tests) {
      # Create precompile tests
      my @test_files;
      find(
        sub {
          if (-f $File::Find::name) {
            my $rel_path = $File::Find::name;
            $rel_path =~ s|^\Q$FindBin::Bin/||;
            
            # Slip hidden files
            return if $rel_path =~ /[\/\\]\./;

            push @test_files, $rel_path;
          }
        },
        "$FindBin::Bin/t/02_vm"
      );
      my $test_files_str = join(' ', @test_files);
      
      my $time_stamp_file = "$FindBin::Bin/t/03_precompile/time_stamp.txt";
      
      $make_rule .= "dynamic :: $time_stamp_file\n";
      $make_rule .= "\t\$(NOECHO) \$(NOOP)\n\n";

      $make_rule .= "$time_stamp_file :: t/utils/copy_vm_to_precompile.pl $test_files_str\n";
      $make_rule .= "\tperl t/utils/copy_vm_to_precompile.pl\n";
    }
    ########## [SPVM Only Test Generation End] ##########
    
    return $make_rule;
  }
  
}

sub generate_config_global_file {
  my ($config_global_file, $options) = @_;
  
  my $ccflags       = $options->{ccflags} // [];
  my $defines       = $options->{defines} // [];
  my $ldflags       = $options->{ldflags} // [];
  my $build_type    = $options->{build_type};
  my $debug         = $options->{debug};
  my $asan_on_linux = $options->{asan_on_linux};
  
  # --- Handle options ---
  if ($debug) {
    $build_type = 'Debug';
  }
  
  if ($asan_on_linux) {
    push @$ccflags, "-fsanitize=address", "-fno-omit-frame-pointer";
    push @$ldflags, "-fsanitize=address";
  }
  
  # --- Generate content ---
  my $config_global_content = '';
  
  $config_global_content .= <<"EOS";
my \$config_global = SPVM::Builder::Config::Global->new;
EOS
  
  if (defined $build_type) {
    $config_global_content .= <<"EOS";
\$config_global->build_type('$build_type');
EOS
  }
  
  for my $ccflag (@$ccflags) {
    $config_global_content .= <<"EOS";
\$config_global->build_rule_any({'+ccflags' => ['$ccflag']});
EOS
  }
  
  for my $define (@$defines) {
    $config_global_content .= <<"EOS";
\$config_global->build_rule_any({'+defines' => ['$define']});
EOS
  }
  
  for my $ldflag (@$ldflags) {
    $config_global_content .= <<"EOS";
\$config_global->build_rule_any({'+ldflags' => ['$ldflag']});
EOS
  }
  
  $config_global_content .= <<"EOS";
\$config_global;
EOS
  
  # --- Write to file ---
  open my $fh, '>', $config_global_file or die "Can't open file '$config_global_file': $!";
  print $fh $config_global_content;
}

1;

=head1 Name 

SPVM Makefile.PL

=head1 Description

Makefile.PL for L<SPVM> build.

These options are also available in the C<Makefile.PL> generated by L<spvmdist>.

=head1 Usage

  # Basic build
  perl Makefile.PL

  # Debug build with specific compiler flags
  perl Makefile.PL --debug
  
  # Debug build with specific compiler flags and ASan on Linux
  perl Makefile.PL --debug --asan-on-linux
   
  # Parallel build and test
  perl Makefile.PL --parallel-make --parallel-test
  
  # Generate metadata only
  perl Makefile.PL --meta

=head1 Command Line Options

=head2 --ccflag

Specifies additional compiler flags.

=head2 --define

Adds C<-D> macros to the compiler.

=head2 --ldflag

Specifies additional linker flags.

=head2 --debug

Sets the build type L</"--build-type"> to C<Debug>.

=head2 --build-type

Specifies the build type. The possible values are C<Debug>, C<Release>, C<RelWithDebInfo>, or C<MinSizeRel>.

=head2 --asan-on-linux

Enables AddressSanitizer (ASan) for memory error detection on Linux.

=head2 --no-build-spvm-modules

Disables the build of SPVM native and precompile modules.

=head2 --meta

Runs in metadata generation mode.

=head2 --parallel-make

Enables parallel builds using multiple CPU cores.

=head2 --parallel-test

Enables parallel execution of tests.

=head2 --jobs

Specifies the number of parallel jobs for the build and testing. Defaults to C<CPU count + 2> (max 16).

=cut
