package NGCP::Config;

=encoding utf8

=head1 NAME

NGCP::Config - class to implement NGCP config handling

=cut

=head1 DESCRIPTION

The NGCP::Config class can be used to easily load configuration options
from simple configuration files, with a format similar to a section-less
INI file, but with support for multiple repeated values, and with a schema
describing the format of each option.

=cut

use strict;
use warnings;

use Exporter qw(import);
use Carp;
use NGCP::Log;

our $VERSION = '0.01';

our @EXPORT = qw(
    CONF_NONE
    CONF_TEXT
    CONF_INT
    CONF_BOOL
    CONF_LIST
    CONF_FILE
    CONF_HOST
    CONF_PORT
    CONF_LOGS
);

=head1 CONSTANTS

=over 4

=item CONF_NONE

Sentinel value.

=item CONF_TEXT

The option contains free form text.

=item CONF_INT

The option contains an integer.

=item CONF_BOOL

The option contains a boolean.

=item CONF_LIST

The option is a list either repeated, or separated by some character.

=item CONF_FILE

The option is a filename.

=item CONF_HOST

The option is a hostname (or IP address).

=item CONF_PORT

The option is a port number.

=item CONF_LOGS

The option is a log specification.

=back

=cut

use constant {
    CONF_NONE => 0,
    CONF_TEXT => 1,
    CONF_INT  => 2,
    CONF_BOOL => 3,
    CONF_LIST => 4,
    CONF_FILE => 5,
    CONF_HOST => 6,
    CONF_PORT => 7,
    CONF_LOGS => 8,
};

my %BOOL_VALUES = (
    false => 0,
    no => 0,
    off => 0,
    true => 1,
    yes => 1,
    on => 1,
);

=head1 METHODS

=item $config = NGCP::Config->new(%opts)

Create a new NGCP::Config object.
It accepts the following options:

=over 4

=item log => object

An L<NGCP::Log> object to use for logging.
Defaults to creating a new object with default options.

=item filenames => $arrayref

An arrayref of filenames to load in sequence.

=item schema => $hashref

A hashref containing entries for the configuration schema to use when parsing
the configuration file. Each element contains a C<type> key with one of the
B<CONF_*> constants, an optional C<default> key with a default value,
an optional C<sep> key for type C<CONF_LIST> with a regex that matches on
the list separator.

=item aliases => $hashref

A hashref with configuration item name aliases.
This can be used to add backward compatibility when renaming option names,
or to add aliases for some names.

=back

=cut

sub new
{
    my ($this, %opts) = @_;
    my $class = ref $this || $this;

    my $self = {
        log => $opts{log} // NGCP::Log->new(),
        filenames => $opts{filenames},
        config => {},
        schema => $opts{schema},
        aliases => $opts{aliases},
    };
    bless $self, $class;

    return $self;
}

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

    return $self->{aliases}{$name} // $name;
}

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

    $self->{log}->error("filename '$filename' is not an absolute pathname")
        unless $filename =~ m{^/};
    $self->{log}->error("filename '$filename' is too short")
        unless length $filename > 2;

    return $filename;
}

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

    return $logsname
        if $logsname eq 'console' or $logsname eq 'syslog';
    return $self->parse_filename($logsname);
}

sub parse_list
{
    my ($self, $name, $item) = @_;

    if (exists $self->{schema}{$name}{sep}) {
        return split /$self->{schema}{$name}{sep}/, $item;
    } else {
        return ($item);
    }
}

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

    return $BOOL_VALUES{$bool} // $bool;
}

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

    $self->{log}->error("invalid integer value '$int'")
        unless $int =~ m/^[-+]?\d+$/;

    return $int;
}

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

    $self->{log}->error("port $port out of range")
        if $port < 0 || $port > 65535;

    return $port;
}

sub init
{
    my $self = shift;

    # Initialize with defaults.
    foreach my $name (keys %{$self->{schema}}) {
        next unless exists $self->{schema}{$name}{default};

        my $default = $self->{schema}{$name}{default};

        if ($self->{schema}{$name}{type} == CONF_BOOL) {
            $self->{config}{$name} = $self->parse_bool($default);
        } elsif ($self->{schema}{$name}{type} == CONF_LIST) {
            foreach my $item (@{$default}) {
                push @{$self->{config}{$name}}, $self->parse_list($name, $item);
            }
        } elsif (exists $self->{schema}{$name}{default}) {
            $self->{config}{$name} = $default;
        }
    }
}

sub parse
{
    my $self = shift;

    # Parse config file.
    foreach my $config_file (@{$self->{filenames}}) {
        next unless -e $config_file;

        open my $config_fh, '<', $config_file
            or error("cannot open the configuration file '$config_file'");

        while (<$config_fh>) {
            chomp;                  # no newline
            s/#.*//;                # no comments
            s/^\s+//;               # no leading white
            s/\s+$//;               # no trailing white
            next unless length;     # anything left?

            my ($var, $value) = split(/\s*=\s*/, $_, 2);

            # Remove quotes.
            if ($value =~ m/^['"]([^'"]*)['"]$/) {
                $value = $1;
            }

            my $name = $self->_config_varname($var);
            if (not exists $self->{schema}{$name}) {
                $self->{log}->warning("unknown configuration variable '$name'");
                next;
            }

            if ($self->{schema}{$name}{type} == CONF_LIST) {
                push @{$self->{config}{$name}}, $self->parse_list($name, $value);
            } elsif ($self->{schema}{$name}{type} == CONF_BOOL) {
                $self->{config}{$name} = $self->parse_bool($value);
            } elsif ($self->{schema}{$name}{type} == CONF_INT) {
                $self->{config}{$name} = $self->parse_int($value);
            } elsif ($self->{schema}{$name}{type} == CONF_PORT) {
                $self->{config}{$name} = $self->parse_port($value);
            } elsif ($self->{schema}{$name}{type} == CONF_FILE) {
                $self->{config}{$name} = $self->parse_filename($value);
            } elsif ($self->{schema}{$name}{type} == CONF_LOGS) {
                $self->{config}{$name} = $self->parse_logsname($value);
            } else {
                $self->{config}{$name} = $value;
            }
        }
        close $config_fh;
    }
}

sub set
{
    my ($self, $name, $value) = @_;

    croak('cannot set without a variable name') unless defined $name;
    croak('cannot set without a variable value') unless defined $value;

    $name = $self->_config_varname($name);

    $self->{config}{$name} = $value;
    return $value;
}

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

    if (defined $name) {
        $name = $self->_config_varname($name);
        return $self->{config}{$name};
    } else {
        return $self->{config};
    }
}

=head1 BUGS AND LIMITATIONS

Please report problems you notice to the Sipwise
Development Team <support@sipwise.com>.

=head1 AUTHOR

Guillem Jover, C<< <gjover@sipwise.com> >>

=head1 LICENSE

Copyright (c) 2018-2022 Sipwise GmbH, Austria.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

=cut

1;
