package JIRA::REST::Class::Abstract;
use base qw( Class::Accessor::Fast );
use strict;
use warnings;
use v5.10;

our $VERSION = '0.02';

# ABSTRACT: An abstract class for C<JIRA::REST::Class> that most of the other objects are based on.

use Carp;
use Data::Dumper::Concise;
use Scalar::Util qw( weaken blessed reftype );

__PACKAGE__->mk_ro_accessors(qw( data factory issue lazy_loaded ));


sub init {
    my $self    = shift;
    my $factory = shift;

    # if the first thing we're passed is supposed to be the factory object
    if (blessed $factory && blessed $factory eq 'JIRA::REST::Class::Factory') {
        $self->{factory} = $factory;
        weaken $self->{factory};
    }
    else {
        # if we're not passed a factory, let's complain about it
        local $Carp::CarpLevel = $Carp::CarpLevel+1;
        confess "factory not passed to init!";
    }

    $self->unload_lazy;
}


sub unload_lazy {
    my $self = shift;
    if ( $self->{lazy_loaded} ) {
        foreach my $field ( keys %{ $self->{lazy_loaded} } ) {
            delete $self->{$field};
            delete $self->{lazy_loaded}->{$field};
        }
    }
    else {
        $self->{lazy_loaded} = { };
    }
}


sub jira { shift->factory->jira }


sub JIRA_REST { shift->jira->{jira_rest} }


sub REST_CLIENT { shift->JIRA_REST->{rest} }


sub make_object { shift->factory->make_object(@_) }
sub make_date   { shift->factory->make_date(@_) }
sub class_for   { shift->factory->get_factory_class(@_) }


sub obj_isa  {
    my ($self, $obj, $type) = @_;
    return unless blessed $obj;
    my $class = $self->class_for($type);
    $obj->isa( $class );
}


sub name_for_user {
    my($self, $user) = @_;
    return $self->obj_isa($user, 'user') ? $user->name : $user;
}


sub key_for_issue {
    my($self, $issue) = @_;
    return $self->obj_isa($issue, 'issue') ? $issue->key : $issue;
}


sub find_link_name_and_direction {
    my ($self, $link, $dir) = @_;

    return unless defined $link;

    # determine the link directon, if provided. defaults to inward.
    $dir = ($dir && $dir =~ /out(?:ward)?/) ? 'outward' : 'inward';

    # if we were passed a link type object, return
    # the name and the direction we were given
    if ( $self->obj_isa($link, 'linktype') ) {
        return $link->name, $dir;
    }

    # search through the link types
    my @types = $self->jira->link_types;
    foreach my $type ( @types ) {
        if (lc $link eq lc $type->inward) {
            return $type->name, 'inward';
        }
        if (lc $link eq lc $type->outward) {
            return $type->name, 'outward';
        }
        if (lc $link eq lc $type->name) {
            return $type->name, $dir;
        }
    }

    # we didn't find anything, so just return what we were passed
    return $link, $dir;
}


sub populate_scalar_data {
    my ($self, $name, $type, $field) = @_;

    if (defined $self->data->{$field}) {
        $self->{$name} = $self->make_object($type, {
            data => $self->data->{$field}
        });
    }
}


sub populate_date_data {
    my ($self, $name, $field) = @_;
    if (defined $self->data->{$field}) {
        $self->{$name} = $self->make_date( $self->data->{$field} );
    }
}


sub populate_list_data {
    my ($self, $name, $type, $field) = @_;
    if (defined $self->data->{$field}) {
        $self->{$name} = [
            map {
                $self->make_object($type, { data => $_ })
            } @{ $self->data->{$field} }
        ];
    }
    else {
        $self->{$name} = []; # rather than undefined, return an empty list
    }
}


sub populate_scalar_field {
    my ($self, $name, $type, $field) = @_;
    if (defined $self->fields->{$field}) {
        $self->{$name} = $self->make_object($type, {
            data => $self->fields->{$field}
        });
    }
}


sub populate_list_field {
    my ($self, $name, $type, $field) = @_;
    if (defined $self->fields->{$field}) {
        $self->{$name} = [
            map {
                $self->make_object($type, { data => $_ })
            } @{ $self->fields->{$field} }
        ];
    }
    else {
        $self->{$name} = []; # rather than undefined, return an empty list
    }
}

###########################################################################
#
# the code in here is liberally borrowed from
# Class::Accessor, Class::Accessor::Fast, and Class::Accessor::Contextual
#

if (eval { require Sub::Name }) {
    Sub::Name->import;
}


