# Copyright (c) 2023 Yuki Kimoto
# MIT License

class Format {
  version_from SPVM;

  use StringBuffer;
  use Fn;
  
  precompile static method sprintf : string ($format : string, $args : object[]) {
    my $format_length = length $format;
    my $index = 0;
    
    my $buffer = StringBuffer->new;
    my $arg_count = 0;
    my $constant_string_length = 0;
    
    while ($index + $constant_string_length < $format_length) {
      if ($format->[$index + $constant_string_length] != '%') {
        # Read constant string
        ++$constant_string_length;
      }
      elsif ($index + $constant_string_length + 1 < $format_length &&
          $format->[$index + $constant_string_length + 1] == '%') {
        # Read %%
        ++$constant_string_length;
        
        # Add constant string
        if ($constant_string_length > 0) {
          my $format_part = Fn->substr($format, $index, $constant_string_length);
          
          $buffer->push($format_part);
          $index += $constant_string_length;
          $constant_string_length = 0;
        }
        
        # Skip second %
        ++$index;
      }
      elsif ($index + $constant_string_length + 1 >= $format_length) {
        die "Invalid conversion in sprintf: end of string.";
      }
      else {
        # Add constant string
        if ($constant_string_length > 0) {
          my $format_part = Fn->substr($format, $index, $constant_string_length);
          $buffer->push($format_part);
          $index += $constant_string_length;
          $constant_string_length = 0;
        }
        
        # Check the next element of @$args corresponding to the specifier
        unless ($arg_count < @$args) {
          die "Missing argument in sprintf.";
        }
        
        # Read specifier %[flags][width][.precision][length]type
        my $specifier_base_index = $index;
        ++$index; # '%'
        
        # Read `flags`
        my $pad_char = ' ';
        my $plus_sign = 0;
        my $left_justified = 0;
        
        while ($index < $format_length) {
          my $flag = (int)($format->[$index]);
          switch($flag) {
            case '0': {
              ++$index;
              $pad_char = '0';
              break;
            }
            case '+': {
              ++$index;
              $plus_sign = 1;
              break;
            }
            case '-': {
              ++$index;
              $left_justified = 1;
              break;
            }
            default: {
              last;
              break;
            }
          }
        }
        
        if ($left_justified) {
          $pad_char = ' ';
        }
        
        # Width
        my $width = 0;
        while ($index < $format_length) {
          my $c = $format->[$index];
          if ($c < '0' || '9' < $c) {
            last;
          }
          $width = $width * 10 + $c - '0';
          ++$index;
        }
        
        # Precision
        my $precision = 0;
        my $has_precision = 0;
        if ($index < $format_length && $format->[$index] == '.') {
          ++$index;
          while ($index < $format_length) {
            my $c = $format->[$index];
            if ($c < '0' || '9' < $c) {
              last;
            }
            $has_precision = 1;
            $precision = $precision * 10 + $c - '0';
            ++$index;
          }
        }
        unless ($has_precision) {
          $precision = -1;
        }
        
        unless ($index < $format_length) {
          die "Invalid conversion in sprintf: '"
              . Fn->substr($format, $specifier_base_index, $index - $specifier_base_index) . "'";
        }
        
        my $specifier_first_char = $format->[$index];
        $index++;
        
        my $specifier = (string)"";
        switch ((int) $specifier_first_char) {
          case 'X' : {
            $specifier = "X";
            break;
          }
          case 'c' : {
            $specifier = "c";
            break;
          }
          case 'd' : {
            $specifier = "d";
            break;
          }
          case 'f' : {
            $specifier = "f";
            break;
          }
          case 'g' : {
            $specifier = "g";
            break;
          }
          case 'l' : {
            if ($index < length $format) {
              if ($format->[$index] == 'X') {
                $index++;
                $specifier = "lX";
              }
              elsif ($format->[$index] == 'd') {
                $index++;
                $specifier = "ld";
              }
              elsif ($format->[$index] == 'u') {
                $index++;
                $specifier = "lu";
              }
              elsif ($format->[$index] == 'x') {
                $index++;
                $specifier = "lx";
              }
            }
            break;
          }
          case 'p' : {
            $specifier = "p";
            break;
          }
          case 's' : {
            $specifier = "s";
            break;
          }
          case 'u' : {
            $specifier = "u";
            break;
          }
          case 'x' : {
            $specifier = "x";
            break;
          }
        }
        
        if ($specifier eq "X") {
          my $arg = (Int)$args->[$arg_count];
          my $formatted_value = &_native_snprintf_x($arg);
          
          $formatted_value = Fn->uc($formatted_value);
          
          &_push_formatted_string_unsigned($buffer, $formatted_value, $width, $pad_char, $left_justified);
        }
        elsif ($specifier eq "c") {
          my $arg_value : int;
          my $arg = $args->[$arg_count];
          if ($arg isa Byte) {
            $arg_value = $arg->(Byte)->value;
          }
          else {
            $arg_value = $arg->(Int)->value;
          }
          my $formatted_value = Fn->chr($arg_value);
          if (!$formatted_value) {
            $formatted_value = "?";
          }
          &_push_formatted_string_unsigned($buffer, $formatted_value, $width, $pad_char, $left_justified);
        }
        elsif ($specifier eq "d") {
          my $arg = (Int)$args->[$arg_count];
          my $formatted_value = &_native_snprintf_d($arg);
          &_push_formatted_string_signed($buffer, $formatted_value, $width, $pad_char, $left_justified, $plus_sign);
        }
        elsif ($specifier eq "f") {
          my $arg_value : double;
          my $arg = $args->[$arg_count];
          if ($arg isa Float) {
            $arg_value = $arg->(Float)->value;
          }
          else {
            $arg_value = $arg->(Double)->value;
          }
          my $formatted_value = &_native_snprintf_f($arg_value, $precision);
          &_push_formatted_string_signed($buffer, $formatted_value, $width, $pad_char, $left_justified, $plus_sign);
        }
        elsif ($specifier eq "g") {
          my $arg_value : double;
          my $arg = $args->[$arg_count];
          if ($arg isa Float) {
            $arg_value = $arg->(Float)->value;
          }
          else {
            $arg_value = $arg->(Double)->value;
          }
          my $formatted_value = &_native_snprintf_g($arg_value, $precision);
          &_push_formatted_string_signed($buffer, $formatted_value, $width, $pad_char, $left_justified, $plus_sign);
        }
        elsif ($specifier eq "lX") {
          my $arg = (Long)$args->[$arg_count];
          my $formatted_value = &_native_snprintf_lx($arg);
          
          $formatted_value = Fn->uc($formatted_value);
          
          &_push_formatted_string_unsigned($buffer, $formatted_value, $width, $pad_char, $left_justified);
        }
        elsif ($specifier eq "ld") {
          my $arg = (Long)$args->[$arg_count];
          my $formatted_value = &_native_snprintf_ld($arg);
          &_push_formatted_string_signed($buffer, $formatted_value, $width, $pad_char, $left_justified, $plus_sign);
        }
        elsif ($specifier eq "lu") {
          my $arg = (Long)$args->[$arg_count];
          my $formatted_value = &_native_snprintf_lu($arg);
          &_push_formatted_string_unsigned($buffer, $formatted_value, $width, $pad_char, $left_justified);
        }
        elsif ($specifier eq "lx") {
          my $arg = (Long)$args->[$arg_count];
          my $formatted_value = &_native_snprintf_lx($arg);
          
          &_push_formatted_string_unsigned($buffer, $formatted_value, $width, $pad_char, $left_justified);
        }
        elsif ($specifier eq "p") {
          my $arg = $args->[$arg_count];
          my $formatted_value = &_native_snprintf_p($arg);
          &_push_formatted_string_signed($buffer, $formatted_value, $width, $pad_char, $left_justified, $plus_sign);
        }
        elsif ($specifier eq "s") {
          my $arg = (string)$args->[$arg_count];
          if ($has_precision) {
            my $arg_length = length $arg;
            if ($precision < $arg_length) {
              $arg = Fn->substr($arg, 0, $precision);
              $width = $precision;
            }
          }
          my $formatted_value = $arg;
          &_push_formatted_string_unsigned($buffer, $formatted_value, $width, $pad_char, $left_justified);
        }
        elsif ($specifier eq "u") {
          my $arg = (Int)$args->[$arg_count];
          my $formatted_value = &_native_snprintf_u($arg);
          &_push_formatted_string_unsigned($buffer, $formatted_value, $width, $pad_char, $left_justified);
        }
        elsif ($specifier eq "x") {
          my $arg = (Int)$args->[$arg_count];
          my $formatted_value = &_native_snprintf_x($arg);
          
          &_push_formatted_string_unsigned($buffer, $formatted_value, $width, $pad_char, $left_justified);
        }
        else {
          die "Invalid specifier '%$specifier'.";
        }
        
        ++$arg_count;
      }
    }
    
    # Add constant string
    if ($constant_string_length > 0) {
      my $format_part = Fn->substr($format, $index, $constant_string_length);
      $buffer->push($format_part);
      $index += $constant_string_length;
      $constant_string_length = 0;
    }
    
    my $result = $buffer->to_string;
    return $result;
  }
  
