
package VSO;

use strict;
use warnings 'all';
use Carp qw( confess croak );
use Scalar::Util qw( weaken openhandle );
use Data::Dumper;
use base 'Exporter';
use VSO::Subtype;

our $VERSION = '0.017';

our @EXPORT = qw(
  has
  before
  after
  extends
  
  subtype as where message
  coerce from via
  enum
);

my $_meta       = { };
my $_subtypes   = { };
my $_coercions  = { };

sub import
{
  # Turn on strict and warnings in the caller:
  import warnings;
  $^H |= 1538;
  my $class = shift;
  my $caller = caller;
  return if $caller eq __PACKAGE__;
  no strict 'refs';
  map {
    *{"$caller\::$_"} = \&{$_}
  } @EXPORT;
  push @{"$caller\::ISA"}, $class if $class eq __PACKAGE__;
  
  $_meta->{ $caller } ||= _new_meta();
  no warnings 'redefine';
  *{"$caller\::meta"} = sub { $_meta->{$caller} };
}# end import()


sub new
{
  my ($class, %args) = @_;
  
  my $s = bless \%args, $class;
  $s->_build();
  $s->BUILD() if $s->can('BUILD');
  
  return $s;
}# end new()


sub _build
{
  my $s = shift;
  
  my $class = ref($s);
  my $meta = $class->meta();
  $meta->{field_names} ||= [ sort keys %{ $meta->{fields} } ];
  my $fields = $meta->{fields};
  
  FIELD: foreach my $name ( @{ $meta->{field_names} } )
  {
    my $props = $fields->{$name};
    my $value = _build_arg( $s, $name, $s->{$name}, $props );
    
    if( $props->{weak_ref} )
    {
      weaken( $s->{$name} = $value );
    }
    else
    {
      $s->{$name} = $value;
    }# end if()
  }# end foreach()
}# end _build()


sub _build_arg
{
  my ($s, $name, $value, $props) = @_;
  
  # No value, no default and it's required:
  if( $props->{required} && (! defined($value) ) && (! $props->{default}) )
  {
    croak "Required param '$name' is required but was not provided.";
  }
  # No value, but we have a default:
  elsif( $props->{default} && (! defined($value) ) )
  {
    if( $props->{lazy} )
    {
      # Deal with this later.
      return;
    }
    else
    {
      return $s->{$name} = $value = $props->{default}->( $s );
    }# end if()
  }# end if()
  
  $value = _validate_field( $s, $name, $value, $props );
  
  if( $props->{where} && defined($value) )
  {
    local $_ = $value;
    confess "Invalid value for property '$name': '$_'"
      unless $props->{where}->( $s );
  }# end if()
  
  return $value;
}# end _build_arg()


sub _validate_field
{
  my ($s, $name, $new_value, $props) = @_;
  
  my $original_type = VSO::Subtype->find(_discover_type( $new_value ));
  my $original_value = $new_value;
  if( $props->{isa} && $props->{required} )
  {
    if( ! defined($new_value) )
    {
      # Nothing?
    }# end if()
    my $is_ok = 0;
    ISA: foreach my $isa ( split /\|/, $props->{isa} )
    {
      $isa =~ s{^(.+?)\[(.+?)\]}{"$1" . "::of::$2"}e
        if $isa =~ m{^.+?\[.+?\]$};
      TYPE_CHECK: {
        my $current_type = VSO::Subtype->find(_discover_type( $new_value ));
        my $wanted_type = VSO::Subtype->find($isa);

        # Verify that the value matches the entire chain of dependencies:
        if( $current_type->isa( $wanted_type ) || $current_type eq $wanted_type )
        {
          if( my $ref = $wanted_type->can('where') )
          {
            local $_ = $new_value;
            if( $wanted_type->where( $s ) )
            {
              $is_ok = 1;
              last ISA;
            }# end if()
          }
          else
          {
            $is_ok = 1;
            last ISA;
          }# end if()
        }
        elsif( my $can_coerce = $props->{coerce} && exists($_coercions->{ $wanted_type }->{ $current_type }) )
        {
          # Can we coerce from this type to the wanted type?:
          my $coercion = $can_coerce ? $_coercions->{ $wanted_type }->{ "$current_type" } : undef;
          local $_ = $new_value;
          if( $coercion )
          {
            $new_value = $coercion->( $s );
            $is_ok = 1;
          }
          else
          {
            next TYPE_CHECK;
          }# end if()
        }
        elsif( eval { $wanted_type->as eq $current_type } )
        {
          local $_ = $new_value;
          if( $wanted_type->where( $s ) )
          {
            $is_ok = 1;
            last ISA;
          }# end if()
        }# end if()

      };
      next ISA;
    }# end foreach()
    
    unless( $is_ok )
    {
      local $_ = $original_value;
      croak "Invalid value for @{[ref($s)]}.$name: isn't a $props->{isa}: [$original_type] '$_'" . (eval{ ': ' . VSO::Subtype->find($props->{isa})->message($s) }||'');
    }# end unless()
  }# end if()
  
  return $new_value;
}# end _validate_field()


