###################################################################
# Copyright (C) 2006 RoyaltyShare, Inc.  All Rights Reserved
###################################################################

package Common::Config;

###
#   Common::Config
#
#   Defines an object to read configuration files.  Config files come in two
#   types, global and host configurations.  When the config files are parsed
#   first the global configuration is read, then the host configuration can
#   overwrite the global options.
#
#   Config files can be found in search paths which are defined in the object.
#   Once a configuration file is found in the seach path the operation stops.
#   This means that only one global configuration and one host configuration
#   will be read.  Also global configuration files are required, while host
#   configurations are not.
#
#   Usage:
#
#   This object is designed to be used through the Common::RSApp interface,
#   however, it these objects can also be instantiated independantly.
#
#   == Via Common::RSApp ==
#
#   # Force a parse of a configuration file.
#   Common::RSApp::GetConfig( 'module' );
#
#   # Read a parameter from the 'module' configuration.
#   my $email = Common::RSApp::GetConfig( 'module', 'option_name' );
#
#   == Via Configuration Object ==
#
#   my $config = new Distribution::Config();  # Reads config file.
#   my $email = $config->get( 'option_name' );
#
#   See Also:
#
#   This object is based on the AppConfig class, see:
#
#   http://search.cpan.org/dist/AppConfig/lib/AppConfig.pm
###

use strict;
use warnings;
use Carp;
use Date::Format;
use Data::Dumper;

use AppConfig;
use AppConfig::State;
use AppConfig::File;

use File::Basename;
use Sys::Hostname;

use lib '/app/tools/common/lib';
use Common::Assert;

###
#   Defines a hash ref containing configuration parameters to define.  Each
#   entry is a key pointing to a hashref is configuration options for the
#   define call.  The key is the actual configuration option.  The hashref
#   param can be empty.
#
#   Options for the configuration parameters can be found in the AppConfig
#   documentation: http://search.cpan.org/dist/AppConfig/lib/AppConfig.pm
#
#   eg.
#
#   { my_option_name => { ARGCOUNT => AppConfig::ARGCOUNT_LIST } }
####
sub _configParameters {
    my $self = shift;

    croak "virtual method must be overloaded for derived classes"
      unless ( ref($self) eq "Common::Config" );

    ####
    #    Common Configuration Items
    ####
    {
        smtp_server            => {},
        perl                   => { ARGCOUNT => AppConfig::ARGCOUNT_LIST },
        development_repository => {
            DEFAULT => "$ENV{HOME}/workspace",
        },
        development_code_dir => {
            DEFAULT => "/app/tools",
        },
        development_mysql_dir => {
            DEFAULT => "/space/mysql/current",
        },
        development_mysql_repository => {
            DEFAULT => "/space/mysql",
        },
        staging_repository => {
            DEFAULT => "/app/branches",
        },
        staging_code_dir => {
            DEFAULT => "/app/tools",
        },
        staging_mysql_dir => {
            DEFAULT => "/space/mysql/current",
        },
        staging_mysql_repository => {
            DEFAULT => "/space/mysql",
        },
        mysql_init => {
            DEFAULT => "/etc/init.d/mysql",
        },
        apache_init => {
            DEFAULT => "/etc/init.d/httpd",
        },
    };
}

###
#   Defines the configuration file name.  Must be overloaded.
###
sub _configFileName {
    my $self = shift;

    croak "virtual method must be overloaded for derived classes"
      unless ( ref($self) eq "Common::Config" );

    'rs_common.conf';
}

###
#   Define a list of paths where we should search for the configuration file.
#   NOTE: Once a configuration file is found we stop searching the path.
###
sub _globalConfigSearchPath {
    ( '/app/tools/sysadmin/conf', 'D:/app/tools/sysadmin/conf/', 'C:/app/tools/sysadmin/conf' );
}

###
#   Define a list of paths where we should search for the host specific
#   configuration file.  This file will overwrite any options in the
#   global configuration file.
#   NOTE: Once a configuration file is found we stop searching the path.
###
sub _localConfigSearchPath {
    #( '/app/config', "$ENV{HOME}/.royaltyshare", 'D:/app/config', 'C:/app/config' );
    ( '/app/config', 'D:/app/config', 'C:/app/config' );
}

###
#   Define options to use when creating the configuration object.  See the
#   AppConfig documentation:
#       http://search.cpan.org/dist/AppConfig/lib/AppConfig.pm
###
sub _globalAppConfigDefaults {
    {
        GLOBAL => {
            ARGCOUNT => AppConfig::ARGCOUNT_ONE,
            EXPAND   => AppConfig::EXPAND_ALL,
        },
        PEDANTIC => 0,    # Don't ignore warnings when parsing config file
    };
}

sub new {
    my ( $class, %args ) = @_;
    my $self = bless {}, $class;

    return $self->_init(%args);
}

sub _init {
    my ( $self, %args ) = @_;

    $self->{_config} = $self->_getAppConfigObject();

    $self->_readConfig();

    return $self;
}

sub get {
    my $self    = shift;
    my $keyword = shift;

    return unless ( $self->{_config} );

    croak "'$keyword' not defined in config"
      unless ( $self->{_config}->_exists($keyword) );

    return $self->{_config}->get($keyword);
}

sub _readConfig {
    my $self = shift;

    $self->_parseGlobalConfigFile();
    $self->_parseLocalConfigFile();
}

sub _parseGlobalConfigFile {
    my $self = shift;
    my $dir;
    my $config_file;
    my @path = $self->_globalConfigSearchPath();

    for $dir (@path) {
        $config_file = "$dir/" . $self->_configFileName();
        if ( -e $config_file ) {
            if ( $self->{_config}->file($config_file) ) {
                return 1;
            } else {
                croak("Failed to parse config file: $config_file: $@");
            }
        }
    }

    croak "'" . $self->_configFileName() . "' not found in global config path: '@path'";
}

sub _parseLocalConfigFile {
    my $self = shift;
    my $dir;
    my $config_file;
    my @path = $self->_localConfigSearchPath();

    for $dir (@path) {
        $config_file = "$dir/" . $self->_configFileName();
        if ( -e $config_file ) {
            if ( $self->{_config}->file($config_file) ) {
                return 1;
            } else {
                croak("Failed to parse config file: $config_file: $@");
            }
        }
    }
}

sub _getAppConfigObject {
    my $self = shift;

    my $config = new AppConfig( $self->_globalAppConfigDefaults() );

    my $params = $self->_configParameters();

    $config->define(%$params);

    return $config;
}

sub getFirstDirectory {
    assert( @_ > 1, "Missing config option name" );
    return _findFirstFile( @_, 'd' );
}

sub getFirstFile {
    assert( @_ > 1, "Missing config option name" );
    return _findFirstFile( @_, 'f' );
}

sub getFirstExecutable {
    assert( @_ > 1, "Missing config option name" );
    return _findFirstFile( @_, 'x' );
}

sub getFirstExists {
    assert( @_ > 1, "Missing config option name" );
    return _findFirstFile( @_, 'e' );
}

sub _findFirstFile {
    my $self       = shift;
    my $configName = shift;
    my $fileType   = shift;

    my $configParam = $self->get($configName);

    return $configParam
      if ( not defined($configParam) || not ref($configParam) );

    assert( ref($configParam) eq 'ARRAY', "Currently only support array config options" );

    foreach my $config (@$configParam) {
        return $config
          if ( ( $fileType eq 'd' && -d $config )
            || ( $fileType eq 'f' && -f $config )
            || ( $fileType eq 'e' && -f $config )
            || ( $fileType eq 'x' && -x $config ) );
    }

    return;
}

1;
