package Zing::Logic::Timer;

use 5.014;

use strict;
use warnings;

use registry 'Zing::Types';
use routines;

use Data::Object::Class;
use Data::Object::ClassHas;

extends 'Zing::Logic';

use Zing::Flow;
use Zing::Queue;

use Time::Crontab;
use Time::Piece;

our $VERSION = '0.11'; # VERSION

# ATTRIBUTES

has 'on_timer' => (
  is => 'ro',
  isa => 'CodeRef',
  new => 1,
);

fun new_on_timer($self) {
  $self->can('handle_timer_event')
}

has 'schedules' => (
  is => 'ro',
  isa => 'ArrayRef[Schedule]',
  new => 1
);

fun new_schedules($self) {
  $self->process->schedules
}

has 'relays' => (
  is => 'ro',
  isa => 'HashRef[Queue]',
  new => 1
);

fun new_relays($self) {
  +{map {$_, Zing::Queue->new(name => $_)} map @{$$_[1]}, @{$self->schedules}}
}

# SHIMS

sub _tick {
  localtime->truncate(to => 'minute')->epoch
}

sub _time {
  CORE::time
}

# METHODS

method flow() {
  my $step_0 = $self->next::method;

  my $step_1 = Zing::Flow->new(
    name => 'on_timer',
    code => fun($step, $loop) { $self->trace('on_timer')->($self) }
  );

  $step_0->append($step_1);
  $step_0
}

my $aliases = {
  # at 00:00 on day-of-month 1 in january
  '@yearly' => '0 0 1 1 *',
  # at 00:00 on day-of-month 1 in january
  '@annually' => '0 0 1 1 *',
  # at 00:00 on day-of-month 1
  '@monthly' => '0 0 1 * *',
  # at 00:00 on monday
  '@weekly' => '0 0 * * 1',
  # at 00:00 on saturday
  '@weekend' => '0 0 * * 6',
  # at 00:00 every day
  '@daily' => '0 0 * * *',
  # at minute 0 every hour
  '@hourly' => '0 * * * *',
  # at every minute
  '@minute' => '* * * * *',
};

method handle_timer_event($name) {
  my $process = $self->process;

  my $_tick = _tick;
  my $_time = _time;

  for (my $i = 0; $i < @{$self->schedules}; $i++) {
    # run each schedule initially, and then once per minute
    next if $_tick == (
      $self->{tick}[$i] || 0
    );

    # unpack schedule
    my $schedule = $self->schedules->[$i];
    my $frequency = $schedule->[0] || '';
    my $cronexpr = $aliases->{$frequency} || $frequency;
    my $queues = $schedule->[1];
    my $message = $schedule->[2];

    # cached crontab object
    my $cron = $self->{cron}[$i] ||= Time::Crontab->new($cronexpr);

    # next unless our times is here!
    next unless $cron->match($_time);

    # record tick for once-per-minute excution
    $self->{tick}[$i] = $_tick;

    # deliver messages to queues
    for my $name (@$queues) {
      $self->relays->{$name}->send($message);
    }
  }

  return $self;
}

1;

=encoding utf8

=head1 NAME

Zing::Logic::Timer - Timer Logic

=cut

=head1 ABSTRACT

Timer Process Logic Chain

=cut

=head1 SYNOPSIS

  package Process;

  use parent 'Zing::Process';

  sub schedules {
    [['@minute', ['tasks'], { do => 1 }]]
  }

  package main;

  use Zing::Logic::Timer;

  my $logic = Zing::Logic::Timer->new(process => Process->new);

  # $logic->execute;

=cut

=head1 DESCRIPTION

This package provides the logic (or logic chain) to be executed by the timer
process event-loop.

=cut

=head1 INHERITS

This package inherits behaviors from:

L<Zing::Logic>

=cut

=head1 LIBRARIES

This package uses type constraints from:

L<Zing::Types>

=cut

=head1 ATTRIBUTES

This package has the following attributes:

=cut

=head2 interupt

  interupt(Interupt)

This attribute is read-only, accepts C<(Interupt)> values, and is optional.

=cut

=head2 on_perform

  on_perform(CodeRef)

This attribute is read-only, accepts C<(CodeRef)> values, and is optional.

=cut

=head2 on_receive

  on_receive(CodeRef)

This attribute is read-only, accepts C<(CodeRef)> values, and is optional.

=cut

=head2 on_register

  on_register(CodeRef)

This attribute is read-only, accepts C<(CodeRef)> values, and is optional.

=cut

=head2 on_reset

  on_reset(CodeRef)

This attribute is read-only, accepts C<(CodeRef)> values, and is optional.

=cut

=head2 on_suicide

  on_suicide(CodeRef)

This attribute is read-only, accepts C<(CodeRef)> values, and is optional.

=cut

=head2 on_timer

  on_timer(CodeRef)

This attribute is read-only, accepts C<(CodeRef)> values, and is optional.

=cut

=head2 process

  process(Process)

This attribute is read-only, accepts C<(Process)> values, and is required.

=cut

=head2 relays

  relays(HashRef[Queue])

This attribute is read-only, accepts C<(HashRef[Queue])> values, and is optional.

=cut

=head2 schedules

  schedules(ArrayRef[Schedule])

This attribute is read-only, accepts C<(ArrayRef[Schedule])> values, and is optional.

=cut

=head1 METHODS

This package implements the following methods:

=cut

=head2 flow

  flow() : Flow

The flow method builds and returns the logic flow for the process event-loop.

=over 4

=item flow example #1

  # given: synopsis

  my $flow = $logic->flow;

=back

=cut

=head2 signals

  signals() : HashRef

The signals method builds and returns the process signal handlers.

=over 4

=item signals example #1

  # given: synopsis

  my $signals = $logic->signals;

=back

=cut

=head1 AUTHOR

Al Newkirk, C<awncorp@cpan.org>

=head1 LICENSE

Copyright (C) 2011-2019, Al Newkirk, et al.

This is free software; you can redistribute it and/or modify it under the terms
of the The Apache License, Version 2.0, as elucidated in the L<"license
file"|https://github.com/iamalnewkirk/zing/blob/master/LICENSE>.

=head1 PROJECT

L<Wiki|https://github.com/iamalnewkirk/zing/wiki>

L<Project|https://github.com/iamalnewkirk/zing>

L<Initiatives|https://github.com/iamalnewkirk/zing/projects>

L<Milestones|https://github.com/iamalnewkirk/zing/milestones>

L<Contributing|https://github.com/iamalnewkirk/zing/blob/master/CONTRIBUTE.md>

L<Issues|https://github.com/iamalnewkirk/zing/issues>

=cut
