package # T - Time
	T;  # Hide from PAUSE

# Package to work with dates easily


use strict;
use warnings;

use DateTime;
use DateTime::HiRes;
use DateTime::TimeZone;
use DateTime::Format::Pg;



# Cache local timezone:
# https://metacpan.org/pod/DateTime#Determining-the-Local-Time-Zone-Can-Be-Slow
my $tz =  DateTime::TimeZone->new( name => 'local' );
# set defaults
sub _sd   { shift
	->set_time_zone( $tz )
	->set_formatter( 'DateTime::Format::Pg' )
}

# Please notice subtle difference:
#   DateTime->new creates object in 'float' time zone
#   DateTime->now creates object in 'UTC' time zone
# https://metacpan.org/pod/DateTime#Making-Things-Simple



# new ()    -> now
# new false -> undef
# new ref   -> ref    (Do nothing if already DateTime)
# new unix  -> DateTime
# new str   -> DateTime
sub new {
	@_            or return now(); # Return now()  if nothing was provided
	$_ =  shift   or return undef; # Return undef  if we get false value

	ref $_        &&  return $_;
	m/^[\d.]+$/   &&  return _sd( DateTime->from_epoch( epoch =>  $_ ) ); # Test float 6 and 9

	return eval{ _sd( DateTime::Format::Pg->parse_datetime( $_ ) ) }; #Test: What is parsed string was with TZ?
}



sub now    { _sd( DateTime->now       ( time_zone =>  $tz ) ) }
sub hnow   { _sd( DateTime::HiRes->now( time_zone =>  $tz ) ) }
sub today  { _sd( DateTime->today     ( time_zone =>  $tz ) ) }
sub tmrw   { tday(new @_  or return undef)->add( days => 1 ) }

sub tday   { (new @_  or return undef)->clone->truncate( to =>  'day'   ) }
sub tmonth { (new @_  or return undef)->clone->truncate( to =>  'month' ) }
sub tyear  { (new @_  or return undef)->clone->truncate( to =>  'year'  ) }

sub next;
sub nhour  { T::next +(new @_?shift:()  or return undef), hours  =>  shift }
sub nday   { T::next +(new @_?shift:()  or return undef), days   =>  shift }
sub nmonth { T::next +(new @_?shift:()  or return undef), months =>  shift }
sub nyear  { T::next +(new @_?shift:()  or return undef), years  =>  shift }

