package Reaction::UI::Skin;

use Reaction::Class;

# declaring dependencies
use Reaction::UI::LayoutSet;
use Reaction::UI::RenderingContext;
use File::ShareDir;
use File::Basename;
use Config::Any;

use aliased 'Path::Class::Dir';

use namespace::clean -except => [ qw(meta) ];


has '_layout_set_cache'   => (is => 'ro', default => sub { {} });
has '_widget_class_cache'   => (is => 'ro', default => sub { {} });

has 'name' => (is => 'ro', isa => 'Str', required => 1);
has 'skin_dir' => (is => 'rw', isa => Dir, lazy_fail => 1);

has 'widget_search_path' => (
  is => 'rw', isa => 'ArrayRef', required => 1, default => sub { [] }
);

has 'view' => (
  is => 'ro', required => 1, weak_ref => 1,
  handles => [ qw(layout_set_class) ],
);

has 'super' => (
  is => 'rw', isa => Skin, required => 0, predicate => 'has_super',
);

sub BUILD {
  my ($self, $args) = @_;
  $self->_find_skin_dir($args);
  $self->_load_skin_config($args);
}
sub _find_skin_dir {
  my ($self, $args) = @_;
  my $skin_name = $self->name;
  if ($skin_name =~ s!^/(.*?)/!!) {
    my $dist = $1;
    $args->{skin_base_dir} = eval {
        Dir->new(File::ShareDir::dist_dir($dist))
           ->subdir('skin');
    };
    if ($@) {
        # No installed Reaction
        my $file = __FILE__;
        my $dir = Dir->new(dirname($file));
        my $skin_base;
        while ($dir->parent) {
            if (-d $dir->subdir('share') && -d $dir->subdir('share')->subdir('skin')) {
                $skin_base = $dir->subdir('share')->subdir('skin');
                last;
            }
            $dir = $dir->parent;
        }
        confess "could not find skinbase by recursion. ended up at $dir, from $file"
          unless $skin_base;
        $args->{skin_base_dir} = $skin_base; 
    }
  }
  my $base = $args->{skin_base_dir}->subdir($skin_name);
  confess "No such skin base directory ${base}"
    unless -d $base;
  $self->skin_dir($base);
};
sub _load_skin_config {
  my ($self, $args) = @_;
  my $base = $self->skin_dir;
  my $lst = sub { (ref $_[0] eq 'ARRAY') ? $_[0] : [$_[0]] };
  my @files = (
    $args->{skin_base_dir}->file('defaults.conf'), $base->file('skin.conf')
  );
  # we get [ { $file => $conf }, ... ]
  my %cfg = (map { %{(values %{$_})[0]} }
              @{Config::Any->load_files({
                files => [ grep { -e $_ } @files ],
                use_ext => 1,
              })}
            );
  if (my $super_name = $cfg{extends}) {
    my $super = $self->new(
      name => $super_name,
      view => $self->view,
      skin_base_dir => $args->{skin_base_dir},
    );
    $self->super($super);
  }
  if (exists $cfg{widget_search_path}) {
    $self->widget_search_path($lst->($cfg{widget_search_path}));
  }
  # For some reason this conditional doesn't work correctly without
  # the "my @x". Answers on a postcard.
  unless (my @x = $self->full_widget_search_path) {
    confess "No widget_search_path in defaults.conf or skin.conf"
            .($self->has_super
              ? " and no search path provided from super skin "
                .$self->super->name
              : "");
  }
}
sub create_layout_set {
  my ($self, $name) = @_;
  $self->_create_layout_set($name, [], $self);
};
sub _create_layout_set {
  my ($self, $name, $tried, $top_skin) = @_;
  if (my $path = $self->layout_path_for($name)) {
    return $self->layout_set_class->new(
             $self->layout_set_args_for($name),
             source_file => $path,
             top_skin => $top_skin,
           );
  }
  $tried = [ @{$tried}, $self->our_path_for_type('layout') ];
  if ($self->has_super) {
    return $self->super->_create_layout_set($name, $tried, $top_skin);
  }
  confess "Couldn't find layout set file for ${name}, tried "
          .join(', ', @$tried);
};
sub layout_set_args_for {
  my ($self, $name) = @_;
  return (
    name => $name,
    skin => $self,
    ($self->has_super ? (next_skin => $self->super) : ()),
    $self->view->layout_set_args_for($name),
  );
};
sub layout_path_for {
  my ($self, $layout) = @_;
  my $file_name = join(
    '.', $layout, $self->view->layout_set_file_extension
  );
  my $path = $self->our_path_for_type('layout')
                  ->file($file_name);
  return (-e $path ? $path : undef);
};
sub search_path_for_type {
  my ($self, $type) = @_;
  return [
    $self->our_path_for_type($type),
    ($self->has_super
      ? @{$self->super->search_path_for_type($type)}
      : ()
    )
  ];
};
sub our_path_for_type {
  my ($self, $type) = @_;
  return $self->skin_dir->subdir($type)
};
sub full_widget_search_path {
  my ($self) = @_;
  return (
    @{$self->widget_search_path},
    ($self->has_super ? $self->super->full_widget_search_path : ())
  );
};
sub widget_class_for {
  my ($self, $layout_set) = @_;
  my $base = blessed($self);
  my $widget_type = $layout_set->widget_type;
  return $self->_widget_class_cache->{$widget_type} ||= do {

    my @search_path = $self->full_widget_search_path;
    my @haystack = map {join('::', $_, $widget_type)} @search_path;

    foreach my $class (@haystack) {
      #if the class is already loaded skip the call to Installed etc.
      return $class if Class::MOP::is_class_loaded($class);
      next unless Class::Inspector->installed($class);

      my $ok = eval { Class::MOP::load_class($class) };
      confess("Failed to load widget '${class}': $@") if $@;
      return $class;
    }
    confess "Couldn't locate widget '${widget_type}' for layout "
      ."'${\$layout_set->name}': tried: ".join(", ", @haystack);
  };
};

__PACKAGE__->meta->make_immutable;


1;
