package RISCOS::Time;

use RISCOS::SWI;
use Carp;
#use SelfLoader;
require Exporter;
use strict;
use vars qw (@ISA @EXPORT_OK $VERSION $time2utc $time2local $time2_mask
	     $s_1900to1970 $seconds_per_day $cs_1900to1970_256 $cs_per_day_256 
	     $start $standard_date_and_time $standard_date_and_time_mask);

@ISA = qw(Exporter);
@EXPORT_OK = qw(time_age2cs time_age2riscos time_age2unix
		time_cs2age time_cs2riscos time_cs2unix
		time_riscos2age time_riscos2cs time_riscos2unix
		time_unix2age time_unix2cs time_unix2riscos
		time_guess2riscos $s_1900to1970 $seconds_per_day
		time2utc time2local time2_raw standard_date_and_time);
$VERSION = 0.04;

$time2utc = SWINumberFromString("XTerritory_ConvertTimeToUTCOrdinals");
$time2local = SWINumberFromString("XTerritory_ConvertTimeToOrdinals");
$time2_mask = &regmask([0..2]);
$standard_date_and_time = SWINumberFromString("XOS_ConvertStandardDateAndTime");
$standard_date_and_time_mask = &regmask([0..2]);

# Make constants
# Seconds between 1st January 1900 and 1st January 1970
*s_1900to1970		= \2208988800;	# 17 leap years, 53 normal
*cs_1900to1970_256	= \862886250;	# cs ÷ 256
*seconds_per_day	= \86400;
*cs_per_day_256		= \33750;
# Start time of script in cs/256 since 1st January 1900.
*start			= \($^T / 2.56 + $cs_1900to1970_256);

#$time2utc && $time2local && $standard_date_and_time;
#__DATA__

sub time_age2cs {
    return ($^T - $_[0] * $seconds_per_day + $s_1900to1970) * 100
      unless wantarray;
    map {  ($^T - $_	* $seconds_per_day + $s_1900to1970) * 100 } @_
}

sub time_age2riscos {
    my $temp;	# Remember subroutines are pass by reference, so assigning to
		# $_[0] clobbers the caller's array.
    unless (wantarray) {
	$temp = $start - $_[0] * $cs_per_day_256;
	return pack 'CV', ($temp - int $temp) * 256, $temp;
    }
    map {
	$temp = $start - $_ * $cs_per_day_256;
	pack 'CV', ($temp - int $temp) * 256, $temp;
    } @_
}

sub time_age2unix {
    return pack 'I', ($^T - $_[0] * $seconds_per_day) unless wantarray;
    map {  pack 'I', ($^T - $_	  * $seconds_per_day) } @_
}

sub time_cs2age {
    return ($^T + $s_1900to1970 - $_[0] / 100) / $seconds_per_day
      unless wantarray;
    map {  ($^T + $s_1900to1970 - $_	/ 100) / $seconds_per_day } @_
}

sub time_cs2riscos {
    # Inspection of pp.c reveals that % (ie pp_modulo) doesn't do doubles. :-(
    my $temp;
    unless (wantarray) {
	$temp = int $_[0] / 256;
	return pack 'CV', $_[0] - $temp * 256, $temp;
    }
    map {
	$temp = int $_ / 256;
	pack 'CV', $_ - $temp * 256, $temp;
    } @_
}

sub time_cs2unix {
    return pack 'I', ($_[0] / 100 - $s_1900to1970) unless wantarray;
    map {  pack 'I', ($_    / 100 - $s_1900to1970) } @_
}

sub time_riscos2age {
    # ord equivalent to unpack ('C'). Hence whole is unpack 'CV'
    return ($start - ord ($_[0]) / 256 - unpack ('xV', $_[0])) / $cs_per_day_256	unless wantarray;
    map {  ($start - ord ($_) / 256 - unpack ('xV', $_)) / $cs_per_day_256 } @_
}

sub time_riscos2cs {
    return ord ($_[0]) + 256 * unpack ('xV', $_[0]) unless wantarray;
    map { ord ($_) + 256 * unpack ('xV', $_)} @_
}

sub time_riscos2unix {
    # Very odd conversion to want to make
    time_cs2unix (&time_riscos2cs)
}

sub time_unix2age {
    return ($^T - unpack 'I', $_[0]) / $seconds_per_day unless wantarray;
    map {  ($^T - unpack 'I', $_   ) / $seconds_per_day } @_
}

