# File: Timer.pm
# Author: pragma_
#
# Purpose: Provides functionality to register and execute one or more subroutines every X seconds.
#
# Caveats: Uses ALARM signal and all its issues.

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

package PBot::Timer;
use parent 'PBot::Class';

use warnings; use strict;
use feature 'unicode_strings';

our $min_timeout = 1;
our $max_seconds = 1000000;
our $seconds = 0;
our @timer_funcs;

$SIG{ALRM} = sub {
  $seconds += $min_timeout;
  alarm $min_timeout;

  # call timer func subroutines
  foreach my $func (@timer_funcs) { &$func; }

  # prevent $seconds over-flow
  $seconds -= $max_seconds if $seconds > $max_seconds;
};

sub initialize {
  my ($self, %conf) = @_;
  my $timeout = $conf{timeout} // 10;
  $min_timeout = $timeout if $timeout < $min_timeout;
  $self->{name} = $conf{name} // "Unnamed $timeout Second Timer";
  $self->{handlers} = [];
  $self->{enabled} = 0;
  # alarm signal handler (poor-man's timer)
  $self->{timer_func} = sub { on_tick_handler($self) };
  return $self;
}

sub start {
  my $self = shift;
  $self->{enabled} = 1;
  push @timer_funcs, $self->{timer_func};
  alarm $min_timeout;
}

sub stop {
  my $self = shift;
  $self->{enabled} = 0;
  @timer_funcs = grep { $_ != $self->{timer_func} } @timer_funcs;
}

sub on_tick_handler {
  my $self = shift;
  my $elapsed = 0;

  if ($self->{enabled}) {
    if ($#{ $self->{handlers} } > -1) {
      # call handlers supplied via register() if timeout for each has elapsed
      foreach my $func (@{ $self->{handlers} }) {
        if (defined $func->{last}) {
          $func->{last} -= $max_seconds if $seconds < $func->{last}; # handle wrap-around of $seconds

          if ($seconds - $func->{last} >= $func->{timeout}) {
            $func->{last} = $seconds;
            $elapsed = 1;
          }
        } else {
          $func->{last} = $seconds;
          $elapsed = 1;
        }

        if ($elapsed) {
          &{ $func->{subref} }($self);
          $elapsed = 0;
        }
      }
    } else {
      # call default overridable handler if timeout has elapsed
      if (defined $self->{last}) {
        $self->{last} -= $max_seconds if $seconds < $self->{last}; # handle wrap-around

        if ($seconds - $self->{last} >= $self->{timeout}) {
          $elapsed = 1;
          $self->{last} = $seconds;
        }
      } else {
        $elapsed = 1;
        $self->{last} = $seconds;
      }

      if ($elapsed) {
        $self->on_tick();
        $elapsed = 0;
      }
    }
  }
}

# overridable method, executed whenever timeout is triggered
sub on_tick {
  my $self = shift;
  print "Tick! $self->{name} $self->{timeout} $self->{last} $seconds\n";
}

sub register {
  my $self = shift;
  my ($ref, $timeout, $id) = @_;

  Carp::croak("Must pass subroutine reference to register()") if not defined $ref;

  # TODO: Check if subref already exists in handlers?
  $timeout = 300 if not defined $timeout; # set default value of 5 minutes if not defined
  $id = 'timer' if not defined $id;

  my $h = { subref => $ref, timeout => $timeout, id => $id };
  push @{ $self->{handlers} }, $h;

  if ($timeout < $min_timeout) {
    $min_timeout = $timeout;
  }

  if ($self->{enabled}) {
    alarm $min_timeout;
  }
}

sub unregister {
  my ($self, $id) = @_;
  Carp::croak("Must pass timer id to unregister()") if not defined $id;
  @{ $self->{handlers} } = grep { $_->{id} ne $id } @{ $self->{handlers} };
}

sub update_interval {
  my ($self, $id, $interval) = @_;

  foreach my $h (@{ $self->{handlers} }) {
    if ($h->{id} eq $id) {
      $h->{timeout} = $interval;
      last;
    }
  }
}

1;