package Venus::Try;

use 5.018;

use strict;
use warnings;

use Venus::Class 'attr', 'base';

base 'Venus::Kind::Utility';

use Scalar::Util ();

# ATTRIBUTES

attr 'invocant';
attr 'arguments';
attr 'on_try';
attr 'on_catch';
attr 'on_default';
attr 'on_finally';

# BUILDERS

sub build_arg {
  my ($self, $data) = @_;

  return {
    on_try => $data,
  };
}

sub build_self {
  my ($self, $data) = @_;

  $self->on_catch([]) if !defined $self->on_catch;

  return $self;
}

# METHODS

sub call {
  my ($self, $callback) = @_;

  $self->on_try($self->callback($callback));

  return $self;
}

sub callback {
  my ($self, $callback) = @_;

  if (not(UNIVERSAL::isa($callback, 'CODE'))) {
    my $method;
    my $invocant = $self->invocant;

    if (defined($invocant)) {
      $method = $invocant->can($callback);
    }
    else {
      $method = $self->can($callback);
    }

    if (!$method) {
      my $throw;
      my $error = sprintf(qq(Can't locate object method "%s" on package "%s"),
        ($callback, $invocant ? ref($invocant) : ref($self)));
      $throw = $self->throw;
      $throw->name('on.callback');
      $throw->message($error);
      $throw->error;
    }

    $callback = sub {goto $method};
  }

  return $callback;
}

sub catch {
  my ($self, $package, $callback) = @_;

  push @{$self->on_catch}, [$package, $self->callback($callback)];

  return $self;
}

sub default {
  my ($self, $callback) = @_;

  $self->on_default($self->callback($callback));

  return $self;
}

sub error {
  my ($self, $variable) = @_;

  $self->on_default(sub{($$variable) = @_}) if $variable;

  return $self;
}

sub execute {
  my ($self, $callback, @args) = @_;

  unshift @args, @{$self->arguments}
    if $self->arguments && @{$self->arguments};

  unshift @args, $self->invocant
    if defined($self->invocant);

  return wantarray ? ($callback->(@args)) : $callback->(@args);
}

sub finally {
  my ($self, $callback) = @_;

  $self->on_finally($self->callback($callback));

  return $self;
}

sub maybe {
  my ($self) = @_;

  $self->on_default(sub{''});

  return $self;
}

sub no_catch {
  my ($self) = @_;

  $self->on_catch([]);

  return $self;
}

sub no_default {
  my ($self) = @_;

  $self->on_default(undef);

  return $self;
}

sub no_finally {
  my ($self) = @_;

  $self->on_finally(undef);

  return $self;
}

sub no_try {
  my ($self) = @_;

  $self->on_try(undef);

  return $self;
}

sub result {
  my ($self, @args) = @_;

  my $dollarat = $@;
  my @returned;

  # try
  my $error = do {
    local $@;
    eval {
      my $tryer = $self->on_try;
      @returned = ($self->execute($tryer, @args));
    };
    $@;
  };

  # catch
  if ($error) {
    my $caught = $error;
    my $catchers = $self->on_catch;
    my $default = $self->on_default;

    for my $catcher (@$catchers) {
      if (UNIVERSAL::isa($caught, $catcher->[0])) {
        @returned = ($catcher->[1]->($caught));
        last;
      }
    }

    # catchall
    if(!@returned) {
      if ($default) {
        @returned = ($default->($caught))
      }
    }

    # uncaught
    if(!@returned) {
      if (Scalar::Util::blessed($caught)) {
        die $caught;
      }
      else {
        if (UNIVERSAL::isa($caught, 'Venus::Error')) {
          $caught->throw;
        }
        else {
          require Venus::Error;
          Venus::Error->throw($caught);
        }
      }
    }
  }

  # finally
  if (my $finally = $self->on_finally) {
    $self->execute($finally, @args);
  }

  $@ = $dollarat;

  return wantarray ? (@returned) : $returned[0];
}

1;



=head1 NAME

Venus::Try - Try Class

=cut

=head1 ABSTRACT

Try Class for Perl 5

=cut

=head1 SYNOPSIS

  package main;

  use Venus::Try;

  my $try = Venus::Try->new;

  $try->call(sub {
    my (@args) = @_;

    # try something

    return time;
  });

  $try->catch('Example::Error', sub {
    my ($caught) = @_;

    # caught an error (exception)

    return;
  });

  $try->default(sub {
    my ($caught) = @_;

    # catch the uncaught

    return;
  });

  $try->finally(sub {
    my (@args) = @_;

    # always run after try/catch

    return;
  });

  my @args;

  my $result = $try->result(@args);

=cut

=head1 DESCRIPTION

This package provides an object-oriented interface for performing complex
try/catch operations.

=cut

=head1 ATTRIBUTES

This package has the following attributes:

=cut

=head2 invocant

  invocant(Object)

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

=cut

=head2 arguments

  arguments(ArrayRef)

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

=cut

=head2 on_try

  on_try(CodeRef)

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

=cut

=head2 on_catch

  on_catch(ArrayRef[CodeRef])

This attribute is read-write, accepts C<(ArrayRef[CodeRef])> values, is optional, and defaults to C<[]>.

=cut

=head2 on_default

  on_default(CodeRef)

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

=cut

=head2 on_finally

  on_finally(CodeRef)

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

=cut

=head1 INHERITS

This package inherits behaviors from:

L<Venus::Kind::Utility>

=cut

=head1 METHODS

This package provides the following methods:

=cut

=head2 call

  call(Str | CodeRef $method) (Try)

The call method takes a method name or coderef, registers it as the tryable
routine, and returns the object. When invoked, the callback will received an
C<invocant> if one was provided to the constructor, the default C<arguments> if
any were provided to the constructor, and whatever arguments were provided by
the invocant.




I<Since C<0.01>>

=over 4

=item call example 1

  package main;

  use Venus::Try;

  my $try = Venus::Try->new;

  my $call = $try->call(sub {
    my (@args) = @_;

    return [@args];
  });

  # bless({ on_catch => ... }, "Venus::Try")

=back

=cut

=head2 callback

  callback(Str | CodeRef $method) (CodeRef)

The callback method takes a method name or coderef, and returns a coderef for
registration. If a coderef is provided this method is mostly a passthrough.

I<Since C<0.01>>

=over 4

=item callback example 1

  package main;

  use Venus::Try;

  my $try = Venus::Try->new;

  my $callback = $try->callback(sub {
    my (@args) = @_;

    return [@args];
  });

  # sub { ... }

=back

=over 4

=item callback example 2

  package Example1;

  sub new {
    bless {};
  }

  sub test {
    my (@args) = @_;

    return [@args];
  }

  package main;

  use Venus::Try;

  my $try = Venus::Try->new(
    invocant => Example1->new,
  );

  my $callback = $try->callback('test');

  # sub { ... }

=back

=over 4

=item callback example 3

  package main;

  use Venus::Try;

  my $try = Venus::Try->new;

  my $callback = $try->callback('missing_method');

  # Exception! Venus::Try::Error (isa Venus::Error)

=back

=cut

=head2 catch

  catch(Str $isa, Str | CodeRef $method) (Try)

The catch method takes a package or ref name, and when triggered checks whether
the captured exception is of the type specified and if so executes the given
callback.

I<Since C<0.01>>

=over 4

=item catch example 1

  package main;

  use Venus::Try;

  my $try = Venus::Try->new;

  $try->call(sub {
    my (@args) = @_;

    die $try;
  });

  my $catch = $try->catch('Venus::Try', sub {
    my (@args) = @_;

    return [@args];
  });

  # bless({ on_catch => ... }, "Venus::Try")

=back

=cut

=head2 default

  default(Str | CodeRef $method) (Try)

The default method takes a method name or coderef and is triggered if no
C<catch> conditions match the exception thrown.

I<Since C<0.01>>

=over 4

=item default example 1

  package main;

  use Venus::Try;

  my $try = Venus::Try->new;

  $try->call(sub {
    my (@args) = @_;

    die $try;
  });

  my $default = $try->default(sub {
    my (@args) = @_;

    return [@args];
  });

  # bless({ on_catch => ... }, "Venus::Try")

=back

=cut

=head2 error

  error(Ref $variable) (Try)

The error method takes a scalar reference and assigns any uncaught exceptions
to it during execution.

I<Since C<0.01>>

=over 4

=item error example 1

  package main;

  use Venus::Try;

  my $try = Venus::Try->new;

  $try->call(sub {
    my (@args) = @_;

    die $try;
  });

  my $error = $try->error(\my $object);

  # bless({ on_catch => ... }, "Venus::Try")

=back

=cut

=head2 execute

  execute(CodeRef $code, Any @args) (Any)

The execute method takes a coderef and executes it with any given arguments.
When invoked, the callback will received an C<invocant> if one was provided to
the constructor, the default C<arguments> if any were provided to the
constructor, and whatever arguments were passed directly to this method. This
method can return a list of values in list-context.

I<Since C<0.01>>

=over 4

=item execute example 1

  package Example2;

  sub new {
    bless {};
  }

  package main;

  use Venus::Try;

  my $try = Venus::Try->new(
    invocant => Example2->new,
    arguments => [1,2,3],
  );

  my $execute = $try->execute(sub {
    my (@args) = @_;

    return [@args];
  });

  # [bless({}, "Example2"), 1, 2, 3]

=back

=cut

=head2 finally

  finally(Str | CodeRef $method) (Try)

The finally method takes a package or ref name and always executes the callback
after a try/catch operation. The return value is ignored. When invoked, the
callback will received an C<invocant> if one was provided to the constructor,
the default C<arguments> if any were provided to the constructor, and whatever
arguments were provided by the invocant.

I<Since C<0.01>>

=over 4

=item finally example 1

  package Example3;

  sub new {
    bless {};
  }

  package main;

  use Venus::Try;

  my $try = Venus::Try->new(
    invocant => Example3->new,
    arguments => [1,2,3],
  );

  $try->call(sub {
    my (@args) = @_;

    return $try;
  });

  my $finally = $try->finally(sub {
    my (@args) = @_;

    $try->{args} = [@args];
  });

  # bless({ on_catch => ... }, "Venus::Try")

=back

=cut

=head2 maybe

  maybe() (Try)

The maybe method registers a default C<catch> condition that returns falsy,
i.e. an empty string, if an exception is encountered.

I<Since C<0.01>>

=over 4

=item maybe example 1

  package main;

  use Venus::Try;

  my $try = Venus::Try->new;

  $try->call(sub {
    my (@args) = @_;

    die $try;
  });

  my $maybe = $try->maybe;

  # bless({ on_catch => ... }, "Venus::Try")

=back

=cut

=head2 no_catch

  no_catch() (Try)

The no_catch method removes any configured catch conditions and returns the
object.

I<Since C<0.01>>

=over 4

=item no_catch example 1

  package main;

  use Venus::Try;

  my $try = Venus::Try->new;

  $try->call(sub {
    my (@args) = @_;

    die $try;
  });

  $try->catch('Venus::Try', sub {
    my (@args) = @_;

    return [@args];
  });


  my $no_catch = $try->no_catch;

  # bless({ on_catch => ... }, "Venus::Try")

=back

=cut

=head2 no_default

  no_default() (Try)

The no_default method removes any configured default condition and returns the
object.

I<Since C<0.01>>

=over 4

=item no_default example 1

  package main;

  use Venus::Try;

  my $try = Venus::Try->new;

  $try->call(sub {
    my (@args) = @_;

    die $try;
  });

  my $default = $try->default(sub {
    my (@args) = @_;

    return [@args];
  });

  my $no_default = $try->no_default;

  # bless({ on_catch => ... }, "Venus::Try")

=back

=cut

=head2 no_finally

  no_finally() (Try)

The no_finally method removes any configured finally condition and returns the
object.

I<Since C<0.01>>

=over 4

=item no_finally example 1

  package Example4;

  sub new {
    bless {};
  }

  package main;

  use Venus::Try;

  my $try = Venus::Try->new(
    invocant => Example4->new,
    arguments => [1,2,3],
  );

  $try->call(sub {
    my (@args) = @_;

    return $try;
  });

  $try->finally(sub {
    my (@args) = @_;

    $try->{args} = [@args];
  });

  my $no_finally = $try->no_finally;

  # bless({ on_catch => ... }, "Venus::Try")

=back

=cut

=head2 no_try

  no_try() (Try)

The no_try method removes any configured C<try> operation and returns the
object.

I<Since C<0.01>>

=over 4

=item no_try example 1

  package main;

  use Venus::Try;

  my $try = Venus::Try->new;

  $try->call(sub {
    my (@args) = @_;

    return [@args];
  });

  my $no_try = $try->no_try;

  # bless({ on_catch => ... }, "Venus::Try")

=back

=cut

=head2 result

  result(Any @args) (Any)

The result method executes the try/catch/default/finally logic and returns
either 1) the return value from the successfully tried operation 2) the return
value from the successfully matched catch condition if an exception was thrown
3) the return value from the default catch condition if an exception was thrown
and no catch condition matched. When invoked, the C<try> and C<finally>
callbacks will received an C<invocant> if one was provided to the constructor,
the default C<arguments> if any were provided to the constructor, and whatever
arguments were passed directly to this method. This method can return a list of
values in list-context.

I<Since C<0.01>>

=over 4

=item result example 1

  package main;

  use Venus::Try;

  my $try = Venus::Try->new;

  $try->call(sub {
    my (@args) = @_;

    return [@args];
  });

  my $result = $try->result;

  # []

=back

=over 4

=item result example 2

  package main;

  use Venus::Try;

  my $try = Venus::Try->new;

  $try->call(sub {
    my (@args) = @_;

    return [@args];
  });

  my $result = $try->result(1..5);

  # [1..5]

=back

=over 4

=item result example 3

  package main;

  use Venus::Try;

  my $try = Venus::Try->new;

  $try->call(sub {die});

  my $result = $try->result;

  # Exception! Venus::Error

=back

=cut