package Data::Object::Exception;

use 5.014;

use strict;
use warnings;
use routines;

use Moo;

use overload (
  '""' => 'explain',
  '~~' => 'explain',
  fallback => 1
);

our $VERSION = '2.02'; # VERSION

has id => (
  is => 'ro'
);

has context => (
  is => 'ro'
);

has frames => (
  is => 'ro'
);

has message => (
  is => 'ro',
  default => 'Exception!'
);

# BUILD

fun BUILD($self, $args) {

  # build stack trace
  return $self->trace(2) if !$self->frames;
}

fun BUILDARGS($class, @args) {

  # constructor arguments
  return {
    @args == 1
      # ...
      ? !ref($args[0])
        # single non-ref argument
        ? (message => $args[0])
        # ...
        : 'HASH' eq ref($args[0])
        # single hash-based argument
        ? %{$args[0]}
        # non hash-based argument
        : ()
        # multiple arguments
      : @args
  };
}

# FUNCTIONS

fun throw($self, $message, $context, $offset) {
  my $id;

  my $class = ref $self || $self;

  my $args = {};

  if (ref $message eq 'ARRAY') {
    ($id, $message) = @$message;
  }

  if (ref $self) {
    for my $name (keys %$self) {
      $args->{$name} = $self->{$name};
    }
  }

  $args->{id} = $id if $id;
  $args->{message} = $message if $message;
  $args->{context} = $context if $context;

  my $exception = $self->new($args);

  die $exception->trace($offset);
}

# METHODS

method explain() {
  $self->trace(1, 1) if !$self->{frames};

  my $frames = $self->{frames};

  my $file = $frames->[0][1];
  my $line = $frames->[0][2];
  my $pack = $frames->[0][0];
  my $subr = $frames->[0][3];

  my $message = $self->{message} || 'Exception!';

  my @stacktrace = ("$message in $file at line $line");

  for (my $i = 1; $i < @$frames; $i++) {
    my $pack = $frames->[$i][0];
    my $file = $frames->[$i][1];
    my $line = $frames->[$i][2];
    my $subr = $frames->[$i][3];

    push @stacktrace, "\t$subr in $file at line $line";
  }

  return join "\n", @stacktrace, "";
}

method trace($offset, $limit) {
  $self->{frames} = my $frames = [];

  for (my $i = $offset // 1; my @caller = caller($i); $i++) {
    push @$frames, [@caller];

    last if defined $limit && $i + 1 == $offset + $limit;
  }

  return $self;
}

1;

=encoding utf8

=head1 NAME

Data::Object::Exception

=cut

=head1 ABSTRACT

Exception Class for Perl 5

=cut

=head1 SYNOPSIS

  use Data::Object::Exception;

  my $exception = Data::Object::Exception->new;

  # $exception->throw

=cut

=head1 DESCRIPTION

This package provides functionality for creating, throwing, and introspecting
exception objects.

=cut

=head1 SCENARIOS

This package supports the following scenarios:

=cut

=head2 args-1

  use Data::Object::Exception;

  my $exception = Data::Object::Exception->new('Oops!');

  # $exception->throw

The package allows objects to be instantiated with a single argument.

=cut

=head2 args-kv

  use Data::Object::Exception;

  my $exception = Data::Object::Exception->new(message => 'Oops!');

  # $exception->throw

The package allows objects to be instantiated with key-value arguments.

=cut

=head1 ATTRIBUTES

This package has the following attributes:

=cut

=head2 context

  context(Any)

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

=cut

=head2 id

  id(Str)

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

=cut

=head2 message

  message(Str)

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

=cut

=head1 METHODS

This package implements the following methods:

=cut

=head2 explain

  explain() : Str

The explain method returns an error message with stack trace.

=over 4

=item explain example #1

  use Data::Object::Exception;

  my $exception = Data::Object::Exception->new('Oops!');

  $exception->explain

=back

=cut

=head2 throw

  throw(Tuple[Str, Str] | Str $message, Any $context, Maybe[Number] $offset) : Any

The throw method throws an error with message (and optionally, an ID).

=over 4

=item throw example #1

  use Data::Object::Exception;

  my $exception = Data::Object::Exception->new;

  $exception->throw('Oops!')

=back

=over 4

=item throw example #2

  use Data::Object::Exception;

  my $exception = Data::Object::Exception->new('Oops!');

  $exception->throw

=back

=over 4

=item throw example #3

  use Data::Object::Exception;

  my $exception = Data::Object::Exception->new;

  $exception->throw(['E001', 'Oops!'])

=back

=cut

=head2 trace

  trace(Int $offset, $Int $limit) : Object

The trace method compiles a stack trace and returns the object. By default it
skips the first frame.

=over 4

=item trace example #1

  use Data::Object::Exception;

  my $exception = Data::Object::Exception->new('Oops!');

  $exception->trace(0)

=back

=over 4

=item trace example #2

  use Data::Object::Exception;

  my $exception = Data::Object::Exception->new('Oops!');

  $exception->trace(1)

=back

=over 4

=item trace example #3

  use Data::Object::Exception;

  my $exception = Data::Object::Exception->new('Oops!');

  $exception->trace(0,1)

=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/data-object-exception/blob/master/LICENSE>.

=head1 PROJECT

L<Wiki|https://github.com/iamalnewkirk/data-object-exception/wiki>

L<Project|https://github.com/iamalnewkirk/data-object-exception>

L<Initiatives|https://github.com/iamalnewkirk/data-object-exception/projects>

L<Milestones|https://github.com/iamalnewkirk/data-object-exception/milestones>

L<Contributing|https://github.com/iamalnewkirk/data-object-exception/blob/master/CONTRIBUTE.md>

L<Issues|https://github.com/iamalnewkirk/data-object-exception/issues>

=cut