
use v5.10;
use strict;
use warnings;

package Context::Singleton::Frame;

our $VERSION = v1.0.5;

use List::Util;
use Scalar::Util;

use Context::Singleton::Frame::DB;
use Context::Singleton::Exception::Invalid;
use Context::Singleton::Exception::Deduced;
use Context::Singleton::Exception::Nondeducible;
use Context::Singleton::Frame::Promise;
use Context::Singleton::Frame::Promise::Builder;
use Context::Singleton::Frame::Promise::Rule;

use overload (
	'""' => sub { ref ($_[0]) . '[' . $_[0]->{depth} . ']' },
	fallback => 1,
);

sub new {
	my ($class, %proclaim) = @_;
	my $self = {
		promises    => {},
		depth       => 0,
		db          => $class->default_db_instance,
	};

	if (ref $class) {
		$self->{root}   = $class->{root};
		$self->{parent} = $class;
		$self->{db}     = $class->{db};
		$self->{depth}  = $class->{depth} + 1;

		$class = ref $class;
	}

	unless ($self->{root}) {
		$self->{root} = $self;
		Scalar::Util::weaken $self->{root};
	}

	$self = bless $self, $class;

	$self->proclaim (%proclaim);

	return $self;
}

sub depth {
	$_[0]->{depth};
}

sub parent {
	$_[0]->{parent};
}

sub default_db_class {
	'Context::Singleton::Frame::DB';
}

sub default_db_instance {
	$_[0]->default_db_class->instance;
}

sub db {
	$_[0]->{db};
}

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

	my $sub = (caller(1))[3];
	$sub =~ s/^.*://;

	use feature 'say';
	say "# [${\ $self->depth}] $sub ${\ join ' ', @message }";
}

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

	my $promise = $self->_class_builder_promise->new (
		depth   => $self->depth,
		builder => $builder,
	);

	my %optional = $builder->default;
	my %required = map +($_ => 1), $builder->required;
	delete @required{ keys %optional };

	$promise->add_dependencies (
		map $self->_search_promise_for ($_), keys %required
	);

	$promise->set_deducible (0) unless keys %required;

	$promise->listen ($self->_search_promise_for ($_))
		for keys %optional;

	$promise;
}

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

	$self->{promises}{$rule} // do {
		my $promise = $self->{promises}{$rule} = $self->_class_rule_promise->new (
			depth => $self->depth,
			rule => $rule,
		);

		$promise->add_dependencies ($self->parent->_search_promise_for ($rule))
			if $self->parent;

		for my $builder ($self->db->find_builder_for ($rule)) {
			$promise->add_dependencies (
				$self->_build_builder_promise_for ($builder)
			);
		}

		$promise;
	};
}

sub _class_builder_promise {
	'Context::Singleton::Frame::Promise::Builder';
}

sub _class_rule_promise {
	'Context::Singleton::Frame::Promise::Rule';
}

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

	my $promise = $self->_search_promise_for( $rule );
	return $promise->value if $promise->is_deduced;

	my $builder_promise = $promise->deducible_builder;
	return $builder_promise->value if $builder_promise->is_deduced;

	my $builder = $builder_promise->builder;
	my %deduced = $builder->default;

	for my $dependency ($builder->required) {
		# dependencies with default values may not be deducible
		# relying on promises to detect deducible values
		next unless $self->is_deducible( $dependency );

		$deduced{$dependency} = $self->deduce ($dependency);
	}

	$builder->build (\%deduced);
}

sub _execute_triggers {
	my ($self, $rule, $value) = @_;

	$_->($value) for $self->db->find_trigger_for ($rule);
}

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

	$self->{promises}{$rule};
}

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

	return if $depth < 0;

	my $distance = $self->depth - $depth;
	return if $distance < 0;

	my $found = $self;

	$found = $found->parent
		while $distance-- > 0;

	$found;
}

sub _root_frame {
	$_[0]->{root};
}

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

	$self->_find_promise_for ($rule)
		// $self->_build_rule_promise_for ($rule)
		;
}

sub _set_promise_value {
	my ($self, $promise, $value) = @_;

	$promise->set_value ($value, $self->depth);
	$self->_execute_triggers ($promise->rule, $value);

	$value;
}

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

	throw Context::Singleton::Exception::Deduced ($rule);
}

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

	throw Context::Singleton::Exception::Nondeducible ($rule);
}

sub contrive {
	my ($self, $rule, @how) = @_;

	$self->db->contrive ($rule, @how);
}

sub load_rules {
	shift->db->load_rules (@_);
}

sub trigger {
	shift->db->trigger (@_);
}

sub deduce {
	my ($self, $rule, @proclaim) = @_;

	$self = $self->new (@proclaim) if @proclaim;

	$self->_throw_nondeducible ($rule)
		unless $self->try_deduce ($rule);

	$self->_find_promise_for ($rule)->value;
}

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

	return unless my $promise = $self->_find_promise_for ($rule);
	return $promise->is_deduced;
}

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

	return unless my $promise = $self->_search_promise_for ($rule);
	return $promise->is_deducible;
}

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

	return unless @proclaim;

	my $retval;
	while (@proclaim) {
		my $key = shift @proclaim;
		my $value = shift @proclaim;

		my $promise = $self->_find_promise_for ($key)
			// $self->_build_rule_promise_for ($key)
			;

		$self->_throw_deduced ($key)
			if $promise->is_deduced;

		$retval = $self->_set_promise_value ($promise, $value);
	}

	$retval;
}

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

	my $promise = $self->_search_promise_for ($rule);
	return unless $promise->is_deducible;

	my $value = $self
		->_frame_by_depth ($promise->deduced_in_depth)
		->_deduce_rule ($promise->rule)
		;

	$promise->set_value ($value, $promise->deduced_in_depth);

	1;
}

1;

__END__