sub extends(@)
{
  my $class = caller;
  
  no strict 'refs';
  my $meta = $class->meta();
  map {
    load_class( $_ );
    push @{"$class\::ISA"}, $_;
    my $parent_meta = $_->meta or die "Class $_ has no meta!";
    map {
      $meta->{fields}->{$_} = $parent_meta->{fields}->{$_}
    } keys %{ $parent_meta->{fields} };
    map {
      $meta->{triggers}->{$_} = $parent_meta->{triggers}->{$_}
    } keys %{ $parent_meta->{triggers} };
  } @_;
}# end extends()


sub before($&)
{
  my $class = caller;
  my ($name, $sub) = @_;
  my $meta = $class->meta;
  
  # Sanity:
  croak "You must define property $class.$name before adding triggers to it"
    unless exists($meta->{fields}->{$name}) || $class->can($name);
  
  if( exists($meta->{fields}->{$name}) )
  {
    $meta->{triggers}->{"before.$name"} ||= [ ];
    push @{ $meta->{triggers}->{"before.$name"} }, $sub;
  }
  else
  {
    my $orig = $class->can($name);
    no strict 'refs';
    no warnings 'redefine';
    *{"$class\::$name"} = sub {
      $sub->( @_ );
      $orig->( @_ );
    };
  }# end if()
}# end before()


sub after($&)
{
  my $class = caller;
  my ($name, $sub) = @_;
  my $meta = $class->meta;
  
  # Sanity:
  croak "You must define property $class.$name before adding triggers to it"
    unless exists($meta->{fields}->{$name}) || $class->can($name);
  
  if( exists($meta->{fields}->{$name}) )
  {
    $meta->{triggers}->{"after.$name"} ||= [ ];
    push @{ $meta->{triggers}->{"after.$name"} }, $sub;
  }
  else
  {
    my $orig = $class->can($name);
    no strict 'refs';
    no warnings 'redefine';
    *{"$class\::$name"} = sub {
      my $context = defined(wantarray) ? wantarray ? 'list' : 'scalar' : 'void';
      my ($res,@res);
      $context eq 'list' ? @res = $orig->( @_ ) : $context eq 'scalar' ? $res = $orig->( @_ ) : $orig->( @_ );
      $sub->( @_ );
      $context eq 'list' ? return @res : $context eq 'scalar' ? return $res : return;
    };
  }# end if()
}# end after()