sub mk_contextual_ro_accessors {
    my($class, @fields) = @_;

    foreach my $field ( @fields ) {
        my $accessor = sub {
            if (@_ == 1) {
                return $_[0]->{$field} unless wantarray;
                return @{ $_[0]->{$field} } if ref($_[0]->{$field}) eq 'ARRAY';
                return %{ $_[0]->{$field} } if ref($_[0]->{$field}) eq 'HASH';
                return $_[0]->{$field};
            }
            else {
                my $caller = caller;
                $_[0]->_croak("'$caller' cannot alter the value of '$field' " .
                              "on objects of class '$class'");
            }
        };

        $class->make_subroutine($field, $accessor);
    }

    return $class;
};


sub mk_deep_ro_accessor {
    my($class, @field) = @_;

    my $accessor = sub {
        if (@_ == 1) {
            my $ptr = $_[0];
            foreach my $f (@field) {
                $ptr = $ptr->{$f};
            }
            return $ptr unless wantarray;
            return @$ptr if ref($ptr) eq 'ARRAY';
            return %$ptr if ref($ptr) eq 'HASH';
            return $ptr;
        }
        else {
            my $caller = caller;
            $_[0]->_croak("'$caller' cannot alter the value of '$field[-1]' " .
                          "on objects of class '$class'");
        }
    };

    $class->make_subroutine($field[-1], $accessor);

    return $class;
};


sub mk_lazy_ro_accessor {
    my($class, $field, $constructor) = @_;

    my $accessor = sub {
        if (@_ == 1) {
            unless ($_[0]->{lazy_loaded}->{$field}) {
                $_[0]->{$field} = $constructor->(@_);
                $_[0]->{lazy_loaded}->{$field} = 1;
            }
            return $_[0]->{$field} unless wantarray;
            return @{ $_[0]->{$field} } if ref($_[0]->{$field}) eq 'ARRAY';
            return %{ $_[0]->{$field} } if ref($_[0]->{$field}) eq 'HASH';
            return $_[0]->{$field};
        }
        else {
            my $caller = caller;
            $_[0]->_croak("'$caller' cannot alter the value of '$field' " .
                          "on objects of class '$class'");
        }
    };

    $class->make_subroutine($field, $accessor);

    return $class;
};


sub mk_data_ro_accessors {
    my $class = shift;

    foreach my $field ( @_ ) {
        $class->mk_deep_ro_accessor(qw( data ), $field);
    }
}


sub mk_field_ro_accessors {
    my $class = shift;

    foreach my $field ( @_ ) {
        $class->mk_deep_ro_accessor(qw( data fields ), $field);
    }
}


{   # we're going some magic here
    no strict 'refs'; ## no critic

    sub make_subroutine {
        my($proto, $name, $sub) = @_;
        my($class) = ref $proto || $proto;

        my $fullname = "${class}::$name";
        unless (defined &{$fullname}) {
            subname($fullname, $sub) if defined &subname;
            *{$fullname} = $sub;
        }
    }

} # end of ref no-stricture zone

###########################################################################


sub dump {
    my $self = shift;
    return $self->shallow_dump( $self );
}


sub shallow_dump {
    shift; # we don't need $self
    return Dumper( __shallow_copy(@_, 'top') );
}

