# Copyright © 2010 Julian Blake Kongslie <jblake@omgwallhack.org>
# Licensed under the BSD 3-clause license.

use strict;
use warnings;

package Piny::Config;

use Moose;
use Moose::Util::TypeConstraints;
use MooseX::StrictConstructor;

use Carp;
use Config::GitLike;

use Piny::Environment;

# Types

subtype 'GitBool'
  => as 'Str'
  => where { $_ =~ /^(1|0|true|false)$/i }
  => message { 'Not correct format for a git-compatible boolean.' }
  ;

subtype 'Path'
  => as 'Str'
  => where { $_ =~ /^\// and -e $_ }
  => message { 'Not an absolute path, or does not exist.' }
  ;

subtype 'PathDir'
  => as 'Path'
  => where { -d $_ }
  => message { 'Not an absolute path, or not a directory.' }
  ;

subtype 'HttpUrl'
  => as 'Str'
  => where { $_ =~ /^(http|https):\/\//i }
  => message { 'Not a http:// or https:// URL.' }
  ;

subtype 'HttpsUrl'
  => as 'Str'
  => where { $_ =~ /^https:\/\//i }
  => message { 'Not a https:// URL.' }
  ;

subtype 'RepoPermission'
  => as 'Str'
  => where { $_ eq "0666" or $_ eq "0664" or $_ eq "0660" or $_ eq "0640" or $_ eq "group" or $_ eq "true" or $_ eq "false" or $_ eq "all" or $_ eq "everybody" or $_ eq "world" or $_ eq "1" or $_ eq "2" }
  => message { 'Must be one of 0666, 0664 (or all, everybody, world), 0660 (or true, 1, group), or 0640 (or false, 0).' }
  ;

# Attributes

has 'confpath' =>
  ( is          => 'ro'
  , isa         => 'Str'
  , predicate   => 'has_confpath'
  );

has '_conf' =>
  ( is          => 'ro'
  , isa         => 'HashRef[Str]'
  , lazy_build  => 1
  , clearer     => 'clear_conf'
  , init_arg    => undef
  );

# Builder methods

# If constructed with just one argument, then treat it as a config path.
around BUILDARGS => sub {
  my ( $orig, $class ) = ( shift, shift );

  if ( @_ == 1 && ! ref $_[0] ) {
    return $class->$orig( confpath => $_[0] );
  } else {
    return $class->$orig( @_ );
  };
};

sub _build__conf {
  my ( $s ) = @_;

  my $conf;

  if ( $s->has_confpath and -s $s->confpath ) {
    $conf = Config::GitLike->load_file( $s->confpath );
  } else {
    $conf = { };
  };

  my $home = Piny::Environment->instance->user->home . "/.gitconfig";

  if ( -s $home ) {

    my $userconf = Config::GitLike->load_file( $home );

    foreach my $key ( keys %$userconf ) {
      if ( not exists $conf->{$key} ) {
        $conf->{$key} = $userconf->{$key};
      };
    };

  };

  if ( -s "/etc/piny-default.conf" ) {

    my $default = Config::GitLike->load_file( "/etc/piny-default.conf" );

    foreach my $key ( keys %$default ) {
      if ( not exists $conf->{$key} ) {
        $conf->{$key} = $default->{$key};
      };
    };

  };

  if ( -s "/etc/piny-override.conf" ) {

    my $override = Config::GitLike->load_file( "/etc/piny-override.conf" );

    foreach my $key ( keys %$override ) {
      $conf->{$key} = $override->{$key};
    };

  };

  foreach my $key ( keys %$conf ) {
    $conf->{$key} = "" unless defined $conf->{$key};
  };

  return $conf;
};

# Save the config

sub save {
  my ( $s ) = @_;

  if ( not $s->has_confpath ) {
    croak "Can't save a Piny::Config if the confpath is not set!";
  };

  my ( $override, $userconf, $default ) = ( {}, {}, {} );

  if ( -s "/etc/piny-override.conf" ) {
    $override = Config::GitLike->load_file( "/etc/piny-override.conf" );
  };

  my $home = Piny::Environment->instance->user->home . "/.gitconfig";

  if ( $s->confpath ne $home and -s $home ) {
    $userconf = Config::GitLike->load_file( $home );
  };

  if ( -s "/etc/piny-default.conf" ) {
    $default = Config::GitLike->load_file( "/etc/piny-default.conf" );
  };

  foreach my $key ( keys %{$s->_conf} ) {
    if ( exists $override->{$key} ) {
      $s->_conf->{$key} = $override->{$key};
      delete $s->_conf->{$key} if $key =~ /^piny\./;
    } elsif ( exists $userconf->{$key} ) {
      delete $s->_conf->{$key} if $userconf->{$key} eq $s->_conf->{$key};
    } elsif ( exists $default->{$key} ) {
      delete $s->_conf->{$key} if $key =~ /^piny\./ and $default->{$key} eq $s->_conf->{$key};
    };
  };

  my $cs = Config::GitLike->new( confname => 'blank' );

  foreach my $key ( keys %{$s->_conf} ) {
    $cs->set( key => $key, value => $s->_conf->{$key}, filename => $s->confpath );
  };

  $s->clear_conf;
};

# Dump all tweakables

my @tweakables = ( );

sub all_tweakables {
  my ( $s ) = @_;

  my $r = { };

  foreach my $t ( @tweakables ) {
    eval {
      $r->{$t} = $s->$t;
    };
  };

  return $r;
};

# Tweakable helper

sub tweakable {
  my ( $attr, $default, $isa, $inhibit ) = @_;

  $attr = lc $attr;

  my $attrname = $attr;
  $attrname =~ s/_/./;

  if ( $attrname =~ /_/ ) { croak "Illegal attribute name $attrname! (use only one underbar)"; };

  push @tweakables, $attr unless defined $inhibit and $inhibit;

  has $attr =>
    ( is          => 'rw'
    , isa         => $isa
    , lazy_build  => 1
    , trigger     =>  sub {
                        my ( $s, $new, $old ) = @_;

                        $s->_conf->{$attrname} = $new;

                        if ( $s->has_confpath ) {
                          $s->save;
                        } else {
                          carp "Attribute $attrname modification ignored!";
                        };

                        $s->clear_conf;
                        my $clearer = "clear_$attr";
                        $s->$clearer;
                      }
    );

  my $builder = sub {
    my ( $s ) = @_;

    if ( exists $s->_conf->{$attrname} ) {
      return $s->_conf->{$attrname};
    } else {
      return $default;
    };
  };

  {
    no strict "refs";

    *{"_build_" . $attr} = $builder;
  };
};

# The tweakables

# Global tweakables, which only make sense in the global config file.
tweakable "piny_adminemail"           => "jrayhawk\@omgwallhack.org", 'Str', 'inhibit';
tweakable "piny_template"             => undef, 'Maybe[PathDir]', 'inhibit';
tweakable "piny_gitpath"              => '/srv/git', 'PathDir';

# Repo-specific tweakables, in the repos' .git/config files.
tweakable "piny_apachereload"           => "true", 'GitBool';
tweakable "piny_ikiwiki"                => "true", 'GitBool';
tweakable "piny_ikiwikidestdir"         => "/srv/www/piny.be/", 'PathDir';
tweakable "piny_ikiwikisrcdir"          => "/srv/ikiwiki/", 'PathDir';
tweakable "piny_ikiwikiurl"             => "http://piny.be/", 'HttpUrl';
tweakable "piny_ikiwikisecureurl"       => "https://secure.piny.be/", 'HttpsUrl';
tweakable "piny_ikiwikisecurepath"      => "/srv/www/secure.piny.be/", 'PathDir';
tweakable "core_sharedrepository"       => '0664', 'RepoPermission';
tweakable "receive_denynonfastforwards" => "true", 'GitBool';
tweakable "receive_denydeletes"         => "true", 'GitBool';

# User-specific tweakables, in the users' ~/.gitconfig files.
tweakable "user_email"                  => undef, 'Maybe[Str]';

# Moose boilerplate

__PACKAGE__->meta->make_immutable;

1;
