#---------------------------------------------------------------
# ____                   _ _         ____  _
#|  _ \ ___  _   _  __ _| | |_ _   _/ ___|| |__   __ _ _ __ ___
#| |_) / _ \| | | |/ _` | | __| | | \___ \| '_ \ / _` | '__/ _ \
#|  _ < (_) | |_| | (_| | | |_| |_| |___) | | | | (_| | | |  __/
#|_| \_\___/ \__, |\__,_|_|\__|\__, |____/|_| |_|\__,_|_|  \___|
#            |___/             |___/
#
# Copyright (C) 2011 RoyaltyShare, Inc.   All Rights Reserved
#---------------------------------------------------------------

package Report::Dynamic::Parameters;
use strict;

use lib '/app/tools/common/lib';
use lib '/app/tools/report/lib';
use Common::FormObject;
use Common::XMLObject;
use Common::Assert;
use base 'Common::FormObject';
use Report::DB::Item::ReportParameter;

use Data::Dumper;
use URI::Escape;

use overload (
    '=='   => 'equals',
    'bool' => 'bool',
    '""'   => 'stringify'
);

use constant kPropertySeparator => '_';    # could be different from the one in FormObject, but we'll follow its lead

# These constants will be used to declare the 'type' for parameters.
# Specifying a type is _optional_ - Everything will default to
# plain ole' Common::FormObject::Scalar objects (which have no type-specific validation).
#
use constant kTypeScalar     => 'sclr';    # default value.
use constant kTypeInteger    => 'intg';
use constant kTypeDecimal    => 'decm';
use constant kTypeMoney      => 'mony';
use constant kTypePercent    => 'pcnt';
use constant kTypeBoolean    => 'bool';
use constant kTypeDate       => 'date';
use constant kTypeDateTime   => 'dttm';
use constant kTypeEnum       => 'enum';
use constant kTypeRadioGroup => 'rdio';
use constant kTypeMenu       => 'menu';
use constant kTypeCustom     => 'cstm';

# This hash maps our type constants to actual FormObject::Scalar classes.
# Used internally.
#
my %gTypeMap = (
    kTypeScalar()     => 'Common::FormObject::Scalar',
    kTypeInteger()    => 'Common::FormObject::Scalar::Integer',
    kTypeDecimal()    => 'Common::FormObject::Scalar::Decimal',
    kTypeMoney()      => 'Common::FormObject::Scalar::Money',
    kTypePercent()    => 'Common::FormObject::Scalar::Percent',
    kTypeBoolean()    => 'Common::FormObject::Scalar::Boolean',
    kTypeDate()       => 'Common::FormObject::Scalar::Date',
    kTypeDateTime()   => 'Common::FormObject::Scalar::DateTime',
    kTypeEnum()       => 'Common::FormObject::Scalar::Enum',
    kTypeRadioGroup() => 'Common::FormObject::Scalar::Enum',
    kTypeMenu()       => 'Common::FormObject::Scalar::Enum',
    kTypeCustom()     => 'Common::FormObject::Scalar',
);

#
# !!!  Want to call 'new'?   Don't!
# !!!  If you want a fresh Parameter object, invoke the static class method of
# !!!  the Report subclass who's Parameters you want to specify.
# !!!  I.e. RPS::Report::Dynamic::Contracts::ArtistAndProducer->Parameters();
#

sub _OptionalData {
    return {};
}

sub _Criteria {
    return {};
}

sub _Groups {
    return undef;
}

sub _OptionalDataXSLT {
    assert( 0, 'override' );
}

sub _CriteriaXSLT() {
    assert( 0, 'override' );
}

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

    # Build our state based on the optionalData and criteria hashes.
    # We'll do this immediately, then load saved state from the database
    # if we've been given an ID.
    #
    $self->_initProperties(%args);

    $self->{ReportName} = Common::FormObject::Scalar->new();

    if ( $args{reportID} ) {
        $self->_loadPropertiesFromDB( $args{reportID} );
    }

    return $self;
}

