The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#line 1
package TestML::Runtime;
use TestML::Mo;

use TestML::Compiler;

# Since there is only ever one test runtime, it makes things a LOT cleaner to
# keep the reference to it in a global variable accessed by a method, than to
# put a reference to it into every object that needs to access it.
our $self;

has base => default => sub {$0 =~ m!(.*)/! ? $1 : "."};   # Base directory
has testml => ();       # TestML document filename, handle or text
has bridge => ();       # Bridge transform module

# XXX Add TestML.pm support for -library keyword.
has library => default => sub {[]};    # Transform library modules

has function => ();         # Current function executing
has planned => default => sub {0};     # plan() has been called
has test_number => default => sub {0}; # Number of tests run so far

sub BUILD {
    my $self = $TestML::Runtime::self = shift;
    $self->function($self->compile_testml);
    $self->load_variables;
    $self->load_transform_module('TestML::Library::Standard');
    $self->load_transform_module('TestML::Library::Debug');
    if ($self->bridge) {
        $self->load_transform_module($self->bridge);
    }
}

# XXX Move to TestML::Adapter
sub title { }
sub plan_begin { }
sub plan_end { }

sub run {
    my $self = shift;

    my $function = $self->function;
    my $context = TestML::None->new;
    my $args = [];

    $self->run_function($self->function, $context, $args);

    $self->run_plan();
    $self->plan_end();
}

# XXX - TestML exception handling needs to happen at the function level, not
# just at the expression level. Not yet handled here.
sub run_function {
    my $self = shift;
    my $function = shift;
    my $context = shift;
    my $args = shift;

    my $signature = $function->signature;
    die sprintf(
        "Function received %d args but expected %d",
        scalar(@$args),
        scalar(@$signature),
    ) if @$signature and @$args != @$signature;
    $function->setvar('Self', $context);
    for (my $i = 0; $i < @$signature; $i++) {
        my $arg = $args->[$i];
        $arg = $self->run_expression($arg)
            if ref($arg) eq 'TestML::Expression';
        $function->setvar($signature->[$i], $arg);
    }

    my $parent = $self->function;
    $self->function($function);

    for my $statement (@{$function->statements}) {
        $self->run_statement($statement);
    }

    $self->function($parent);

    return TestML::None->new;
}

sub run_statement {
    my $self = shift;
    my $statement = shift;
    my $blocks = @{$statement->points}
        ? $self->select_blocks($statement->points)
        : [1];
    for my $block (@$blocks) {
        $self->function->setvar('Block', $block) if ref($block);
        my $context = $self->run_expression($statement->expression);
        if (my $assertion = $statement->assertion) {
            $self->run_assertion($context, $assertion);
        }
    }
}

sub run_assertion {
    my $self = shift;
    my $left = shift;
    my $assertion = shift;
    my $method = 'assert_' . $assertion->name;

    # Run this as late as possible.
    $self->run_plan;

    $self->test_number($self->test_number + 1);
    $self->function->setvar(
        TestNumber => TestML::Num->new(value => $self->test_number),
    );

    # TODO - Should check 
    my $results = ($left->type eq 'List')
        ? $left->value
        : [ $left ];
    for my $result (@$results) {
        if (@{$assertion->expression->units}) {
            my $right = $self->run_expression($assertion->expression);
            my $matches = ($right->type eq 'List')
                ? $right->value
                : [ $right ];
            for my $match (@$matches) {
                $self->$method($result, $match);
            }
        }
        else {
            $self->$method($result);
        }
    }
}

sub run_expression {
    my $self = shift;
    my $prev_expression = $self->function->expression;
    my $expression = shift;
    $self->function->expression($expression);

    my $units = $expression->units;
    my $context = TestML::None->new;

    for (my $i = 0; $i < @$units; $i++) {
        my $unit = $units->[$i];
        if ($expression->error) {
            next unless
                $unit->isa('TestML::Transform') and
                $unit->name eq 'Catch';
        }
        if ($unit->isa('TestML::Object')) {
            $context = $unit;
            next;
        }
        if ($unit->isa('TestML::Function')) {
            $context = $unit;
            next;
        }
        die "Unexpected unit: $unit" unless $unit->isa('TestML::Transform');
        my $callable = $self->function->getvar($unit->name)
            or die "Can't find transform '${\$unit->name}'";
        my $args = $unit->args;
        if ($callable->isa('TestML::Native')) {
            $context = $self->run_native($callable->value, $context, $args);
        }
        elsif ($callable->isa('TestML::Object')) {
            $context = $callable;
        }
        elsif ($callable->isa('TestML::Function')) {
            if ($i or $unit->explicit_call) {
                my $points = $self->function->getvar('Block')->points;
                for my $key (keys %$points) {
                    $callable->setvar($key, TestML::Str->new(value => $points->{$key}));
                }
                $context = $self->run_function($callable, $context, $args);
            }
            $context = $callable;
        }
        else {
            ZZZ $expression, $unit, $callable;
        }
    }
    if ($expression->error) {
        die $expression->error;
    }
    $self->function->expression($prev_expression);
    return $context;
}