sub time_unix2cs {
    return 100 * ($s_1900to1970 + unpack 'I', $_[0]) unless wantarray;
    map {  100 * ($s_1900to1970 + unpack 'I', $_[0]) } @_
}

sub time_unix2riscos {
    # Less odd conversion to want to make
    time_cs2riscos (&time_unix2cs)
}

sub _time_guess2riscos ($) {
    return undef unless defined $_[0];
    my $length = length $_[0];

    # String is an "age" if all the characters are numeric.
    # count the non-numeric. Can't use tr on read only values. :-(
    # OK. if we can match 1 non-numeric.
    return &time_age2riscos unless ($_[0] =~ m/[^-\d.]/);
    return $_[0] if ($length == 5);
    return &time_unix2riscos if ($length == 4);
    if ($length == 6 and $_[0] =~ /\0$/) {
	# Sanity check that last character is "\0"
	my $time = $_[0];
	chop ($time);
	return $time;
    }
    undef;
}

sub time_guess2riscos {
    return _time_guess2riscos ($_[0]) unless wantarray;
    map {  _time_guess2riscos $_ } @_
}

sub standard_date_and_time ($) {
    my ($time) = $_[0];	# Don't shift
    unless (defined $time and length ($time) == 5) {
	$time = &_time_guess2riscos;	# Pass on @_
    }

    my $buffer = 'x'x256;
    return undef
      unless defined $time and swix ($standard_date_and_time,
				     $standard_date_and_time_mask,
				     $time, $buffer, 256);
    $buffer =~ /^([^\0]*)/;	# Everything until the first \0
    $1;
}
sub time2_raw ($$;$) {
    my ($swi, $time, $terr) = @_;
    $terr = -1 unless defined $terr;	# Won't hurt for UTC call
    unless (defined $time and length ($time) == 5) {
	$time = &_time_guess2riscos ($time);
    }

    my $buffer = 'x'x36;
    return ()
      unless defined $time
	 and defined swix ($swi, $time2_mask, $terr, $time, $buffer);
    unpack 'I*', $buffer;
}

sub fix_os {
    return wantarray ? () : undef
      unless my @raw = &time2_raw;	# Pass on @_
    shift @raw;	# Loose centiseconds
    $raw[4]--;	# Lots of bloody fiddly corrections from OS to ANSI
    $raw[5] -= 1900;
    $raw[6]--;
    $raw[7]--;
    @raw;
}

sub time2utc ($) {
    return () unless defined $_[0];
    return (gmtime (unpack 'I', $_[0]))[0..7] if 4 == length $_[0];
    (fix_os $time2utc, @_);	# Always list context
}

sub time2local ($;$) {
    croak "Can't yet convert territory $_[1]"
      if defined $_[1] and -1 != $_[1];
    return &standard_date_and_time unless wantarray;
    return (localtime (unpack 'I', $_[0]))[0..7] if 4 == length $_[0];
    fix_os $time2local, @_;
}

$time2utc && $time2local && $standard_date_and_time;
__END__

=head1 NAME

RISCOS::Time -- perl interface to S<RISC OS> time SWIs

=head1 SYNOPSIS

    use RISCOS::Time qw (time2utc time2local);
    print 'symtable updated ', scalar time2local $time;
    print 'script started ' . time2utc (0);	# Auto converts from "age"

=head1 DESCRIPTION

This module provides perl interface to the SWIs
C<OS_ConvertStandardDateAndTime>, C<Territory_ConvertTimeToUTCOrdinals> and
C<Territory_ConvertTimeToOrdinals> and functions to convert between different
formats for storing time information.

Time formats currently understood are

=over 4

=item * numeric ages as returned by perl's C<-A>, C<-C> and C<-M> functions

=item 4 byte scalars - Unix times, seconds starting from 1st January 1970

=item 5 byte scalars - S<RISC OS> times, centiseconds starting from 1st January 1900

=item 6 byte scalars - S<RISC OS> times, as stored in C<ALF> files.

=back

Clearly it is not possible to automatically distinguish between all these
formats so where guessing is necessary the following heuristics are used:

=over 4

=item *

If I<time> is composed soley of numeric characters (0-9, "-" and ".") then it
is assumed to be an age in days since the time the script started running
(see L<perlvar/$^T>). Clearly there is possible ambiguity with RISC OS 5 byte
ages that happen to be eqactly 5 characters and composed of legal numeric
characters. However the most recent 5 byte time which could be confused is
C<"99999">, 23:17:53.85 on Friday 18th November 1977, which is likely to predate
most stamped files by about a decade.

=item *

