package Terse::Controller;
use strict;
use warnings;
use attributes ();
use base 'Terse';
use B 'svref_2object';

our %HTTP;
BEGIN { %HTTP = map { $_ => 1 } qw/get post put delete connect head options trace patch/; }

sub new {
	my ($pkg, @args) = @_;
	my $self = $pkg->SUPER::new(
		app => 0,
		restrict_path => '/',
		@args
	);
	(my $namespace = $pkg) =~ s/^.*Controller:://;
	$namespace =~ s/\:\:/\//g;
	$self->namespace = lc( $namespace );
	$self->default_req = [split "/", $self->namespace]->[-1];
	$self->build_controller() if ($self->can('build_controller'));
	return $self;
}

sub build_terse {
	my ($self, $t) = @_;
	if ($self->models) {
		$t->{model} = sub {
			my ($t, $model) = @_;
			$t->raiseError("invalid model: ${model}", 400) unless $self->models->$model;
			return $t->models->$model ||= $self->models->$model->connect();
		};
	}
	if ($self->controllers) {
		$t->{controller} = sub {
			my ($t, $controller) = @_;

			return $self->controllers->{$controller} if $self->controllers->{$controller};
			for my $key (keys %{$self->controllers->{_alias}}) {
				my @captured = $controller =~ m/$key/;
				if (scalar @captured) {
					$controller = $self->controllers->{$self->controllers->{_alias}->{$key}};
					$t->captured = \@captured;
					return $controller;
				}
			}
			return;
		};
	}
	if ($self->views) {
		$t->{view} = sub {
			my ($t, $view) = @_;
			$t->raiseError("invalid view: ${view}", 400) unless $self->views->{$view};
			return $self->views->{$view};
		};
	}
	return $t;
}

sub MODIFY_CODE_ATTRIBUTES {
	my ($package, $coderef, @attributes, @disallowed) = @_;
	my $name = svref_2object($coderef)->GV->NAME;
	my %attr = PARSE_ATTRIBUTES($name, @attributes);
	push @{ $Terse::Controller::dispatcher{$package}{$attr{req}} }, \%attr;
	return ();
}

sub FETCH_CODE_ATTRIBUTES {
	my ($class, $coderef) = @_;
	my $cv = svref_2object($coderef);
	return @{$Terse::Controller::dispatcher{$class}{ $cv->GV->NAME }};
}

sub PARSE_ATTRIBUTES {
	my ($sub, @attributes) = @_;
	my %attr = (
		req => $sub,
		callback => $sub
	);
	for my $attribute (@attributes) {
		if ($attribute =~ m/^\s*params\((.*)\)\s*$/) {
			$attr{params} = { eval $1 };
		}
		elsif ($attribute =~ m/^\s*([^\s\(]+)\s*\(([\s\'\"]*?)(.*)([\s\'\"]*?)\)/) {
			my $k = lc($1);
			$attr{$k} = $3;
			if ($HTTP{$k}) {
				$attr{req} = $3;
			}	
		}
		else {
			$attr{lc($attribute)} = 1; 
		}
	}
	return %attr;
}

sub response_handle {
	my ($self, $t, $response_body, $cookie, $status) = @_;
	my $res = $t->request->new_response($response_body->status_code ||= $status ||= 200);
	$res->cookies->{sid} = $cookie if $cookie;
	my ($content_type, $body) = $self->views->{$self->response_view} 
		? $t->view($self->response_view)->render($t, $response_body) 
		: ('application/json', $response_body->serialize());
	$res->content_type($content_type);
	$res->body($body);
	return $res->finalize;
}

sub dispatch {
	my ($self, $req, $t, @params) = @_;
	my $package = ref $self || $self;
	my $dispatcher = $Terse::Controller::dispatcher{$package}{$req};
	if (!$dispatcher) {
		$t->logError('Invalid dispatch request', 400);
		return;
	}
	if (!$self->{app} && $t->request->uri->path ne $self->restrict_path) {
		$t->logError('Invalid path', 500);
		return;
	}
	my $dispatch;
	DISPATCH: for my $candidate (reverse @{$dispatcher}) {
		next DISPATCH unless ($candidate->{lc($t->request->method)} || $candidate->{any});
		if ($candidate->{params}) {
			for my $param (keys %{$candidate->{params}}) {
				next DISPATCH if (!$t->params->{$param});
				next DISPATCH unless $self->_partial_match(
					$t->params->{$param}, 
					$candidate->{params}->{$param}
				);
			}
		}
		$dispatch = $candidate;
		last;
	}
	$dispatch = $self->dispatch_hook($dispatch) if $self->can('dispatch_hook'); 
	my $callback = $dispatch->{callback};
	if (!$callback) {
		$t->logError('No callback found to dispatch the request', 400);
		return;
	}
	return $self->$callback($t, @params);
}

sub _partial_match {
	my ($self, $param, $spec) = @_;
	return 0 if !$param && $spec;
	my ($ref, $match) = (ref $spec, 1);
	if (!$ref) {
		$match = ref $param ? 0 : $param =~ m/^$spec$/;
	} elsif ($ref eq 'ARRAY') {
		for (my $i = 0; $i < scalar @{$spec}; $i++) {
			$match = $self->_partial_match($param->[$i], $spec->[$i]);
			last if (!$match);
		}
	} elsif ($ref eq 'HASH') {
		for my $key ( keys %{$spec} ) {
			$match = $self->_partial_match($param->{$key}, $spec->{$key});
			last if (!$match);
		}
	}
	return $match;
}

1;

__END__;


=head1 NAME

Terse::Controller - controllers made simple.

=head1 VERSION

Version 0.103

=cut

=head1 SYNOPSIS

	package Stocks;

	use base 'Terse::Controller';

	sub login :any {
		return 1;
	}

	sub auth_prevent :any(auth) {
		return 0;
	}

	sub auth :get :post {
		return 1;
	}

	sub purchase :get {
		... #1
	}
	
	sub purchase_virtual :get(purchase) :params(virtual => 1) {
		... #2
	}

	sub purchase_post :post(purchase) {
		... # 3
	}

	sub purchase_virtual_post :post(purchase) :params(virtual => 1) {
		... # 4
	}

	1;

	.... psgi ...

	use Terse;
	use Stocks;
	our $api = Stocks->new();

	sub {
		my ($env) = (shift);
		Terse->run(
			plack_env => $env,
			application => $api,
		);
	};

	....

	plackup Stocks.psgi

	GET http://localhost:5000/?req=purchase  #1
	POST http://localhost:5000/ {"req":"purchase"} #3

	GET http://localhost:5000/?req=purchase&virtual=1 #2
	POST http://localhost:5000/ {"req":"purchase", "virtual":1} #4

=cut

=head1 AUTHOR

LNATION, C<< <email at lnation.org> >>

=head1 LICENSE AND COPYRIGHT

L<Terse>.

=cut
