# Copyright (c) 2023 Yuki Kimoto
# MIT License

class File::Spec::Instance::Win32 extends File::Spec::Instance::Unix {
  version_from File::Spec;
  
  use Re;
  use StringList;
  use Array;
  use Cwd;
  
  our $DRIVE_RX_STRING : string;
  our $UNC_RX_STRING : string;
  our $VOL_RX_STRING : string;
  
  INIT {
    $DRIVE_RX_STRING = "[a-zA-Z]:";
    $UNC_RX_STRING = "(?:\\\\\\\\|//)[^\\\\/]+[\\\\/][^\\\\/]+";
    $VOL_RX_STRING = "(?:$DRIVE_RX_STRING|$UNC_RX_STRING)";
  }
  
  # Class Methods
  static method new : File::Spec::Instance::Win32 () {
    
    my $self = new File::Spec::Instance::Win32;
    
    return $self;
  }
  
  # Instance Methods
  method devnull : string () {
    return "nul";
  }
  
  method tmpdir : string () {
    
    my $env_tmpdir = Sys->env("TMPDIR");
    my $env_temp = Sys->env("TEMP");
    my $env_tmp = Sys->env("TMP");
    my $dirlist = [
      $env_tmpdir,
      $env_temp,
      $env_tmp,
      "SYS:/temp",
      "C:\system\temp",
      "C:/temp",
      "/tmp",
      "/",
    ];

    my $tmpdir = $self->_tmpdir($dirlist);
    return $tmpdir;
  }
  
  method file_name_is_absolute : int ($path : string) {
    
    my $is_abs = 0;
    if (my $match = Re->m($path, "^($VOL_RX_STRING)")) {
      my $vol = $match->cap1;
      
      if (Re->m($vol, "^$UNC_RX_STRING")) {
        $is_abs = 2;
      }
      else {
        if (Re->m($path, "^$DRIVE_RX_STRING[\\\\/]")) {
          $is_abs = 2;
        }
      }
    }
    else {
      if (Re->m($path, "^[\\\\/]")) {
        $is_abs = 1;
      }
    }
    
    return $is_abs;
  }
  
  method catfile : string ($parts : string[]) {
    
    unless ($parts) {
      die "The \$parts must be defined";
    }
    
    my $parts_length = @$parts;
    
    unless ($parts_length > 0) {
      die "The parts \$parts must have elements.";
    }
    
    my $file_base_name = $parts->[$parts_length - 1];
    
    my $dir_parts = Array->copy_string_address($parts, 0, $parts_length - 1);
    
    if (@$dir_parts) {
      unless (length $dir_parts->[0]) {
        $dir_parts->[0] = "/";
      }
      
      if (Re->m($dir_parts->[0], "^$DRIVE_RX_STRING\z")) {
        $dir_parts->[0] .= "\\";
      }
    }
    
    my $new_parts_list = StringList->new($dir_parts);
    $new_parts_list->push($file_base_name);
    my $new_parts = $new_parts_list->to_array;
    
    my $file = $self->_canon_cat($new_parts);
    
    return $file;
  }
  
  method canonpath : string ($path : string) {
    
    unless ($path) {
      die "The \$path must be defined";
    }
    
    unless (length $path) {
      return $path;
    }
    
    my $canonpath = $self->_canon_cat([$path]);
    
    return $canonpath;
  }
  
  method splitpath : string[] ($path : string, $nofile : int = 0) {
    
    my $volume = "";
    my $dir = "";
    my $file = "";
    
    if (my $match = Re->m($path, ["^($VOL_RX_STRING?)(.*)", "s"])) {
      $volume = $match->cap1;
      my $rest = $match->cap2;
      
      if ($nofile) {
        $dir = $rest;
      }
      else {
        if (my $match = Re->m($rest, ["^((?:.*[\\\\/](?:\.\.?\z)?)?)([^\\\\/]*)", "s"])) {
          $dir = $match->cap1;
          $file      = $match->cap2;
        }
      }
    }
    
    return [$volume, $dir, $file];
  }
  
  method splitdir : string[] ($path : string) {
    my $parts = Re->split("[\\\\/]", $path, -1);
    
    return $parts;
  }
  
  method catpath : string ($volume : string, $dir : string, $file : string) {
    unless ($volume) {
      $volume = "";
    }
    unless ($dir) {
      $dir = "";
    }
    unless ($file) {
      $file = "";
    }
    
    my $path = $volume;
    
    # If it's UNC, make sure the glue separator is there, reusing
    # whatever separator is first in the $volume
    my $v = (string)undef;
    if (my $match = Re->m($path, ["^([\\\\/])[\\\\/][^\\\\/]+[\\\\/][^\\\\/]+\z", "s"])) {
      my $v = $match->cap1;
      if (Re->m($dir, "^[^\\\\/]")) {
        $path .= $v;
      }
    }
    
    $path .= $dir;
    
    # If the volume is not just A:, make sure the glue separator is 
    # there, reusing whatever separator is first in the $volume if possible.
    if (!Re->m($path, ["^[a-zA-Z]:\z", "s"]) && Re->m($path, "[^\\\\/]\z")
        && Re->m($file, "[^\\\\/]"))
    {
      my $sep = "\\";
      my $match = Re->m($path, "([\\\\/])");
      if ($match) {
        $sep = $match->cap1;
      }
      
      $path .= $sep;
    }
    
    $path .= $file;
    
    return $path;
  }
  
  method rootdir : string () {
    return "\\";
  }
  
