package SPVM::Unicode {
  use SPVM::IntList;
  use SPVM::StringList;
  
  native sub uchar : int ($str : string, $offset_ref : int&);
  
  native sub uchar_to_u8 : int ($uchar : int, $utf8_bytes : byte[]);
  
  precompile sub u16 : short[] ($str : string) {
    my $pos = 0;
    my $uchars_list = SPVM::IntList->new;
    while ((my $uchar = uchar($str, \$pos)) >= 0) {
      $uchars_list->push($uchar);
    }
    
    unless ($pos >= length $str) {
      die "Can't convert part of string to utf16";
    }
    
    my $uchars = $uchars_list->to_array;
    
    my $u16_chars = convert_u32_to_u16($uchars);
    
    return $u16_chars;
  }
  
  precompile sub u8 : string ($u16_chars : short[]) {
    my $uchars = convert_u16_to_u32($u16_chars);
    
    my $ustrs_list = SPVM::StringList->new;
    
    my $utf8_bytes = new byte[4];
    for (my $i = 0; $i < @$uchars; $i++) {
      my $uchar = $uchars->[$i];
      my $len = uchar_to_u8($uchar, $utf8_bytes);
      if ($len == 0) {
        die "Can't convert part of utf16 string to utf8";
      }
      my $ustr = (string)sliceb($utf8_bytes, 0, $len);
      $ustrs_list->push($ustr);
    }
    
    my $ustrs = $ustrs_list->to_array;
    
    my $str = join("", $ustrs);
    
    return $str;
  }
  
  precompile sub convert_u32_to_u16 : short[] ($code_point_string : int[]) {
    my $length = 0;
    
    # Culcurate length
    for (my $i = 0; $i < @$code_point_string; $i++) {
      my $code_point = $code_point_string->[$i];

      if ($code_point < 0 || $code_point > 0x10FFFF) {
        die "Invalid code point in code point string";
      }

      if ($code_point < 0x10000) {
        $length++;
      }
      else {
        $length += 2;
      }
    }
    
    # Convert conde point to UTF-16
    my $utf16_string = new short[$length];
    my $pos = 0;
    
    for (my $i = 0; $i < @$code_point_string; $i++) {
      my $code_point = $code_point_string->[$i];

      if ($code_point < 0x10000) {
        $utf16_string->[$pos] = (short)$code_point;
        $pos++;
      }
      else {
        $utf16_string->[$pos] = (short)(($code_point - 0x10000) / 0x400 + 0xD800);
        $utf16_string->[$pos + 1] = (short)(($code_point - 0x10000) % 0x400 + 0xDC00);
        $pos += 2;
      }
    }
    
    return $utf16_string;
  }

  private sub is_utf16_high_surrogate : int($ch : int) {
    if ($ch >= 0xD800 && $ch < 0xDC00) {
      return 1;
    }
    else {
      return 0;
    }
  }

  private sub is_utf16_low_surrogate : int($ch : int) {
    if ($ch >= 0xDC00 && $ch < 0xE000) {
      return 1;
    }
    else {
      return 0;
    }
  }

  precompile sub convert_u16_to_u32 : int[] ($utf16_string : short[]) {

    my $length = 0;
    
    # Culcurate length
    for (my $i = 0; $i < @$utf16_string; $i++) {
      if (is_utf16_high_surrogate($utf16_string->[$i] & 0xFFFF)) {
        if ($i + 1 == @$utf16_string - 1) {
          die "Invalid UTF-16 string";
        }
        else {
          $i++;
          unless (is_utf16_low_surrogate($utf16_string->[$i] & 0xFFFF)) {
            die "Invalid UTF-16 string";
          }
        }
      }
      elsif (is_utf16_low_surrogate($utf16_string->[$i] & 0xFFFF)) {
        die "Invalid UTF-16 string";
      }
      $length++;
    }
    
    # Convert UTF-16 to code point
    my $code_point_string = new int[$length];
    my $pos = 0;
    for (my $i = 0; $i < @$utf16_string; $i++) {
      if (is_utf16_high_surrogate($utf16_string->[$i] & 0xFFFF)) {
        $code_point_string->[$pos] = 0x10000 + (($utf16_string->[$i] & 0xFFFF) - 0xD800) * 0x400 + (($utf16_string->[$i + 1] & 0xFFFF) - 0xDC00);
        $i++;
      }
      else {
        $code_point_string->[$pos] = $utf16_string->[$i] & 0xFFFF;
      }
      $pos++;
    }
    
    return $code_point_string;
  }
}
