# Copyright (c) 2023 Yuki Kimoto
# MIT License

class Sys::Time::Util {
  version_from Sys;
  use Sys::Time::Timespec;
  use Sys::Time::Timeval;
  use Math;
  
  static method nanoseconds_to_timespec : Sys::Time::Timespec ($nanoseconds : long) {
    
    my $NANO_PER_SEC = 1_000_000_000L;
    
    my $sec = 0L;
    my $nsec = 0L;
    if ($nanoseconds >= 0) {
      $sec = $nanoseconds / $NANO_PER_SEC;
      $nsec = $nanoseconds % $NANO_PER_SEC;
    }
    else {
      $sec = ($nanoseconds - ($NANO_PER_SEC - 1)) / $NANO_PER_SEC;
      $nsec = $nanoseconds - $sec * $NANO_PER_SEC;
    }
    
    my $ts = Sys::Time::Timespec->new;
    
    $ts->set_tv_sec($sec);
    
    $ts->set_tv_nsec($nsec);
    
    return $ts;
  }
  
  private static method check_timespec_to_nanoseconds : void ($timespec : Sys::Time::Timespec) {
    
    my $max_duration_sec = (Fn->LONG_MAX / 1_000_000_000) - 1;
    
    unless ($timespec->tv_sec > -$max_duration_sec && $timespec->tv_sec < $max_duration_sec) {
      die "Sys::Time::Timespec \$timespec tv_sec must be greater than -$max_duration_sec and less than $max_duration_sec.";
    }
    
  }
  
  static method timespec_to_nanoseconds : long ($ts : Sys::Time::Timespec) {
    
    unless ($ts) {
      die "\$ts must be defined.";
    }
    
    &check_timespec_to_nanoseconds($ts);
    
    my $tv_sec = $ts->tv_sec;
    
    my $tv_nsec = $ts->tv_nsec;
    
    my $nanoseconds = $ts->tv_sec * 1_000_000_000 + $ts->tv_nsec;
    
    return $nanoseconds;
  }
  
  static method microseconds_to_timeval : Sys::Time::Timeval ($microseconds : long) {
    
    unless ($microseconds >= 0) {
      die "\$microseconds must be greater than or equal to 0.";
    }
    
    my $sec = $microseconds / 1_000_000;
    
    my $usec = $microseconds - $sec * 1_000_000;
    
    my $ts = Sys::Time::Timeval->new;
    
    $ts->set_tv_sec($sec);
    
    $ts->set_tv_usec($usec);
    
    return $ts;
  }
  
  static method timeval_to_microseconds : double ($tv : Sys::Time::Timeval) {
    
    unless ($tv) {
      die "\$tv must be defined.";
    }
    
    my $tv_sec = $tv->tv_sec;
    
    unless ($tv_sec >= 0) {
      die "\$tv->tv_sec must be greater than or equal to 0.";
    }
    
    my $tv_usec = $tv ->tv_usec;
    
    unless ($tv_usec >= 0) {
      die "\$tv->tv_usec must be greater than or equal to 0.";
    }
    
    my $microseconds = $tv_sec * 1_000_000 + $tv_usec;
    
    return $microseconds;
  }
  
  static method float_seconds_to_timespec : Sys::Time::Timespec ($float_seconds : double) {
      
    my $NANO_PER_SEC = 1_000_000_000L;
    
    my $sec_floor = (long)Math->floor($float_seconds);
    
    my $nsec = (long)Math->round(($float_seconds - $sec_floor) * $NANO_PER_SEC);
    
    if ($nsec == $NANO_PER_SEC) {
      $sec_floor++;
      $nsec = 0L;
    }
    
    my $ts = Sys::Time::Timespec->new;
    $ts->set_tv_sec($sec_floor);
    $ts->set_tv_nsec((int)$nsec);
    
    return $ts;
  }
  
  static method timespec_to_float_seconds : double ($ts : Sys::Time::Timespec) {
    
    unless ($ts) {
      die "\$ts must be defined.";
    }
    
    my $tv_sec = $ts->tv_sec;
    
    my $tv_nsec = $ts->tv_nsec;
    
    my $float_seconds = $tv_sec + (double)$tv_nsec / 1_000_000_000;
    
    return $float_seconds;
  }
  
  static method float_seconds_to_timeval : Sys::Time::Timeval ($float_seconds : double) {
    
    unless ($float_seconds >= 0) {
      die "\$float_seconds must be greater than or equal to 0.";
    }
    
    my $sec = (long)$float_seconds;
    
    my $usec = (long)(($float_seconds - $sec) * 1_000_000);
    
    my $tv = Sys::Time::Timeval->new;
    
    $tv->set_tv_sec($sec);
    
    $tv->set_tv_usec($usec);
    
    return $tv;
  }
  
  static method timeval_to_float_seconds : double ($tv : Sys::Time::Timeval) {
    
    unless ($tv) {
      die "\$tv must be defined.";
    }
    
    my $tv_sec = $tv->tv_sec;
    
    unless ($tv_sec >= 0) {
      die "\$tv->tv_sec must be greater than or equal to 0.";
    }
    
    my $tv_usec = $tv ->tv_usec;
    
    unless ($tv_usec >= 0) {
      die "\$tv->tv_usec must be greater than or equal to 0.";
    }
    
    my $float_seconds = $tv_sec + (double)$tv_usec / 1_000_000;
    
    return $float_seconds;
  }
  
  static method float_seconds_to_nanoseconds : long ($float_seconds : double) {
    
    unless ($float_seconds >= 0) {
      die "\$float_seconds must be greater than or equal to 0.";
    }
    
    my $nanoseconds = (long)($float_seconds * 1_000_000_000);
    
    return $nanoseconds;
  }
  