  method path : string[] () {
    
    my $env_path = Sys->env("PATH");
    unless ($env_path) {
      return new string[0];
    }
    
    my $pathes = Fn->split(";", $env_path);
    
    my $pathes_list = StringList->new;
    for (my $i = 0; $i < @$pathes; $i++) {
      my $path = $pathes->[$i];
      Re->s((mutable string)$path, "\"", "");
      if (length $path) {
        $pathes_list->push($path);
      }
    }
    $pathes_list->unshift(".");
    $pathes = $pathes_list->to_array;
    
    return $pathes;
  }
  
  method catdir : string ($parts : string[]) {
    
    unless ($parts) {
      die "The \$parts must be defined";
    }
    
    unless (@$parts) {
      return "";
    }
    
    unless (length $parts->[0]) {
      $parts->[0] = "/";
    }
    
    if (Re->m($parts->[0], "^$DRIVE_RX_STRING\z")) {
      $parts->[0] .= "\\";
    }
    
    my $dir = $self->_canon_cat($parts);
    
    return $dir;
  }
  
  method rel2abs : string ($path : string, $base : string = undef) {
    
    my $is_abs = $self->file_name_is_absolute($path);
    
    my $abs_path = (string)undef;
    
    # Check for volume (should probably document the '2' thing...)
    if ($is_abs == 2) {
      $abs_path = $self->canonpath($path) ;
    }
    elsif ($is_abs) {
      # It's missing a volume, add one
      my $vol_parts = $self->splitpath(Cwd->getcwd);
      my $vol = $vol_parts->[0];
      
      $abs_path = $self->canonpath($vol . $path);
    }
    else {
      if (!$base || $base eq "") {
        my $drive = $self->splitpath($path)->[0];
        if (length $drive) {
          eval { $base = Cwd->getdcwd($drive); };
          if ($@) {
            unless (eval_error_id == basic_type_id Error::System) {
              die $@;
            }
          }
        }
        unless ($base) {
          $base = Cwd->getcwd;
        }
      }
      elsif (!$self->file_name_is_absolute($base)) {
        $base = $self->rel2abs($base) ;
      }
      else {
        $base = $self->canonpath($base) ;
      }
      
      my $path_parts = $self->splitpath($path, 1);
      my $path_dir = $path_parts->[1];
      my $path_base_file = $path_parts->[2];
      
      my $base_parts = $self->splitpath($base, 1);
      my $base_volume = $base_parts->[0];
      my $base_dir = $base_parts->[1];
      
      $path = $self->catpath( 
       $base_volume, 
       $self->catdir([$base_dir, $path_dir]), 
       $path_base_file
      );
      
      $abs_path = $self->canonpath($path);
    }
    
    return $abs_path;
  }
  
  private method _canon_cat : string ($parts : string[]) {
    my $first = copy $parts->[0];
    
    my $volume = (string)undef;
    
    # drive letter - (C:)
    my $match = Re->m($first, "\A([A-Za-z]:)([\\\\/]?)");
    Re->s((mutable string)$first, "\A([A-Za-z]:)([\\\\/]?)", "");
    if ($match) {
      $volume = Fn->ucfirst($match->cap1);
      if (length $match->cap2) {
        $volume .= "\\";
      }
    }
    else {
      my $replace_info = Re->s((mutable string)$first, ["\A(?:\\\\\\\\|//)([^\\\\/]+)(?:[\\\\/]([^\\\\/]+))?[\\\\/]?", "s"], "");
      
      # UNC volume (\\192.168.201.101)
      if ($replace_info) {
        my $match = $replace_info->match;
        $volume = "\\\\" . $match->cap1;
        if ($match->cap2) {
          $volume .= "\\" . $match->cap2;
        }
        $volume .= "\\";
      }
      else {
        my $replace_info = Re->s((mutable string)$first, "\A[\\\\/]", "");
        
        # root dir (\foo)
        if ($replace_info) {
          $volume = "\\";
        }
        else {
          $volume = "";
        }
      }
    }
    
    $parts->[0] = $first;
    
    my $path = Fn->join("\\", $parts);
    
    # /+ to \
    Re->s((mutable string)$path, ["/+", "g"], "\\");
    
    # \+ to \
    Re->s((mutable string)$path, ["\\\\+", "g"], "\\");
    
    # xx\.\.\yy --> xx\yy
    Re->s((mutable string)$path, ["(?:(?:\A|\\\\)\.(?:\\\\\.)*(?:\\\\|\z))+", "g"], "\\");
    
    # xx\yy\..\zz --> xx\zz
    my $parts_list = StringList->new;
    $parts = Fn->split("\\", $path);
    for (my $i = 0; $i < @$parts; $i++) {
      my $part = $parts->[$i];
      if ($part eq "..") {
        if ($parts_list->length > 0) {
          my $before_part = $parts_list->get($parts_list->length - 1);
          unless ($before_part eq "..") {
            $parts_list->pop;
            next;
          }
        }
      }
      $parts_list->push($part);
    }
    $parts = $parts_list->to_array;
    $path = Fn->join("\\", $parts);
    
    # \xx --> xx
    
    Re->s((mutable string)$path, ["\A\\\\", "g"], "");
    
    # xx\ --> xx
    Re->s((mutable string)$path, ["\\\\\z", "g"], "");
    
    if (Re->m($volume, "\\\\\z")) {
      # <vol>\.. --> <vol>\
      Re->s((mutable string)$path, "\A\.\.(?:\\\\\.\.)*(?:\\\\|\z)", "");
      
      # \\HOST\SHARE\ --> \\HOST\SHARE
      my $match = Re->m($volume, ["\A(\\\\\\\\.*)\\\\\z", "s"]);
      if (!length $path && $match) {
        $path = $match->cap1;
        return $path;
      }
    }
    
    if (length $path || length $volume) {
      $path = $volume . $path;
    }
    else {
      $path = ".";
    }
    
    return $path;
  }
  
}