  native static method _native_snprintf_d : string ($value : int);
  native static method _native_snprintf_ld : string ($value : long);
  native static method _native_snprintf_lu : string ($value : long);
  native static method _native_snprintf_u : string ($value : int);
  native static method _native_snprintf_x : string ($value : int);
  native static method _native_snprintf_lx : string ($value : long);
  native static method _native_snprintf_f : string ($value : double, $precision : int);
  native static method _native_snprintf_g : string ($value : double, $precision : int);
  native static method _native_snprintf_p : string ($value : object);
  
  static method _push_formatted_string_unsigned : void($buffer : StringBuffer, $formatted_value : string, $width : int, $pad_char : byte, $left_justified : int) {
    
    # "+" sign is always ignored.
    my $plus_sign = 0;
    
    &_push_formatted_string_signed($buffer, $formatted_value, $width, $pad_char, $left_justified, $plus_sign);
  }

  precompile static method _push_formatted_string_signed : void($buffer : StringBuffer, $formatted_value : string, $width : int, $pad_char : byte, $left_justified : int, $plus_sign : int) {
    
    unless ($formatted_value) {
      $formatted_value = "";
    }
    
    my $is_minus = 0;
    if (length $formatted_value > 0 && $formatted_value->[0] == '-') {
       $is_minus = 1;
    }
    
    my $space_count = $width - length $formatted_value;
    if (!$is_minus && $plus_sign) {
      --$space_count;
    }
    
    if ($left_justified) {
      if (!$is_minus && $plus_sign) {
        $buffer->push_char('+');
      }
      
      $buffer->push($formatted_value);
      
      if ($space_count > 0) {
        for (; $space_count > 0; --$space_count) {
          $buffer->push_char($pad_char);
        }
      }
    }
    else {
      if ($space_count > 0) {
        for (; $space_count > 0; --$space_count) {
          $buffer->push_char($pad_char);
        }
      }
      
      if (!$is_minus && $plus_sign) {
        $buffer->push_char('+');
      }
      
      $buffer->push($formatted_value);
    }
  }
}
