package Venus::Meta;

use 5.018;

use strict;
use warnings;

use Venus;

use base 'Venus::Core';

# METHODS

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

  return 0 if !$name;

  my $data = {map +($_,$_), @{$self->attrs}};

  return $data->{$name} ? true : false;
}

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

  if ($self->{attrs}) {
    return $self->{attrs};
  }

  my $name = $self->{name};
  my @attrs = attrs_resolver($name);

  for my $base (@{$self->bases}) {
    push @attrs, attrs_resolver($base);
  }

  for my $role (@{$self->roles}) {
    push @attrs, attrs_resolver($role);
  }

  my %seen;
  my $results = $self->{attrs} ||= [grep !$seen{$_}++, @attrs];

  return wantarray ? (@$results) : $results;
}

sub attrs_resolver {
  my ($name) = @_;

  no strict 'refs';
  no warnings 'once';

  if (${"${name}::META"} && $${"${name}::META"}{ATTR}) {
    return (sort {
      $${"${name}::META"}{ATTR}{$a}[0] <=> $${"${name}::META"}{ATTR}{$b}[0]
    } keys %{$${"${name}::META"}{ATTR}});
  }
  else {
    return ();
  }
}

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

  return 0 if !$name;

  my $data = {map +($_,$_), @{$self->bases}};

  return $data->{$name} ? true : false;
}

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

  if ($self->{bases}) {
    return $self->{bases};
  }

  my $name = $self->{name};
  my @bases = bases_resolver($name);

  for my $base (@bases) {
    push @bases, bases_resolver($base);
  }

  my %seen;
  my $results = $self->{bases} ||= [grep !$seen{$_}++, @bases];

  return wantarray ? (@$results) : $results;
}

sub bases_resolver {
  my ($name) = @_;

  no strict 'refs';

  return (@{"${name}::ISA"});
}

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

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

  no strict 'refs';

  return ${"${name}::META"};
}


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

  return if !$type;
  return if !$name;

  my $configs;

  for my $source (qw(roles bases mixins self)) {
    $configs = $self->search($source, $type, $name);
    last if @$configs;
  }

  return $configs ? $configs->[0] : undef;
}

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

  return if !$type;

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

  no strict 'refs';

  return if !int grep $type eq $_, qw(attrs bases mixins roles subs);

  my $function = "${type}_resolver";

  my $results = [&{"${function}"}($name)];

  return wantarray ? (@$results) : $results;
}

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

  return 0 if !$name;

  my $data = {map +($_,$_), @{$self->mixins}};

  return $data->{$name} ? true : false;
}

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

  if ($self->{mixins}) {
    return $self->{mixins};
  }

  my $name = $self->{name};
  my @mixins = mixins_resolver($name);

  for my $mixin (@mixins) {
    push @mixins, mixins_resolver($mixin);
  }

  for my $base (@{$self->bases}) {
    push @mixins, mixins_resolver($base);
  }

  my %seen;
  my $results = $self->{mixins} ||= [grep !$seen{$_}++, @mixins];

  return wantarray ? (@$results) : $results;
}

sub mixins_resolver {
  my ($name) = @_;

  no strict 'refs';

  if (${"${name}::META"} && $${"${name}::META"}{MIXIN}) {
    return (map +($_, mixins_resolver($_)), sort {
      $${"${name}::META"}{MIXIN}{$a}[0] <=> $${"${name}::META"}{MIXIN}{$b}[0]
    } keys %{$${"${name}::META"}{MIXIN}});
  }
  else {
    return ();
  }
}

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

  return $self->BLESS(@args);
}

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

  return 0 if !$name;

  my $data = {map +($_,$_), @{$self->roles}};

  return $data->{$name} ? true : false;
}

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

  if ($self->{roles}) {
    return $self->{roles};
  }

  my $name = $self->{name};
  my @roles = roles_resolver($name);

  for my $role (@roles) {
    push @roles, roles_resolver($role);
  }

  for my $base (@{$self->bases}) {
    push @roles, roles_resolver($base);
  }

  my %seen;
  my $results = $self->{roles} ||= [grep !$seen{$_}++, @roles];

  return wantarray ? (@$results) : $results;
}