sub run_native {
    my $self = shift;
    my $function = shift;
    my $context = shift;
    my $args = shift;
    my $value = eval {
        &$function(
            $context,
            map {
                (ref($_) eq 'TestML::Expression')
                ? $self->run_expression($_)
                : $_
            } @$args
        );
    };
    if ($@) {
        $self->function->expression->error($@);
        $context = TestML::Error->new(value => $@);
    }
    elsif (UNIVERSAL::isa($value, 'TestML::Object')) {
        $context = $value;
    }
    else {
        $context = $self->object_from_native($value);
    }
    return $context;
}

sub select_blocks {
    my $self = shift;
    my $wanted = shift;
    my $selected = [];

    OUTER: for my $block (@{$self->function->data}) {
        my %points = %{$block->points};
        next if exists $points{SKIP};
        for my $point (@$wanted) {
            next OUTER unless exists $points{$point};
        }
        if (exists $points{ONLY}) {
            @$selected = ($block);
            last;
        }
        push @$selected, $block;
        last if exists $points{LAST};
    }
    return $selected;
}

sub object_from_native {
    my $self = shift;
    my $value = shift;
    return
        not(defined $value) ? TestML::None->new :
        ref($value) eq 'ARRAY' ? TestML::List->new(value => $value) :
        $value =~ /^-?\d+$/ ? TestML::Num->new(value => $value + 0) :
        "$value" eq "$TestML::Constant::True" ? $value :
        "$value" eq "$TestML::Constant::False" ? $value :
        "$value" eq "$TestML::Constant::None" ? $value :
        TestML::Str->new(value => $value);
}

sub compile_testml {
    my $self = shift;
    my $path = ref($self->testml)
        ? $self->testml
        : join '/', $self->base, $self->testml;
    my $function = TestML::Compiler->new(base => $self->base)->compile($path)
        or die "TestML document failed to compile";
    return $function;
}

sub load_variables {
    my $self = shift;
    my $global = $self->function->outer;
    $global->setvar(Block => TestML::Block->new);
    $global->setvar(Label => TestML::Str->new(value => '$BlockLabel'));
    $global->setvar(True => $TestML::Constant::True);
    $global->setvar(False => $TestML::Constant::False);
    $global->setvar(None => $TestML::Constant::None);
}

sub load_transform_module {
    my $self = shift;
    my $module_name = shift;
    if ($module_name ne 'main') {
        eval "require $module_name; 1"
            or die "Can't use $module_name:\n$@";
    }

    my $global = $self->function->outer;
    no strict 'refs';
    for my $key (sort keys %{"$module_name\::"}) {
        next if $key eq "\x16";
        my $glob = ${"$module_name\::"}{$key};
        if (my $function = *$glob{CODE}) {
            $global->setvar(
                $key => TestML::Native->new(value => $function),
            );
        }
        elsif (my $object = *$glob{SCALAR}) {
            if (ref($$object)) {
                $global->setvar($key => $$object);
            }
        }
    }
}

sub get_label {
    my $self = shift;
    my $label = $self->function->getvar('Label')->value;
    sub label {
        my $self = shift;
        my $var = shift;
        my $block = $self->function->getvar('Block');
        return $block->label if $var eq 'BlockLabel';
        if (my $v = $block->points->{$var}) {
            $v =~ s/\n.*//s;
            $v =~ s/^\s*(.*?)\s*$/$1/;
            return $v;
        }
        if (my $v = $self->function->getvar($var)) {
            return $v->value;
        }
    }
    $label =~ s/\$(\w+)/label($self, $1)/ge;
    return $label ? ($label) : ();
}

sub run_plan {
    my $self = shift;
    if (! $self->planned) {
        $self->title();
        $self->plan_begin();
        $self->planned(1);
    }
}

sub get_error {
    my $self = shift;
    return $self->function->expression->error;
}

sub clear_error {
    my $self = shift;
    return $self->function->expression->error(undef);
}

sub throw {
    require Carp;
    Carp::croak $_[1];
}