# This method (which should ONLY be called by the Report class) will
# write the arguments out to the database.
# It ought to also nuke any existing parameters associated with the ID.
#
sub save {
    my ( $self, $id ) = @_;
    assert($id);

    # I don't feel like having two tables, so will flatten it all out into one.
    #
    Report::DB::Item::ReportParameter->DeleteForID($id);

    if ( $self->{OptionalData} ) {
        foreach my $key ( keys %{ $self->{OptionalData} } ) {
            next if ( '_' eq substr( $key, 0, 1 ) );

            $self->saveParam( id => $id, key => $key, value => $self->{OptionalData}{$key}->access() );
        }
    }

    if ( $self->{Criteria} ) {
        foreach my $key ( keys %{ $self->{Criteria} } ) {
            next if ( '_' eq substr( $key, 0, 1 ) );
            if ( $self->{Criteria}{$key}->isa('Common::FormHash') ) {
                my $formHash = $self->{Criteria}{$key};
                my $tagName  = $formHash->getTagName();
                my $hash     = $formHash->getHash();
                foreach my $hashKey ( @{ $formHash->getKeys() } ) {
                    foreach my $fieldName ( keys %{ $hash->{$hashKey} } ) {
                        next if ( '_' eq substr( $fieldName, 0, 1 ) );
                        $self->saveParam(
                            id    => $id,
                            key   => join( kPropertySeparator, $key, $tagName, $hashKey, $fieldName ),
                            value => $hash->{$hashKey}{$fieldName}->access()
                        );
                    }
                }
            } else {
                $self->saveParam( id => $id, key => $key, value => $self->{Criteria}{$key}->access() );
            }
        }
    }
}

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

    my $newItem = Report::DB::Item::ReportParameter->Create();
    $newItem->report_id( $args{id} );
    $newItem->param_key( $args{key} );
    $newItem->param_value( $args{value} );
    $newItem->save();
}

sub specifiedOptions {
    my ($self) = @_;

    my %specifiedOptions;

    if ( $self->{OptionalData} ) {
        foreach my $key ( keys %{ $self->{OptionalData} } ) {

            # Going to leave this here for now.
            # It should let us ignore params that aren't used in the backend code.
            next if ( '_' eq substr( $key, 0, 1 ) );

            my $value = $self->{OptionalData}{$key}->access();

            if ($value) {
                my $config = $self->_OptionalData()->{$key};

                # First check for a specified filename
                if ( $config->{filename} ) {
                    my $filename = $config->{filename};
                    if ( ref($filename) eq 'ARRAY' ) {
                        my %filenameHash;
                        my $validValues = $config->{validValues};
                        @filenameHash{@$validValues} = @$filename;
                        $filename = $filenameHash{$value};
                    }

                    if ( $filename eq 'undef' ) {
                        next;
                    } else {
                        $specifiedOptions{$filename} = 1;
                    }
                }

                # For booleans, default to the key
                elsif ( $config->{type} eq Report::Dynamic::Parameters::kTypeBoolean ) {
                    $specifiedOptions{$key} = 1;
                }

                # All others default to the value
                else {
                    $specifiedOptions{$value} = 1;
                }
            }
        }
        return keys %specifiedOptions;
    }

    return undef;

}

sub specifiedCriteria {
    my ($self) = @_;

    my %specifiedCriteria;

    if ( $self->{Criteria} ) {
        foreach my $key ( keys %{ $self->{Criteria} } ) {

            # Going to leave this here for now.
            # It should let us ignore params that aren't used in the backend code.
            next if ( '_' eq substr( $key, 0, 1 ) );

            my $value = $self->{Criteria}{$key}->access();

            if ($value) {
                my $config = $self->_Criteria()->{$key};

                # !!! Adding criteria to the filename is very rare so far,
                # !!! so by default we will _not_ add anything.
                # !!! This is the opposite of the specifiedOptions behavior,
                # !!! but it seems like the right thing to do here.

                if ( $config->{inFilename} ) {
                    $specifiedCriteria{$value} = 1;

                }
            }
        }
        return keys %specifiedCriteria;
    }

    return undef;

}

