package App::ArduinoBuilder::Builder;

use strict;
use warnings;
use utf8;

use App::ArduinoBuilder::Config 'get_os_name';
use App::ArduinoBuilder::DepCheck 'check_dep';
use App::ArduinoBuilder::FilePath 'find_all_files_with_extensions';
use App::ArduinoBuilder::Logger ':all_logger';
use File::Basename 'basename';
use File::Path 'make_path';
use File::Spec::Functions 'catfile';

my @supported_source_extensions = qw(c cpp S ino);

sub new {
  my ($class, $config) = @_;
  return bless {config => $config}, $class;
}

sub _execute {
  my ($cmd) = @_;
  log_cmd $cmd;
  system($cmd) and fatal "Can’t execute the following command: $!\n\t${cmd}";
  return 1;
}

sub _run_recipe_pattern {
  # It’s undocumented but any recipe can have several patterns (and this is used
  # by several cores).
  my ($this, $recipe_name, %options) = @_;
  my $recipes = $this->{config}->filter("recipe.${recipe_name}");
  my $is_hook = $recipe_name =~ m/^hooks\./;
  my $nb_recipes = $recipes->size();
  unless ($is_hook || $nb_recipes) {
    error "Can’t find recipe for '${recipe_name}'";
    return;
  }
  debug "Running %d hook%s for '${recipe_name}", $nb_recipes, ($nb_recipes > 1 ? 's' : '') if $is_hook && $nb_recipes;
  for my $k (sort $recipes->keys()) {
    fatal "Invalid recipe name: recipe.${recipe_name}.${k}" unless $k =~ m/^(?:\d+\.)?pattern$/ || $options{is_objcopy};
    fatal "Invalid objcopy recipe name: recipe.${recipe_name}.${k}" if $options{is_objcopy} && $k !~ m/^\w+\.(?:\d+\.)?pattern$/;
    _execute($recipes->get($k, base => $this->{config}, %options));
  }
  return;
}

# This method is a very partial implementation of:
# https://arduino.github.io/arduino-cli/0.32/sketch-build-process/#pre-processing
# TODO: theoritically all .ino and .pde (?) files should be concatenated together
# rather than processed separately.
# TODO: we need to generate prototypes for the methods, using ctags.
sub _ino_to_cpp {
  my ($this, $source, $target) = @_;

  open my $fi, '<', "${source}" or fatal "Can’t open the source file '${source}' for reading: $!";
  open my $fo, '>', "${target}.cpp-pre" or fatal "Can’t open intermediate file '${target}.cpp-pre' for writing: $!";
  print $fo "#include \"Arduino.h\"\n";
  while (my $l = <$fi>) {
    print $fo $l;
  }
  close $fi;
  close $fo;

  my $recipe;
  # This is ugly but we can’t easily do this elsewhere because we need the recipe command to be
  # resolved to be able to find the MMD flag, but we can’t resolve it too early because some
  # parameters will vary with each call.
  if (!$this->{config}->exists('recipe.preproc.macros')) {
    # We assume that this recipe exists (and that it’s not a multi-pattern recipe).
    $recipe = $this->{config}->get('recipe.cpp.o.pattern', allow_partial => 1, with => {source_file => "${target}.cpp-pre", object_file => "${target}.cpp"});
    # This is a heuristic because the '-x c++' must appear before the source file, so we can’t put
    # it at the end. It’s unclear how this command is actually generated by the Arduino GUI.
    $recipe =~ s/(?<=\s)-MMD\b/-w -x c++ -E -CC/ or fatal "Can’t modify the recipe.cpp.o.pattern command: ${recipe}";
  } else {
    # It’s weird but this is not a "pattern" recipe. Why?
    $recipe = $this->{config}->get('recipe.preproc.macros', with => {source_file => "${target}.cpp-pre", preprocessed_file_path => "${target}.cpp"});
  }
  _execute($recipe);

  # TODO: there is a step in the real Arduino tool that is badly documented but which uses ctags to extract the
  # prototype of the functions in the .ino files (and adds them at the beginning of the C++ file), so that the
  # functions can be called before they are declared.
  # my $null = get_os_name() eq 'windows' ? 'nul' : '/dev/null';
  return;
}

# dirname($target) must already exist.
sub build_file {
  my ($this, $source, $target) = @_;
  my @ext = $source =~ m/\.([^.]+)$/;
  fatal "File '$source' has no recognizable extension." unless @ext;
  my $ext = $ext[0];
  if ($ext eq 'ino') {
    debug "Converting '${source}' to C++";
    $this->_ino_to_cpp($source, $target);
    $source = $target.'.cpp';
    $ext = 'cpp';
  }
  debug "Building '${source}'";
  $this->_run_recipe_pattern("${ext}.o", with => {source_file => $source, object_file => $target});
  return;
}

sub _add_to_archive {
  my ($this, $object_file, $archive) = @_;
  $this->_run_recipe_pattern('ar', with => {object_file => $object_file, archive_file => $archive, archive_file_path => catfile($this->{config}->get('build.path'), $archive)});
  return;
}

# target_dir has all the intermediate file, $archive is a file name that goes in build.path.
sub build_archive {
  my ($this, $source_dirs, $target_dir, $archive, $force) = @_;
  make_path($target_dir);
  my $did_something = 0;
  for my $d (@{$source_dirs}) {
    my @sources = find_all_files_with_extensions($d, [@supported_source_extensions]);
    for my $s (@sources) {
      # BUG: There is a bug if several files have the same name in the core, but this is improbable...
      # Note: if a source file is removed, it won’t be removed from the archive. So this is not
      # perfecte but probably acceptable.
      my $object_file = catfile($target_dir, basename($s).'.o');
      if ($force || check_dep($s, $object_file)) {
        $did_something = 1;
        $this->build_file($s, $object_file);
        $this->_add_to_archive($object_file, $archive);
      }
    }
  }
  return $did_something;
}

# Like in find_all_files_with_extensions, $source_dir can be a single directory (scalar)
# or an array-ref of directories.
# excluded_dirs must be an array-ref.
sub build_object_files {
  my ($this, $source_dir, $target_dir, $excluded_dirs, $force, $no_recurse) = @_;
  make_path($target_dir);
  my @sources = find_all_files_with_extensions($source_dir, [@supported_source_extensions], $excluded_dirs, $no_recurse);
  my $did_something = 0;
  for my $s (@sources) {
    # Same BUG here as in build_archive
    my $object_file = catfile($target_dir, basename($s).'.o');
    if ($force || check_dep($s, $object_file)) {
      $did_something = 1;
      $this->build_file($s, $object_file);
    }
  }
  return $did_something;
}

sub link_executable {
  my ($this, $object_files, $archive) = @_;
  $this->_run_recipe_pattern('c.combine', with => {object_files => join(' ', @{$object_files}), archive_file => $archive, archive_file_path => catfile($this->{config}->get('build.path'), $archive)});
  return;
}

sub run_hook {
  my ($this, $hook_name) = @_;
  $this->_run_recipe_pattern("hooks.${hook_name}");
  return;
}

sub objcopy {
  my ($this) = @_;
  $this->_run_recipe_pattern('objcopy', is_objcopy => 1);
}

1;