sub roles_resolver {
  my ($name) = @_;

  no strict 'refs';
  no warnings 'once';

  if (${"${name}::META"} && $${"${name}::META"}{ROLE}) {
    return (map +($_, roles_resolver($_)), sort {
      $${"${name}::META"}{ROLE}{$a}[0] <=> $${"${name}::META"}{ROLE}{$b}[0]
    } keys %{$${"${name}::META"}{ROLE}});
  }
  else {
    return ();
  }
}

sub search {
  my ($self, $from, $type, $name) = @_;

  return if !$from;
  return if !$type;
  return if !$name;

  no strict 'refs';

  my @configs;
  my @sources;

  if (lc($from) eq 'bases') {
    @sources = bases_resolver($self->{name});
  }
  elsif (lc($from) eq 'roles') {
    @sources = roles_resolver($self->{name});
  }
  elsif (lc($from) eq 'mixins') {
    @sources = mixins_resolver($self->{name});
  }
  else {
    @sources = ($self->{name});
  }

  for my $source (@sources) {
    if (lc($type) eq 'sub') {
      if (*{"${source}::${name}"}{"CODE"}) {
        push @configs, [$source, [1, [*{"${source}::${name}"}{"CODE"}]]];
      }
    }
    else {
      if ($${"${source}::META"}{uc($type)}{$name}) {
        push @configs, [$source, $${"${source}::META"}{uc($type)}{$name}];
      }
    }
  }

  my $results = [@configs];

  return wantarray ? (@$results) : $results;
}

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

  return 0 if !$name;

  my $data = {map +($_,$_), @{$self->subs}};

  return $data->{$name} ? true : false;
}

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

  if ($self->{subs}) {
    return $self->{subs};
  }

  my $name = $self->{name};
  my @subs = subs_resolver($name);

  for my $base (@{$self->bases}) {
    push @subs, subs_resolver($base);
  }

  my %seen;
  my $results = $self->{subs} ||= [grep !$seen{$_}++, @subs];

  return wantarray ? (@$results) : $results;
}

sub subs_resolver {
  my ($name) = @_;

  no strict 'refs';

  return (
    grep *{"${name}::$_"}{"CODE"},
    grep /^[_a-zA-Z]\w*$/, keys %{"${name}::"}
  );
}

1;



=head1 NAME

Venus::Meta - Class Metadata

=cut

=head1 ABSTRACT

Class Metadata for Perl 5

=cut

=head1 SYNOPSIS

  package Person;

  use Venus::Class;

  attr 'fname';
  attr 'lname';

  package Identity;

  use Venus::Role;

  attr 'id';
  attr 'login';
  attr 'password';

  sub EXPORT {
    # explicitly declare routines to be consumed
    ['id', 'login', 'password']
  }

  package Authenticable;

  use Venus::Role;

  sub authenticate {
    return true;
  }

  sub AUDIT {
    my ($self, $from) = @_;
    # ensure the caller has a login and password when consumed
    die "${from} missing the login attribute" if !$from->can('login');
    die "${from} missing the password attribute" if !$from->can('password');
  }

  sub EXPORT {
    # explicitly declare routines to be consumed
    ['authenticate']
  }

  package Novice;

  use Venus::Mixin;

  sub points {
    100
  }

  package User;

  use Venus::Class 'attr', 'base', 'mixin', 'test', 'with';

  base 'Person';

  with 'Identity';

  mixin 'Novice';

  attr 'email';

  test 'Authenticable';

  sub valid {
    my ($self) = @_;
    return $self->login && $self->password ? true : false;
  }

  package main;

  my $user = User->new(
    fname => 'Elliot',
    lname => 'Alderson',
  );

  my $meta = $user->meta;

  # bless({name => 'User'}, 'Venus::Meta')

=cut

=head1 DESCRIPTION

This package provides configuration information for L<Venus> derived classes,
roles, and interfaces.

=cut

=head1 METHODS

This package provides the following methods:

=cut

=head2 attr

  attr(Str $name) (Bool)

The attr method returns true or false if the package referenced has the
attribute accessor named.

I<Since C<1.00>>

=over 4