sub has($;@)
{
  my $class = caller;
  my $name = shift;
  my %properties = @_;
  my $meta = $class->meta;
  
  foreach my $type ( split /\|/, ($properties{isa}||'') )
  {
    
    if( my ($reftype, $valtype) = $type =~ m{^((?:Hash|Array)Ref)\[(.+?)\]$} )
    {
      load_class($type)
        unless VSO::Subtype->find($type);
      unless( VSO::Subtype->subtype_exists($type) )
      {
        _add_subtype(
          'name'    => $type,
          'as'      => $reftype,
          'where'   =>
            $reftype eq 'ArrayRef' ?
              sub {
                my $vals = $_;
                ! grep {! ( _discover_type($_)->isa($valtype) || VSO::Subtype->find(_discover_type($_))->isa($valtype) ) } @$vals
              }
              :
              sub {
                my $vals = [ values %$_ ];
                ! grep { ! ( _discover_type($_)->isa($valtype) || VSO::Subtype->find(_discover_type($_))->isa($valtype) ) } @$vals
              },
          'message' => sub { "Must be a valid '$type'" },
        );
      }# end unless()
    }
    else
    {
      load_class($type)
        unless VSO::Subtype->find($type);
    }# end if()
  }# end foreach()
  
  my $props = $meta->{fields}->{$name} = {
    is        => 'rw',
    required  => 1,
    isa       => undef,
    lazy      => 0,
    weak_ref  => 0,
    coerce    => 0,
    %properties,
  };
  
  no strict 'refs';
  *{"$class\::$name"} = sub {
    my $s = shift;
    
    # Getter:
    unless( @_ )
    {
      # Support laziness:
      if( ( ! defined($s->{$name}) ) && $props->{default} )
      {
        if( $props->{weak_ref} )
        {
          weaken($s->{$name} = $props->{default}->( $s ));
        }
        else
        {
          $s->{$name} = $props->{default}->( $s );
        }# end if()
      }# end if()
      
      return $s->{$name};
    }# end unless()
    
    if( $props->{is} eq 'ro' )
    {
      croak "Cannot change readonly property '$name'";
    }
    elsif( $props->{is} eq 'rw' )
    {
      my $new_value = shift;
      my $old_value = $s->{$name};
      
      $new_value = _build_arg( $s, $name, $new_value, $props );
      
      if( my $triggers = $meta->{triggers}->{"before.$name"} )
      {
        map {
          $_->( $s, $new_value, $old_value );
        } @$triggers;
      }# end if()
      
      # Now change the value:
      if( $props->{weak_ref} )
      {
        weaken($s->{$name} = $new_value);
      }
      else
      {
        $s->{$name} = $new_value;
      }# end if()
      
      if( my $triggers = $meta->{triggers}->{"after.$name"} )
      {
        map {
          $_->( $s, $s->{$name}, $old_value);
        } @$triggers;
      }# end if()
      
      # Default to returning the new value:
      $new_value if defined wantarray();
    }# end if()
  };
}# end has()


sub _parse_isa
{
  my $isa = shift;
  
  return split /\|/, $isa;
}# end _parse_isa()


sub _discover_type
{
  my ($val) = @_;
  
  if( my $ref = ref($val) )
  {
    return 'ScalarRef' if $ref eq 'SCALAR';
    return 'ArrayRef' if $ref eq 'ARRAY';
    return 'HashRef' if $ref eq 'HASH';
    return 'CodeRef' if $ref eq 'CODE';
    return 'GlobRef' if $ref eq 'GLOB';
    return 'RegexpRef' if $ref eq 'Regexp';
    return 'FileHandle' if openhandle($val);
    # Otherwise, it's a reference to some kind of object:
    return $ref;
  }
  else
  {
    return 'Undef' unless defined($val);
    return 'Bool' if $val =~ m{^(?:0|1)$};
    return 'Int' if $val =~ m{^\d+$};
    return 'Num' if $val =~ m{^\d+\.?\d*?$};
    # ClassName?:
    (my $fn = "$val.pm") =~ s{::}{/}g;
    return 'ClassName' if exists($INC{$fn});
    return 'Str';
  }# end if()
}# end _discover_type()


sub _new_meta
{
  return {
    fields    => { },
    triggers  => { }
  };
}# end _new_meta()


sub load_class
{
  my $class = shift;

  (my $file = "$class.pm") =~ s|::|/|g;
  no strict 'refs';
  eval { require $file unless defined(@{"$class\::ISA"}) || $INC{$file}; 1 }
    or die $@;
  $INC{$file} ||= $file;
  $class->import(@_);
}# end load_class()