Otherwise C<if (length $time == 5)> then C<$time> is assumed to be a 5 byte RISC
OS time, encoded as centiseconds since 1900/1/1.

=item *

C<if (length $time == 4)> then C<$time> is taken to be a 4 byte Unix time,
seconds since 1970/1/1 C<pack>ed as C<'I'>.

=item *

C<if (length $time == 6)> then C<$time> is expected to be a 6 byte time as
stored in C<ALF> files, also centiseconds since 1900/1/1. If the MSB is not 0
the time is rejected, otherwise it is treated as the equivalent 5 byte RISC OS
time.

=back

=head2 SWI interface

As the SWI interface functions are expect to be used with native times if
C<length $time == 5> then it is taken as a RISC OS time, taking priority over
the guessing heuristic.

=over 4

=item standard_date_and_time <time>

calls C<OS_ConvertStandardDateAndTime> for I<time>, returning the string
representation of that time for the local timezone (the format being set by
C<<Sys$DateFormatE<gt>>). 

=item time2utc <time>

calls C<Territory_ConvertTimeToUTCOrdinals> for I<time>, returning an array of
integers, adjusted to be consistent with the return array from C<gmtime>.

    #  0    1    2     3     4    5     6     7
    ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = time2utc($time);

In particular this means that C<$mon> has the range 0..11 and C<$wday> has
the range 0..6 with Sunday as day 0.  B<Note> C<$year> is the number of
years since 1900, B<not> simply the last two digits of the year.
If C<length $time == 4> calls C<gmtime> directly, but drops the 9th element
("is daylight saving time").

=item time2local <time> [, <territory>]

calls C<Territory_ConvertTimeToOrdinals> for I<time> to convert to local times.
In scalar context behaves as a call to C<standard_date_and_time>, in array
context returns an array as described in C<time2utc>. If C<length $time == 4>
calls C<localtime> directly. I<territory> defaults to C<-1> - the current
territory.

Support for territories other than C<-1> (the current territory) is rather
limited - currently territories may only be used in array context and must be
specified by number only, rather than name. Calls in scalar context may only use
the current territory. (Otherwise the script C<die>s with an error).

=item time2_raw <swi_number>, <time> [, <territory>]

calls the supplied SWI (which is expected to be
C<Territory_ConvertTimeToUTCOrdinals> or C<Territory_ConvertTimeToOrdinals>) and
returns an array of integers

    #  0     1    2     3     4    5    6     7    8
    ($csec,$sec,$min,$hour,$mday,$mon,$year,$wday,$yday)

where the ranges for C<$mon>, C<$wday> and C<$yday> run from B<1> upwards
(I<c.f.> B<0> upwards for the other subroutines and the perl builtins) and
C<$year> is the full Gregorian year (rather then the number of years since
1900).

=back

Values are returned consistent with ANSI C and perl's idea of the base values
for days of the week, days of the year I<etc.> from C<time2utc> and
C<time2local> - the native RISC OS values are available from C<time2_raw>.

=head2 Conversion functions

13 functions are provided to convert between different time formats. In scalar
context all convert only the first argument, in list context a list of
conversions corresponding to the argument list.

=over 4

=item time_guess2riscos

Each argument is processed according to the guessing heuristics to determine
which conversion to use.

=item time_age2cs

=item time_age2riscos

=item time_age2unix

=item time_cs2age

=item time_cs2riscos

=item time_cs2unix

=item time_riscos2age

=item time_riscos2cs

=item time_riscos2unix

=item time_unix2age

=item time_unix2cs

=item time_unix2riscos

Arguments are converted from the first named format to the second, where formats
are

=over 4

=item age

numeric ages as returned by perl's C<-A>, C<-C> and C<-M> functions (see
L<perlfunc/-X>)in days since the script start time (C<$^T>).

=item cs

centiseconds since 1900/1/1 expressed as a number.

=item riscos

centiseconds since 1900/1/1 packed as 5 bytes.

=item unix

seconds since 1970/1/1 packed as 4 bytes.

=back

=back

=head2 Constants

C<RISCOS::Time> is able to export these "useful" constants:

=over 4

=item $s_1900to1970

C<2208988800>, the number of seconds between Janaury 1st 1900 and 1970.

=item $seconds_per_day

C<86400>, the number of seconds in a day.

=back

=head1 BUGS

Support for territories other than the current territory is very limited. It
might help if Acorn supplied more than one territory on the standard UK machine.

=head1 AUTHOR

Nicholas Clark <F<nick@unfortu.net>>

=cut