sub _initProperties {
    my ($self) = @_;

    $self->_initOptionalData();
    $self->_initCriteria();
    $self->_initGroups();
    $self->_initDisplayOptions();
}

sub _initOptionalData {
    my ($self) = @_;

    my $class = ref($self);

    my $configHash = $class->_OptionalData();
    if ($configHash) {
        $self->{OptionalData} = Common::FormObject->new();
        foreach my $key ( keys %$configHash ) {
            my $config = $configHash->{$key};

            $self->{OptionalData}{$key} = $self->_createScalarObject($config);
        }
        $self->{OptionalData}->setXMLParam( 'xslt', $class->_OptionalDataXSLT() );
    }
}

sub _initCriteria {
    my ($self) = @_;

    my $class = ref($self);

    my $configHash = $class->_Criteria();
    if ($configHash) {
        $self->{Criteria} = Common::FormObject->new();
        foreach my $key ( keys %$configHash ) {
            my $config = $configHash->{$key};

            $self->{Criteria}{$key} = $self->_createScalarObject($config);
        }

        $self->{Criteria}->setXMLParam( 'xslt', $class->_CriteriaXSLT() );
    }
}

sub _initGroups {
    my ($self) = @_;

    # Super simple.
    #
    my $class = ref($self);

    my $configHash = $class->_Groups();
    if ($configHash) {

        # We're going to use an XMLObject here because we want some of these elements
        # to be XML Parameters.
        #
        my %xmlParams = ( order => 1, );

        $self->{Groups} = Common::XMLObject->new();

        while ( my ( $key, $value ) = each(%$configHash) ) {
            my $newObj = Common::XMLObject->new();
            while ( my ( $subKey, $subValue ) = each(%$value) ) {
                if ( $xmlParams{$subKey} ) {
                    $newObj->setXMLParam( $subKey, $subValue );
                } else {
                    $newObj->{$subKey} = $subValue;
                }
            }
            $self->{Groups}{$key} = $newObj;
        }
    }
}

# If we have any 'enum'-derived scalars, we break out their options as a distinct
# XML block to make it easier for the templates to digest.
#
sub _initDisplayOptions {
    my ($self) = @_;

    # This doesn't need to be a 'FormObject' - It's just for outbound XML.
    #
    my %displayOptions;

    my $class = ref($self);
    my @configs;
    push @configs, $class->_Criteria();
    push @configs, $class->_OptionalData();

    foreach my $configHash (@configs) {
        next unless $configHash;

        foreach my $key ( keys %{$configHash} ) {
            my $config = $configHash->{$key};
            if ( $config->{validValues} ) {

                # 'validValues' is the _required_ element.  'displayValues' is optional.
                #
                my $validValues   = $config->{validValues};
                my $displayValues = $config->{displayValues};

                my @options;
                for ( my $i = 0 ; $i < scalar @$validValues ; $i++ ) {
                    my $option = { Value => $validValues->[$i], };
                    if ($displayValues) {
                        $option->{Display} = $displayValues->[$i];
                    }

                    push @options, $option;
                }

                $displayOptions{$key}{Options} = \@options;
            }
        }
    }

    if ( scalar keys %displayOptions ) {
        $self->{DisplayOptions} = \%displayOptions;
    }
}