sub _add_subtype
{
  my %args = @_;

  $args{as} ||= '';
  $args{as} =~ s{^(.+?)\[(.+?)\]}{"$1" . "::of::$2"}e
    if $args{as} =~ m{^.+?\[.+?\]$};
  $args{name} =~ s{^(.+?)\[(.+?)\]}{"$1" . "::of::$2"}e
    if $args{name} =~ m{^.+?\[.+?\]$};
  eval <<"PKG";
package $args{name}; {
  our \@ISA = qw( VSO::Subtype $args{as} );
  sub name  { '$args{name}' }
  sub as    { '$args{as}'   }
  sub where;
  sub message;
}
PKG
  no strict 'refs';
  *{"$args{name}::where"} = $args{where};
  *{"$args{name}::message"} = $args{message};
}# end _add_subtype()


sub subtype($;@)
{
  my ($name, %args) = @_;
  
  confess "Subtype '$name' already exists"
    if exists($_subtypes->{$name});
  _add_subtype(
    name    => $name,
    as      => $args{as},
    where   => $args{where} || sub { 1 },
    message => $args{message} || sub { "Must be a valid '$name'" },
  );
}# end subtype()
sub as          { as => shift, @_   }
sub where(&)    { where => $_[0]    }
sub message(&)  { message => $_[0]  }


sub coerce($;@)
{
  my ($to, %args) = @_;
  
  my ($pkg,$filename,$line) = caller;
  confess "Coercion from '$args{from}' to '$to' is already defined in $filename line $line"
    if defined($_coercions->{$to}->{$args{from}});
  $_coercions->{$to}->{$args{from}} = $args{via};
}# end coerce()
sub from    { from => shift, @_ }
sub via(&)  { via  => $_[0]     }


sub enum($$)
{
  my ($name, $vals) = @_;
  _add_subtype(
    name    => $name,
    as      => 'Str',
    where   => sub {
      my $val = $_;
      no warnings 'uninitialized';
      for( @$vals ) {
        return 1 if $_ eq $val;
      }
      return 0;
    },
    message => sub {
      "Must be a valid '$name'"
    }
  );
}# end enum($$)


# All things spring forth from the formless void:

subtype 'Any' =>
  as      '',
  where   { 1 },
  message { '' };

  subtype 'Item'  =>
    as      'Any',
    where   { 1 },
    message { '' };

    subtype 'Undef' =>
      as      'Item',
      where   { ! defined },
      message { "Must not be defined" };

    subtype 'Defined' =>
      as      'Item',
      where   { defined },
      message { "Must be defined" };

      subtype 'Value' =>
        as      'Defined',
        where   { ! ref },
        message { "Cannot be a reference" };

        subtype 'Str' =>
          as      'Value',
          where   { 1 },
          message { '' };

          subtype 'Num' =>
            as      'Str',
            where   { m{^\d+\.?\d*?$} },
            message { 'Must contain only numbers and decimals' };

            subtype 'Int' =>
              as      'Num',
              where   { m{^\d+$} },
              message { 'Must contain only numbers 0-9' };

              subtype 'Bool' =>
                as      'Int',
                where   { ( ! defined($_) ) || m{^(?:1|0)$} },
                message { "Must be a 1 or a 0" };

          subtype 'ClassName' =>
            as      'Str',
            where   { m{^[a-z\:0-9_]+$}i },
            message { 'Must match m{^[a-z\:0-9_]+$}i' };

      subtype 'Ref' =>
        as      'Defined',
        where   { ref },
        message { 'Must be a reference' };

        subtype 'ScalarRef' =>
          as      'Ref',
          where   { ref($_) eq 'SCALAR' },
          message { 'Must be a scalar reference (ScalarRef)' };

        subtype 'ArrayRef'  =>
          as      'Ref',
          where   { ref($_) eq 'ARRAY' },
          message { 'Must be an array reference (ArrayRef)' };

        subtype 'HashRef' =>
          as      'Ref',
          where   {ref($_) eq 'HASH' },
          message { 'Must be a hash reference (HashRef)' };

        subtype 'CodeRef' =>
          as      'Ref',
          where   { ref($_) eq 'CODE' },
          message { 'Must be a code reference (CodeRef)' };

        subtype 'RegexpRef' =>
          as      'Ref',
          where   { ref($_) eq 'Regexp' },
          message { 'Must be a Regexp' };

        subtype 'GlobRef' =>
          as      'Ref',
          where   { ref($_) eq 'GLOB' },
          message { 'Must be a GlobRef (GLOB)' };
        
          subtype 'FileHandle'  =>
            as      'GlobRef',
            where   { openhandle($_) },
            message { 'Must be a FileHandle' };
            
        subtype 'Object'  =>
          as      'Ref',
          where   { no strict 'refs'; scalar(@{ref($_) . "::ISA"}) },
          message { 'Must be an object' };