sub next   {
	my $date =  new @_? shift : ()   or return undef;

	$date->clone->add( end_of_month => 'limit', shift() =>  shift //1 );
}



# use DateTime::Format::Epoch::Unix;
# sub unix { DateTime::Format::Epoch::Unix->format_datetime( shift )       }

sub fmt  { (@_==2? new $_[1] : new   or return undef)->strftime( $_[0] ) }
sub fymd { my $d =  $_[1] //'-'; fmt "%Y$d%m$d%d", new @_? shift : () }
sub fmdy { my $d =  $_[1] //'/'; fmt "%m$d%d$d%Y", new @_? shift : () }
sub fdmy { my $d =  $_[1] //'.'; fmt "%d$d%m$d%Y", new @_? shift : () }

sub fd   { fmt '%Y-%m-%d',  new @_ }
sub ft   { fmt '%H:%M:%S',  new @_ }
sub ftm  { fmt '%T.%6N',    (@_? new @_ : hnow) } # TODO: Test !.000000
sub fsm  { fmt '%S.%6N',    (@_? new @_ : hnow) }
sub fdtm { fmt '%F %T.%6N', (@_? new @_ : hnow) }



sub first_day {
  my $date =  tmonth @_? shift : ()   or return undef;

  my $next =  shift;
  return $next? $date->add( months => $next ) : $date;
}



# Check for monotonic clock support
use constant MONOTONIC => !!eval { Time::HiRes::clock_gettime(Time::HiRes::CLOCK_MONOTONIC()) };

my $time =  MONOTONIC?
  sub () { Time::HiRes::clock_gettime(Time::HiRes::CLOCK_MONOTONIC()) }:
  \&Time::HiRes::time;

sub steady();
{
  no strict 'refs';
  no warnings 'redefine';
  *T::steady =  $time;
}



sub diff_days { shift->delta_days( shift )->{ days } }


my $start;
sub start  { $start =  T::steady }
sub diff_start {
  DateTime->from_epoch(
    epoch =>  ((shift //T::steady) -$start)
  )
}

$start =  T::steady;

1;

=encoding utf8

=head1 NAME

C<T> - Stands for Time

This module mostly a handy wrapper around DateTime module.


=head1 SYNOPSIS

  use T;

  my $dt  =  T::new '2024-01-02 03:04:05';           # 2024-01-02 03:04:05
  my $now =  T::now;
  my $fmt =  T::fdmy T::new '2024-01-02 03:04:05'    # 02.01.2024
  my $day =  T::tday $now;
  my $ymd =  T::fymd $now, '@';                      # 2024@01@02

  my $secs =  T::steady;                             # 429.376863203


=head1 DESCRIPTION

C<T> is a small helper module around L<DateTime>. It normalizes dates into the
local time zone and applies a Postgres-friendly formatter.

Please note, all C<DateTime> objects are in C<'local'> timezone. It is differ in
compare to DateTime where C<new()> and C<now()> functions return objects with
C<'float'> and C<UTC> timezones correspondigly. See L<DateTime> for details.

All functions could be called without parameters, in this case they fallback to
T::new.


=head1 FUNCTIONS

=head1 CONSTRUCTORS

=head2 new

  my $dt =  T::new;                          # Defaults to T::now.
  my $dt =  T::new $dt;                      # Does nothing. Return as is.
  my $dt =  T::new 1737936005;               # DateTime from epoch.
  my $dt =  T::new 1737936005.123457;        # DateTime from epoch with microseconds.
  my $dt =  T::new '2024-01-02 03:04:05';    # Parse string.
  my $dt =  T::new $value;

Parse C<$value> into a L<DateTime> object. All objects assigned L<DateTime::Format::Pg>
formatter and C<local> timezone. Parsing is done via L<DateTime::Format::Pg>.


=over 2

=item *

No arguments: return L</now>.

=item *

False value: return undef.

=item *

DateTime object: return it unchanged. (Does it worth to force formatter and TZ?)

=item *

Integer: treat as unix epoch seconds.

=item *

String: parse using L<DateTime::Format::Pg>.

=back


=head2 now

  my $dt =  T::now;           # 2026-01-28 10:13:56-0500

Current time. Synonym for C<DateTime::now> but additionally assigned C<local>
timezone.


=head2 hnow

  my $dt =  T::hnow;

High-resolution current time. See L<DateTime::HiRes> for details.


=head2 today

  my $dt =  T::today;         # 2027-01-21 00:00:00

Current date (start of the day). Alias to C<DateTime-E<gt>today>.


=head2 tmrw

  my $dt =  T::tmrw           # 2027-01-22 00:00:00
  my $dt =  T::tmrw $value;   # 2024-08-17 15:33:00 -> 2024-08-18 00:00

Return "tomorrow" (C<$value> truncated to day, plus one day).


=head1 TRUNCATE

=head2 tday

  my $dt =  T::tday $value;       # 2025-03-07 05:34 -> 2025-03-07 00:00

Return C<$value> truncated to the beginning of the day.


=head2 tmonth

  my $dt =  T::tmonth $value;     # 2025-03-07 -> 2025-03-01

Return C<$value> truncated to the beginning of the month.


=head2 tyear

  my $dt =  T::tyear $value;      # 2025-03-07 -> 2025-01-01

Return C<$value> truncated to the beginning of the year.


=head1 NEXT / DATE MANIPULATION

=head2 nhour

  my $dt =  T::nhour $value, $hours;

Add C<$hours> hours using L</next> semantics. C<$hours> can be negative.


=head2 nday

  my $dt =  T::nday $value, $days;

Add C<$days> days using L</next> semantics. C<$days> can be negative.


=head2 nmonth

  my $dt =  T::nmonth $value, $months;

Add C<$months> months using L</next> semantics. C<$months> can be negative.


=head2 nyear

  my $dt =  T::nyear $value, $years;

Add C<$years> years using L</next> semantics. C<$years> can be negative.


=head2 next

  my $dt =  T::next $value, days   =>  1;
  my $dt =  T::next $value, months =>  1;
  my $dt =  T::next $value, years  => -2;

Clone C<$value> and add time using C<DateTime-E<gt>add>. It uses
C<end_of_month =E<gt> 'limit'> to handle shorter months.

See C<DateTime-E<gt>add> for details.


=head1 FORMATTING

=head2 fmt

  my $str =  T::fmt $format;
  my $str =  T::fmt $format, $value;
  my $str =  T::fmt '%I:%M%P %j %U %a', '2025-12-31 23:00:00';   # 11:00pm 365 52 Wed

Format a date using C<strftime>. C<$value> is passed through L</new>. See
L<DateTime/strftime-Patterns> for details.

TODO? Should it be just C<T::f>?

=head2 fymd

  my $str =  T::fymd;
  my $str =  T::fymd $value;
  my $str =  T::fymd $value, $sep;

Format as C<YYYYE<lt>sepE<gt>MME<lt>sepE<gt>DD>.


=head2 fmdy

  my $str =  T::fmdy;
  my $str =  T::fmdy $value;
  my $str =  T::fmdy $value, $sep;

Format as C<MME<lt>sepE<gt>DDE<lt>sepE<gt>YYYY>.


=head2 fdmy

  my $str =  T::fdmy;
  my $str =  T::fdmy $value;
  my $str =  T::fdmy $value, $sep;

Format as C<DDE<lt>sepE<gt>MME<lt>sepE<gt>YYYY>.


=head2 fd

  my $str =  T::fd;
  my $str =  T::fd $value;

Format as C<YYYY-MM-DD>. C<fd> stands for 'format date'.


=head2 ft

  my $str =  T::ft;
  my $str =  T::ft $value;

Format as C<HH:MM:SS>. C<ft> stands for 'format time'.


=head2 ftm

  my $str =  T::ftm;
  my $str =  T::ftm $value;

Format as C<HH:MM:SS.NNNNNN>. C<ftm> stands for 'format time micro'.


=head2 fsm

  my $str =  T::fsm;
  my $str =  T::fsm $value;

Format as C<SS.NNNNNN>. C<fsm> stands for 'format seconds with microseconds'.


=head2 fdtm

  my $str =  T::fdtm;
  my $str =  T::fdtm $value;

Format as C<YYYY-MM-DD HH:MM:SS.NNNNNN>. C<fdtm> stands for 'format date time
with microseconds'.


=head1 UTILITY

=head2 first_day

  my $dt =  T::first_day $value;                  # 2027-05-15     -> 2027-05-01
  my $dt =  T::first_day $value, $next_month;     # 2027-05-15,  3 -> 2027-08-01
  my $dt =  T::first_day '2027-05-15', -1;        # 2027-05-15, -1 -> 2027-04-01

Return the first day of the month for C<$value>. If C<$next_month> is
provided, move forward by that many months. Value could be negative.


=head2 steady

  my $seconds =  T::steady;

Return a monotonic timestamp in seconds when available, otherwise
L<Time::HiRes/time>.


=head2 start

  my $seconds =  T::start;

Reset the internal stopwatch start time used by L</diff_start>.


=head2 diff_start

  my $elapsed =  T::diff_start;

Return the time elapsed since the last L</start> call.


=head1 SEE ALSO

L<DateTime>, L<DateTime::Format::Pg>, L<A>, L<C>, L<I>, L<L>, L<M>, L<S>, L<T>, L<U>.


=cut