=item attr example 1

  # given: synopsis

  package main;

  my $attr = $meta->attr('email');

  # 1

=back

=over 4

=item attr example 2

  # given: synopsis

  package main;

  my $attr = $meta->attr('username');

  # 0

=back

=cut

=head2 attrs

  attrs() (ArrayRef)

The attrs method returns all of the attributes composed into the package
referenced.

I<Since C<1.00>>

=over 4

=item attrs example 1

  # given: synopsis

  package main;

  my $attrs = $meta->attrs;

  # [
  #   'email',
  #   'fname',
  #   'id',
  #   'lname',
  #   'login',
  #   'password',
  # ]

=back

=cut

=head2 base

  base(Str $name) (Bool)

The base method returns true or false if the package referenced has inherited
the package named.

I<Since C<1.00>>

=over 4

=item base example 1

  # given: synopsis

  package main;

  my $base = $meta->base('Person');

  # 1

=back

=over 4

=item base example 2

  # given: synopsis

  package main;

  my $base = $meta->base('Student');

  # 0

=back

=cut

=head2 bases

  bases() (ArrayRef)

The bases method returns returns all of the packages inherited by the package
referenced.

I<Since C<1.00>>

=over 4

=item bases example 1

  # given: synopsis

  package main;

  my $bases = $meta->bases;

  # [
  #   'Person',
  #   'Venus::Core::Class',
  #   'Venus::Core',
  # ]

=back

=cut

=head2 data

  data() (HashRef)

The data method returns a data structure representing the shallow configuration
for the package referenced.

I<Since C<1.00>>

=over 4

=item data example 1

  # given: synopsis

  package main;

  my $data = $meta->data;

  # {
  #   'ATTR' => {
  #     'email' => [
  #       'email'
  #     ]
  #   },
  #   'BASE' => {
  #     'Person' => [
  #       'Person'
  #     ]
  #   },
  #   'ROLE' => {
  #     'Authenticable' => [
  #       'Authenticable'
  #     ],
  #     'Identity' => [
  #       'Identity'
  #     ]
  #   }
  # }

=back

=cut

=head2 find

  find(Str $type, Str $name) (Tuple[Str,Tuple[Int,ArrayRef]])

The find method finds and returns the first configuration for the property type
specified. This method uses the L</search> method to search C<roles>, C<bases>,
C<mixins>, and the source package, in the order listed. The "property type" can
be any one of C<attr>, C<base>, C<mixin>, or C<role>.

I<Since C<1.02>>

=over 4

=item find example 1

  # given: synopsis

  package main;

  my $find = $meta->find;

  # ()

=back

=over 4

=item find example 2

  # given: synopsis

  package main;

  my $find = $meta->find('attr', 'id');

  # ['Identity', [ 1, ['id']]]

=back

=over 4

=item find example 3

  # given: synopsis

  package main;

  my $find = $meta->find('sub', 'valid');

  # ['User', [1, [sub {...}]]]

=back

=over 4

=item find example 4

  # given: synopsis

  package main;

  my $find = $meta->find('sub', 'authenticate');

  # ['Authenticable', [1, [sub {...}]]]

=back

=cut

=head2 local

  local(Str $type) (ArrayRef)

The local method returns the names of properties defined in the package
directly (not inherited) for the property type specified. The C<$type> provided
can be either C<attrs>, C<bases>, C<roles>, or C<subs>.

I<Since C<1.02>>

=over 4

=item local example 1

  # given: synopsis

  package main;

  my $attrs = $meta->local('attrs');

  # ['email']

=back

=over 4

=item local example 2

  # given: synopsis

  package main;

  my $bases = $meta->local('bases');

  # ['Person', 'Venus::Core::Class']

=back

=over 4

=item local example 3

  # given: synopsis

  package main;

  my $roles = $meta->local('roles');

  # ['Identity', 'Authenticable']

=back

=over 4

=item local example 4

  # given: synopsis

  package main;

  my $subs = $meta->local('subs');

  # [
  #   'attr',
  #   'authenticate',
  #   'base',
  #   'email',
  #   'false',
  #   'id',
  #   'login',
  #   'password',
  #   'test',
  #   'true',
  #   'valid',
  #   'with',
  # ]