1;# return true:



=pod

=head1 NAME

VSO - Very Simple Objects

=head1 SYNOPSIS

Basic point example:

  package Plane;
  use VSO;
  
  has 'width' => (
    is        => 'ro',
    isa       => 'Int',
  );
  
  has 'height' => (
    is        => 'ro',
    isa       => 'Int',
  );
  
  has 'points' => (
    is        => 'rw',
    isa       => 'ArrayRef[Point2d]',
    required  => 0,
  );


  package Point2d;
  use VSO;
  
  subtype 'ValidValue'
    => as      'Int',
    => where   { $_ >= 0 && $_ <= shift->plane->width },
    => message { 'Value must be between zero and ' . shift->plane->width };
  
  has 'plane' => (
    is        => 'ro',
    isa       => 'Plane',
    weak_ref  => 1,
  );
  
  has 'x' => (
    is        => 'rw',
    isa       => 'ValidValue'
  );
  
  has 'y' => (
    is        => 'rw',
    isa       => 'ValidValue'
  );
  
  after 'x' => sub {
    my ($s, $new_value, $old_value) = @_;
    warn "Moving $s from x$old_value to x$new_value";
  };
  
  after 'y' => sub {
    my ($s, $new_value, $old_value) = @_;
    warn "Moving $s from y$old_value to y$new_value";
  };

Fancy 3D Point:

  package Point3d;
  use VSO;
  
  extends 'Point2d';
  
  has 'z' => (
    is      => 'rw',
    isa     => 'Int',
  );

  sub greet { warn "Hello, World!" }
  
  before 'greet' => sub {
    warn "About to greet you";
  };
  
  after 'greet' => sub {
    warn "I have greeted you";
  };


Enums:

  package Foo;
  use VSO;

  enum 'DayOfWeek' => [qw( Sun Mon Tue Wed Thu Fri Sat )];

  has 'day' => (
    is        => 'ro',
    isa       => 'DayOfWeek',
    required  => 1,
  );

Coercions and Subtypes:

  package Ken;
  use VSO;

  subtype 'Number::Odd'
    => as 'Int'
    => where { $_ % 2 }
    => message { "$_ is not an odd number: %=:" . ($_ % 2) };

  subtype 'Number::Even'
    => as 'Int'
    => where { (! $_) || ( $_ % 2 == 0 ) }
    => message { "$_ is not an even number" };

  coerce 'Number::Odd'
    => from 'Int'
    => via  { $_++ };

  coerce 'Number::Even'
    => from 'Int'
    => via { $_++ };

  has 'favorite_number' => (
    is        => 'ro',
    isa       => 'Number::Odd',
    required  => 1,
    coerce    => 1, # Otherwise no coercion is performed.
  );

  ...

  my $ken = Ken->new( favorite_number => 3 ); # Works
  my $ken = Ken->new( favorite_number => 6 ); # Works, because of coercion.


=head1 DESCRIPTION

VSO aims to offer a declarative OO style for Perl with very little overhead, without
being overly-minimalist.

VSO is a simplified Perl5 object type system I<similar> to L<Moose>, but simpler.

=head2 TYPES

VSO offers the following type system:

  Any
    Item
        Bool
        Undef
        Defined
            Value
                Str
                    Num
                        Int
                    ClassName
            Ref
                ScalarRef
                ArrayRef
                HashRef
                CodeRef
                RegexpRef
                GlobRef
                    FileHandle
                Object

Missing from the Moose type system are:

=over 4

=item Maybe[`a]

If it's a 'Maybe[whatever]', just do C<< required => 0 >>

I<*This might change...>

=item RoleName

VSO does not currently support 'roles'.

=back

=head1 AUTHOR

John Drago <jdrago_999@yahoo.com>

=head1 LICENSE

This software is Free software and may be used and redistributed under the same
terms as perl itself.

=cut