#-----------------------------------------------------------------------------
package TestML::Function;
use TestML::Mo;

has type => default => sub {'Func'};        # Functions are TestML typed objects
# XXX Make this a featherweight reference.
has signature => default => sub {[]};       # Input variable names
has namespace => default => sub {{}};       # Lexical scoped variable stash
has statements => default => sub {[]};      # Exexcutable code statements
has data => default => sub{[]};             # Data section scoped to this function

# Runtime pointers to current objects.
has expression => ();
has block => ();

my $outer = {};
sub outer { @_ == 1 ? $outer->{$_[0]} : ($outer->{$_[0]} = $_[1]) }

sub getvar {
    my $self = shift;
    my $name = shift;
    while ($self) {
        if (my $object = $self->namespace->{$name}) {
            return $object;
        }
        $self = $self->outer;
    }
    return;
}

sub setvar {
    my $self = shift;
    my $name = shift;
    my $object = shift;
    $self->namespace->{$name} = $object;
    return;
}

sub forgetvar {
    my $self = shift;
    my $name = shift;
    delete $self->namespace->{$name};
    return;
}

#-----------------------------------------------------------------------------
package TestML::Statement;
use TestML::Mo;

has expression => default => sub {TestML::Expression->new};
has assertion => ();
has points => default => sub {[]};

#-----------------------------------------------------------------------------
package TestML::Expression;
use TestML::Mo;

has units => default => sub {[]};
has error => ();

#-----------------------------------------------------------------------------
package TestML::Assertion;
use TestML::Mo;

has name => ();
has expression => default => sub {TestML::Expression->new};

#-----------------------------------------------------------------------------
package TestML::Transform;
use TestML::Mo;

has name => ();
has args => default => sub {[]};
has explicit_call => default => 0;

#-----------------------------------------------------------------------------
package TestML::Block;
use TestML::Mo;

has label => default => sub {''};
has points => default => sub {{}};

#-----------------------------------------------------------------------------
package TestML::Object;
use TestML::Mo;

has value => ();

sub type {
    my $type = ref(shift);
    $type =~ s/^TestML::// or die "Can't find type of '$type'";
    return $type;
}

sub runtime { return $TestML::Runtime::self }

sub str { my $t = $_[0]->type; die "Cast from $t to Str is not supported" }
sub num { my $t = $_[0]->type; die "Cast from $t to Num is not supported" }
sub bool { my $t = $_[0]->type; die "Cast from $t to Bool is not supported" }
sub list { my $t = $_[0]->type; die "Cast from $t to List is not supported" }
sub none { $TestML::Constant::None }

#-----------------------------------------------------------------------------
package TestML::Str;
use TestML::Mo;
extends 'TestML::Object';

sub str { shift }
sub num { TestML::Num->new(
    value => ($_[0]->value =~ /^-?\d+(?:\.\d+)$/ ? ($_[0]->value + 0) : 0),
)}
sub bool {
    length($_[0]->value) ? $TestML::Constant::True : $TestML::Constant::False
}
sub list { TestML::List->new(value => [split //, $_[0]->value]) }

#-----------------------------------------------------------------------------
package TestML::Num;
use TestML::Mo;
extends 'TestML::Object';

sub str { TestML::Str->new(value => $_[0]->value . "") }
sub num { shift }
sub bool { ($_[0]->value != 0) ? $TestML::Constant::True : $TestML::Constant::False }
sub list {
    my $list = [];
    $#{$list} = int($_[0]) -1;
    TestML::List->new(value =>$list);
}

#-----------------------------------------------------------------------------
package TestML::Bool;
use TestML::Mo;
extends 'TestML::Object';

sub str { TestML::Str->new(value => $_[0]->value ? "1" : "") }
sub num { TestML::Num->new(value => $_[0]->value ? 1 : 0) }
sub bool { shift }

#-----------------------------------------------------------------------------
package TestML::List;
use TestML::Mo;
extends 'TestML::Object';
sub list { shift }

#-----------------------------------------------------------------------------
package TestML::None;
use TestML::Mo;
extends 'TestML::Object';

sub str { Str('') }
sub num { Num(0) }
sub bool { $TestML::Constant::False }
sub list { List([]) }

#-----------------------------------------------------------------------------
package TestML::Error;
use TestML::Mo;
extends 'TestML::Object';

#-----------------------------------------------------------------------------
package TestML::Native;
use TestML::Mo;
extends 'TestML::Object';

package TestML::Constant;

our $True = TestML::Bool->new(value => 1);
our $False = TestML::Bool->new(value => 0);
our $None = TestML::None->new;