# Copyright (c) 2023 Yuki Kimoto
# MIT License

class File::Find {
  version "0.033";
  
  use File::Find::Info;
  use File::Find::Callback;
  
  use Fn;
  use Hash;
  use StringList;
  use Re;
  use Sys;
  use Sys::OS;
  use IO::Dir;
  
  # Fields
  has follow : ro byte;
  
  has warn : ro byte;
  
  # Undocumented Fields
  has SLnkSeen : Hash of Int;
  
  has file_seen_h : Hash of Int;
  
  has skip_pattern : string;
  
  # Class Methods
  static method new : File::Find ($options : object[] = undef) {
    
    my $self = new File::Find;
    
    my $option_names = [
      "follow",
      "warn",
    ];
    
    Fn->check_option_names($options, $option_names);
    
    my $options_h = Hash->new($options);
    
    $self->{follow} = (byte)$options_h->get_or_default("follow", 0)->(int);
    
    $self->{SLnkSeen} = Hash->new;
    
    $self->{file_seen_h} = Hash->new;
    
    $self->{skip_pattern} = "^\.{1,2}\z";
    
    $self->{warn} = (byte)$options_h->get_or_default("warn", 0)->(int);
    
    return $self;
  }
  
  # Instance Methods
  method find : void ($cb : File::Find::Callback, $top_dir : string) {
    
    unless ($top_dir) {
      die "The top directory \$top_dir must be defined." ;
    }
    
    my $top_dir = copy $top_dir;
    
    # canonicalize directory separators
    if (Sys::OS->is_windows) {
      Re->s((mutable string)$top_dir, ["[/\\\\]", "g"], "/");
    }
    
    # no trailing / unless path is root
    unless (&_is_root($top_dir)) {
      Re->s((mutable string)$top_dir, "/\z", "");
    }
    
    my $warn = $self->{warn};
    my $follow = $self->{follow};
    my $file_seen_h = $self->{file_seen_h};
    
    my $tree_queue = StringList->new;
    
    my $base_name = (string)undef;
    
    my $is_top_dir = 1;
    
    my $name = $top_dir;
    
    $tree_queue->push($name);
    
    my $info = File::Find::Info->new;
    while (1) {
      
      unless ($tree_queue->length > 0) {
        last;
      }
      
      my $name = $tree_queue->shift;
      
      $info->{name} = $name;
      $info->{prune} = 0;
      
      $cb->($info);
      
      if ($info->{prune}) {
        next;
      }
      
      my $dir = $name;
      
      my $lstat = (Sys::IO::Stat)undef;
      eval { $lstat = Sys->lstat($dir); }
      
      if ($@) {
        if ($warn) {
          warn "[Warning]Sys#lstat method failed. File:$dir, Message:\n----------\n$@\n----------";
        }
        next;
      }
      
      my $stat = (Sys::IO::Stat)undef;
      if ($lstat->l) {
        unless ($follow) {
          next;
        }
        
        eval { $stat = Sys->stat($dir); }
        
        if ($@) {
          if ($warn) {
            warn "[Warning]Sys#stat method failed. File:$dir, Message:\n----------\n$@\n----------";
          }
          next;
        }
      }
      else {
        $stat = $lstat;
      }
      
      unless ($stat->d) {
        next;
      }
      
      if ($follow) {
        my $dev = $stat->st_dev;
        my $ino = $stat->st_ino;
        
        my $file_id = "$dev/$ino";
        if ($file_seen_h->exists($file_id)) {
          if ($warn) {
            warn "[Warning]The directory $dir has already been processed. st_dev:$dev, st_ino:$ino";
          }
          next;
        }
        else {
          $file_seen_h->set($file_id, 1);
        }
      }
      
      # Get the list of files in the current directory.
      my $dh = (IO::Dir)undef;
      eval { $dh = IO::Dir->new($dir); }
      if ($@) {
        if ($warn) {
          warn "[Warning]IO::Dir#new method failed. Directory:$dir, Message:\n----------\n$@\n----------";
        }
        next;
      }
      
      while (my $base_name = $dh->read) {
        
        if (Re->m($base_name, $self->{skip_pattern})) {
          next;
        }
        
        my $name = &_is_root($dir) ? "$dir$base_name" : "$dir/$base_name";
        
        $tree_queue->push($name);
      }
    }
  }
  
  private static method _is_root : int ($path : string) {
    if (Sys::OS->is_windows) {
      return !!Re->m($path, "^(?:[A-Za-z]:)?/\z");
    }
    return $path eq "/";
  }
  
}