sub _createScalarObject {
    my ( $self, $config ) = @_;
    assert( $config, 'You have to pass _some_ sort of hash, even {}' );

    assert( !$config->{class} || $config->{type} eq kTypeCustom, 'If you specify a class, the type has to be custom' )
      ;    # at least for now - scott

    # If the config is an empty hash or undef, then we'll default to scalar.
    #
    my $type = kTypeScalar;
    if ( $config->{type} ) {
        $type = $config->{type};
    }

    # Check for programmer goofiness.
    #
    assert( defined $gTypeMap{$type}, "ERROR - unknown field type '$type'" );

    # Use this when you're working with a derived class (like something from Common::FormHash)
    my $class = $config->{class};
    if ( !$class ) {

        # The actual mapping of type to Scalar class lives in a global hash.
        #
        $class = $gTypeMap{$type};
    }

    # Many of these scalar classes have optional arguments.
    # Including the initial (or default) value.
    # Check out all the Scalar classes in Common::FormObject to see what they are.
    # Many of them end up as XML Parameters.
    #
    my $scalarObj = $class->new( %{$config} );

    # We'll add some additional XML Parameters for the UI.
    # !!! We could migrate these to the FormObject::Scalar class
    #
    $scalarObj->setXMLParam( 'type',    $type );
    $scalarObj->setXMLParam( 'display', $config->{display} ) if defined $config->{display};
    $scalarObj->setXMLParam( 'group',   $config->{group} ) if defined $config->{group};
    $scalarObj->setXMLParam( 'order',   $config->{order} ) if defined $config->{order};

    return $scalarObj;
}

sub _loadPropertiesFromDB {
    my ( $self, $id ) = @_;
    assert($id);

    # Parameters are stored 'flat' - one table for
    # all regardless of whether their 'optional data' or 'criteria' or
    # whatever.  The table doesn't record that state.
    #

    my $collection = Report::DB::Item::ReportParameter->GetAllForID($id);
    while ( my $item = $collection->next() ) {
        my $key   = $item->param_key();
        my $value = $item->param_value();
        if ( defined $self->{OptionalData}{$key} ) {
            $self->{OptionalData}{$key}->access($value);
        } elsif ( defined $self->{Criteria}{$key} ) {
            $self->{Criteria}{$key}->access($value);
        } elsif ( index( $key, kPropertySeparator ) > -1 ) {
            my @keys = split( kPropertySeparator, $key );
            my $class = ref( $self->{Criteria}{ $keys[0] } );
            if ( $class && $class->isa('Common::FormHash') ) {
                $self->_accessProperty( \@keys, $value );
            }
        } else {
            die Common::Exception->new("ERROR - unknown key $key for report $id");
        }
    }
}

sub _accessProperty {
    my $self        = shift;
    my $addressList = shift;

    if ($addressList) {    # && @$addressList == 1 ) {
        $addressList = [ 'OptionalData', $addressList->[0] ] if ( $self->_isOptionalDataParameter( $addressList->[0] ) );
        splice( @$addressList, 0, 0, 'Criteria' ) if ( $self->_isCriteriaParameter( $addressList->[0] ) );
    }

    $self->SUPER::_accessProperty( $addressList, @_ );
}

sub _isCriteriaParameter {
    my $self = shift;
    my $name = shift || return;

    my $config = $self->_Criteria || return;

    return $config->{$name} ? 1 : undef;
}

sub _isOptionalDataParameter {
    my $self = shift;
    my $name = shift || return;

    my $config = $self->_OptionalData || return;

    return $config->{$name} ? 1 : undef;
}

sub equals {
    my $self    = shift;
    my $rObject = shift;

    assert($self);
    assert($rObject);

    return "$self" eq "$rObject";
}

sub bool {
    my $obj = shift;
    return !undef($obj);
}

sub stringify {
    my $obj = shift;
    my $string;

    my $data = $obj->_keyValueParameters();

    foreach my $key ( keys %$data ) {
        $string .= sprintf( "%s=%s", $key, uri_escape( $data->{$key} ) );
    }

    return "$string";
}

sub _keyValueParameters {
    my $self = shift;

    my %data;

    foreach my $key ( keys %{ $self->{OptionalData} } ) {
        $data{$key} = $self->OptionalData->$key unless ( $key =~ /^_/ );
    }

    foreach my $key ( keys %{ $self->{Criteria} } ) {
        $data{$key} = $self->Criteria->$key unless ( $key =~ /^_/ );
    }

    return \%data;
}

1;