sub __shallow_copy {
    my $thing = shift;
    my $top   = pop;

    if (not ref $thing) {
        return $thing;
    }

    if ( my $class = blessed $thing ) {
        if ($class eq 'JSON::PP::Boolean') {
            return $thing ? 'JSON::PP::true' : 'JSON::PP::false';
        }
        elsif ($class eq 'JSON') {
            return "$thing";
        }
        elsif ($class eq 'REST::Client') {
            return '%s->host(%s)', $class, $thing->getHost;
        }
        elsif ($class eq 'DateTime') {
            return "DateTime(  $thing  )";
        }
        elsif ($top) {
            if (reftype $thing eq 'ARRAY') {
                return [ map { __shallow_copy($_) } @{$thing} ], $class;
            }
            elsif (reftype $thing eq 'HASH') {
                return +{
                    map { $_ => __shallow_copy($thing->{$_}) } keys %{$thing}
                }, $class;
            }
            return Dumper($thing);
        }
        else {
            foreach my $method (qw/ name key id /) {
                if ( $thing->can($method) ) {
                    my $value = $thing->$method // 'undef';
                    return sprintf '%s->%s(%s)',
                        $class, $method, $value;
                }
            }
            return "$thing";
        }
    }

    if (ref $thing eq 'SCALAR') {
        return $$thing;
    }
    elsif (ref $thing eq 'ARRAY') {
        return [ map { __shallow_copy($_) } @{$thing} ];
    }
    elsif (ref $thing eq 'HASH') {
        return +{
            map { $_ => __shallow_copy($thing->{$_}) } keys %{$thing}
        };
    }
    return $thing;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

JIRA::REST::Class::Abstract - An abstract class for C<JIRA::REST::Class> that most of the other objects are based on.

=head1 VERSION

version 0.02

=head1 METHODS

=head2 B<init>

Method to perform post-instantiation initialization of the object. The first argument will be the factory object which created the object.

=head2 B<unload_lazy>

Clears the hash that tracks lazily loaded methods so they get loaded again.

=head2 B<factory>

Returns the C<JIRA::REST::Class::Factory> object that created this object.

=head2 B<jira>

Returns the C<JIRA::REST::Class> object that created this object.

=head2 B<make_object>

A pass-through method that calls C<JIRA::REST::Class::Factory::make_object()>.

=head2 B<make_date>

A pass-through method that calls C<JIRA::REST::Class::Factory::make_date()>.

=head2 B<class_for>

A pass-through method that calls C<JIRA::REST::Class::Factory::get_factory_class()>.

=head2 B<obj_isa>

When passed a scalar that could be an object and a class string, returns whether the scalar is, in fact, an object of that class.  Looks up the actual class using C<class_for()>, which calls  C<JIRA::REST::Class::Factory::get_factory_class()>.

=head2 B<name_for_user>

When passed a scalar that could be a C<JIRA::REST::Class::User> object, returns the name of the user if it is a C<JIRA::REST::Class::User> object, or the unmodified scalar if it is not.

=head2 B<key_for_issue>

When passed a scalar that could be a C<JIRA::REST::Class::Issue> object, returns the key of the issue if it is a C<JIRA::REST::Class::Issue> object, or the unmodified scalar if it is not.

=head2 B<find_link_name_and_direction>

When passed two scalars, one that could be a C<JIRA::REST::Class::Issue::LinkType> object and another that is a direction (inward/outward), returns the name of the link type and direction if it is a C<JIRA::REST::Class::Issue::LinkType> object, or attempts to determine the link type and direction from the provided scalars.

=head2 B<mk_contextual_ro_accessors> list of accessors to make

Because I didn't want to give up Class::Accessor::Fast, but wanted to be
able to make contextual accessors when it was useful.

=head2 B<mk_deep_ro_accessor> LIST OF KEYS TO HASH

Why do accessors have to be only for the top level of the hash?  Why can't they be several layers deep?  This method takes a list of keys for the hash this object is based on and creates an accessor that goes down deeper than just the first level.

=head2 B<mk_lazy_ro_accessor> field, sub_ref_to_construct_value

Makes an accessor that checks to see if the value for the accessor has been loaded, and, if it hasn't, runs the provided subroutine to construct the value.  Especially good for loading values that are objects populated by REST calls.

=head2 B<dump>

Returns a stringified representation of the issue's data generated by Data::Dumper::Concise.

=head1 INTERNAL METHODS

=head2 B<JIRA_REST>

An accessor that returns the C<JIRA::REST> object being used.

=head2 B<REST_CLIENT>

An accessor that returns the C<REST::Client> object inside the C<JIRA::REST> object being used.

=head2 B<populate_scalar_data>

Code to make instantiating objects from $self->data easier.

=head2 B<populate_date_data>

Code to make instantiating DateTime objects from $self->data easier.

=head2 B<populate_list_data>

Code to make instantiating lists of objects from $self->data easier.

=head2 B<populate_scalar_field>

Code to make instantiating objects from fields easier.

=head2 B<populate_list_field>

Code to make instantiating lists of objects from fields easier.

=head2 B<mk_data_ro_accessors>

Makes accessors for keys under $self->{data}

=head2 B<mk_field_ro_accessors>

Makes accessors for keys under $self->{data}->{fields}

=head2 B<make_subroutine>

Takes a subroutine name and a subroutine reference, and blesses the subroutine into the class used to call this method.  Can be called with either a class name or a blessed object reference.

=head2 B<shallow_dump> THING

A utility function to produce a shallow dump of a thing.

=for Pod::Coverage init

=head1 AUTHOR

Packy Anderson <packy@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2016 by Packy Anderson.

This is free software, licensed under:

  The Artistic License 2.0 (GPL Compatible)

=cut