=back

=cut

=head2 mixin

  mixin(Str $name) (Bool)

The mixin method returns true or false if the package referenced has consumed
the mixin named.

I<Since C<1.02>>

=over 4

=item mixin example 1

  # given: synopsis

  package main;

  my $mixin = $meta->mixin('Novice');

  # 1

=back

=over 4

=item mixin example 2

  # given: synopsis

  package main;

  my $mixin = $meta->mixin('Intermediate');

  # 0

=back

=cut

=head2 mixins

  mixins() (ArrayRef)

The mixins method returns all of the mixins composed into the package
referenced.

I<Since C<1.02>>

=over 4

=item mixins example 1

  # given: synopsis

  package main;

  my $mixins = $meta->mixins;

  # [
  #   'Novice',
  # ]

=back

=cut

=head2 new

  new(Any %args | HashRef $args) (Object)

The new method returns a new instance of this package.

I<Since C<1.00>>

=over 4

=item new example 1

  # given: synopsis

  package main;

  $meta = Venus::Meta->new(name => 'User');

  # bless({name => 'User'}, 'Venus::Meta')

=back

=over 4

=item new example 2

  # given: synopsis

  package main;

  $meta = Venus::Meta->new({name => 'User'});

  # bless({name => 'User'}, 'Venus::Meta')

=back

=cut

=head2 role

  role(Str $name) (Bool)

The role method returns true or false if the package referenced has consumed
the role named.

I<Since C<1.00>>

=over 4

=item role example 1

  # given: synopsis

  package main;

  my $role = $meta->role('Identity');

  # 1

=back

=over 4

=item role example 2

  # given: synopsis

  package main;

  my $role = $meta->role('Builder');

  # 0

=back

=cut

=head2 roles

  roles() (ArrayRef)

The roles method returns all of the roles composed into the package referenced.

I<Since C<1.00>>

=over 4

=item roles example 1

  # given: synopsis

  package main;

  my $roles = $meta->roles;

  # [
  #   'Identity',
  #   'Authenticable'
  # ]

=back

=cut

=head2 search

  search(Str $from, Str $type, Str $name) (ArrayRef[Tuple[Str,Tuple[Int,ArrayRef]]])

The search method searches the source specified and returns the configurations
for the property type specified. The source can be any one of C<bases>,
C<roles>, C<mixins>, or C<self> for the source package. The "property type" can
be any one of C<attr>, C<base>, C<mixin>, or C<role>.

I<Since C<1.02>>

=over 4

=item search example 1

  # given: synopsis

  package main;

  my $search = $meta->search;

  # ()

=back

=over 4

=item search example 2

  # given: synopsis

  package main;

  my $search = $meta->search('roles', 'attr', 'id');

  # [['Identity', [ 1, ['id']]]]

=back

=over 4

=item search example 3

  # given: synopsis

  package main;

  my $search = $meta->search('self', 'sub', 'valid');

  # [['User', [1, [sub {...}]]]]

=back

=over 4

=item search example 4

  # given: synopsis

  package main;

  my $search = $meta->search('self', 'sub', 'authenticate');

  # [['User', [1, [sub {...}]]]]

=back

=cut

=head2 sub

  sub(Str $name) (Bool)

The sub method returns true or false if the package referenced has the
subroutine named on the package directly, or any of its superclasses.

I<Since C<1.00>>

=over 4

=item sub example 1

  # given: synopsis

  package main;

  my $sub = $meta->sub('authenticate');

  # 1

=back

=over 4

=item sub example 2

  # given: synopsis

  package main;

  my $sub = $meta->sub('authorize');

  # 0

=back

=cut

=head2 subs

  subs() (ArrayRef)

The subs method returns all of the subroutines composed into the package
referenced.

I<Since C<1.00>>

=over 4

=item subs example 1

  # given: synopsis

  package main;

  my $subs = $meta->subs;

  # [
  #   'attr', ...,
  #   'base',
  #   'email',
  #   'false',
  #   'fname', ...,
  #   'id',
  #   'lname',
  #   'login',
  #   'new', ...,
  #   'role',
  #   'test',
  #   'true',
  #   'with', ...,
  # ]

=back

=cut