  static method nanoseconds_to_float_seconds : double ($nanoseconds : long) {
    
    unless ($nanoseconds >= 0) {
      die "\$nanoseconds must be greater than or equal to 0.";
    }
    
    my $float_seconds = (double)$nanoseconds / 1_000_000_000;
    
    return $float_seconds;
  }
  
  static method float_seconds_to_microseconds : long ($float_seconds : double) {
    
    unless ($float_seconds >= 0) {
      die "\$float_seconds must be greater than or equal to 0.";
    }
    
    my $microseconds = (long)($float_seconds * 1_000_000);
    
    return $microseconds;
  }
  
  static method microseconds_to_float_seconds : double ($microseconds : long) {
    
    unless ($microseconds >= 0) {
      die "\$microseconds must be greater than or equal to 0.";
    }
    
    my $float_seconds = (double)$microseconds / 1_000_000;
    
    return $float_seconds;
  }
  
  static method timeval_interval : double ($tv_a : Sys::Time::Timeval, $tv_b : Sys::Time::Timeval) {
    
    unless ($tv_a) {
      die "\$tv_a must be defined.";
    }
    
    unless ($tv_b) {
      die "\$tv_b must be defined.";
    }
    
    my $diff_tv = &subtract_timeval($tv_b, $tv_a);
    
    my $tv_interval = &timeval_to_float_seconds($diff_tv);
    
    return $tv_interval;
  }
  
  static method timespec_interval : double ($ts_a : Sys::Time::Timespec, $ts_b : Sys::Time::Timespec) {
    
    unless ($ts_a) {
      die "\$ts_a must be defined.";
    }
    
    unless ($ts_b) {
      die "\$ts_b must be defined.";
    }
    
    my $diff_ts = &subtract_timespec($ts_b, $ts_a);
    
    my $ts_interval = &timespec_to_float_seconds($diff_ts);
    
    return $ts_interval;
  }
  
  static method add_timespec : Sys::Time::Timespec ($ts : Sys::Time::Timespec, $diff_ts : Sys::Time::Timespec) {
    
    unless ($ts) {
      die "\$ts must be defined.";
    }
    
    unless ($diff_ts ) {
      die "\$diff_ts  must be defined.";
    }
    
    my $ts_sec = $ts->tv_sec;
    
    my $ts_nsec = $ts->tv_nsec;
    
    my $result_sec = $ts_sec + $diff_ts->tv_sec;
    
    my $result_nsec = $ts_nsec + $diff_ts->tv_nsec;
    
    if ($result_nsec >= 1_000_000_000) {
      $result_sec += 1;
      $result_nsec -= 1_000_000_000;
    }
    elsif ($result_nsec < 0) {
      $result_sec -= 1;
      $result_nsec = 1_000_000_000 + $result_nsec;
    }
    
    my $result = Sys::Time::Timespec->new($result_sec, $result_nsec);
    
    return $result;
  }
  
  static method add_timeval : Sys::Time::Timeval ($tv : Sys::Time::Timeval, $diff_tv : Sys::Time::Timeval) {
    
    unless ($tv) {
      die "\$ts must be defined.";
    }
    
    unless ($diff_tv ) {
      die "\$diff_tv  must be defined.";
    }
    
    my $tv_sec = $tv->tv_sec;
    
    my $tv_usec = $tv->tv_usec;
    
    my $result_sec = $tv_sec + $diff_tv->tv_sec;
    
    my $result_usec = $tv_usec + $diff_tv->tv_usec;
    
    if ($result_usec >= 1_000_000) {
      $result_sec += 1;
      $result_usec -= 1_000_000;
    }
    elsif ($result_usec < 0) {
      $result_sec -= 1;
      $result_usec = 1_000_000 + $result_usec;
    }
    
    my $result = Sys::Time::Timeval->new($result_sec, $result_usec);
    
    return $result;
  }
  
  static method subtract_timespec : Sys::Time::Timespec ($ts : Sys::Time::Timespec, $diff_ts : Sys::Time::Timespec) {
    
    unless ($ts) {
      die "\$ts must be defined.";
    }
    
    unless ($diff_ts ) {
      die "\$diff_ts  must be defined.";
    }
    
    my $ts_sec = $ts->tv_sec;
    
    my $ts_nsec = $ts->tv_nsec;
    
    my $result_sec = $ts_sec - $diff_ts->tv_sec;
    
    my $result_nsec = $ts_nsec - $diff_ts->tv_nsec;
    
    if ($result_nsec >= 1_000_000_000) {
      $result_sec += 1;
      $result_nsec -= 1_000_000_000;
    }
    elsif ($result_nsec < 0) {
      $result_sec -= 1;
      $result_nsec = 1_000_000_000 + $result_nsec;
    }
    
    my $result = Sys::Time::Timespec->new($result_sec, $result_nsec);
    
    return $result;
  }
  
  static method subtract_timeval : Sys::Time::Timeval ($tv : Sys::Time::Timeval, $diff_tv : Sys::Time::Timeval) {
    
    unless ($tv) {
      die "\$ts must be defined.";
    }
    
    unless ($diff_tv ) {
      die "\$diff_tv  must be defined.";
    }
    
    my $tv_sec = $tv->tv_sec;
    
    my $tv_usec = $tv->tv_usec;
    
    my $result_sec = $tv_sec - $diff_tv->tv_sec;
    
    my $result_usec = $tv_usec - $diff_tv->tv_usec;
    
    if ($result_usec >= 1_000_000) {
      $result_sec += 1;
      $result_usec -= 1_000_000;
    }
    elsif ($result_usec < 0) {
      $result_sec -= 1;
      $result_usec = 1_000_000 + $result_usec;
    }
    
    my $result = Sys::Time::Timeval->new($result_sec, $result_usec);
    
    return $result;
  }
  
}