package RFC::Sum;
#$Id: Sum.pm,v 1.38 2025/03/20 07:58:56 sqko Exp $
#################################################################
#
#
#       RFC::Sum
#
# This class defines several special virtual devices which can be used
# as a sort of filter on top of a normal devices. 
#
# The following description is for a RFC::Sum device, but the other 
# device types (RFC::Min and RFC::Max) behaves similarly when considering 
# the input and output devices
#
# A RFC::Sum device
# operates on other devices and when the Sum device is read 
# (that is it's read function is called), the device retruns the
# arithmetic sum of the input devices read functions.
# 
# Additionally the RFC::Sum device registers itself on all the input
# devices resulting in that if a set or setflow or similar control 
# comand is called on one of the inputs, then the same control
# function on the output device (if any) is called
# (This is achieved using the observer pattern).
#
# In the case of gas devices, if the setflow command is callen on
# any of the inputs, the setflow command is called with the sum of
# the inputs as argument.
# 
# When a read command is called on a RFC::Sum device, the device first
# reads the values of all the inputs, and if the input devices supports
# the cutoff_set command, it checks if the values are below the value of 
# the cutoff.
# If so, the value for that device is then set to 0.
# After this check all the input values are summed together (with the 
# respecive input factors multiplied on the values before summing).
#
# To obtain an instance of the RFC::Sum device call the constructor:
#
# my $id = RFC::Sum->new($name,$rig);
#
# Instances of the other device classes (RFC::Min and RFC::Max which uppon a read 
# request returns either the minimum or maximum value of the inputs) 
# can be obtained as:
#
# my $id = RFC::Min->new($name,$rig);
#
# my $id = RFC::Max->new($name,$rig);
#
# For RFC::Min and RFC::Max devices any commands passed from any of the inputs
# is forwarded to the output device (if any) with the minimum or 
# maximum value (instead of the sum) as for a RFC::Sum device.
#
#
# The Sum device can be used as a simple offsetting filter by only
# specifying a single input.
# If this is configured for tempcntrol devices both the setpoint and the 
# ramprate is transferred.
# Please note, however, that this functionality has been superceeded by the
# Output spline filters and is thus deprecated in version 5.6.4 and onwards.
#
################################################################# 


use strict;
use warnings;
use RFC::BaseDevice;
use RFC::Header;
our @ISA = qw(RFC::BaseDevice);
our $AUTOLOAD;


BEGIN {
    use Exporter ();
    our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
    $VERSION = 1.0;
    %EXPORT_TAGS = (
        'standard'=>[],
                    );
    Exporter::export_ok_tags('standard');
    Exporter::export_tags('standard');
    @ISA = qw(Exporter);
}




sub new()
{
    my $class = shift;
    my $name = shift;
    my $rig = shift;
    my $self = $class->SUPER::new($name,$rig);
    $self->{'_type'} = 'Summing device'; 
    $self->{'_html_template'} = 'Filter.tt';
    $self->{'_setup_tags'} = ['class','mode','device_type','offset','inputs','output_device','title','show_plot','read_function'];
    $self->{'_setup_tags_readonly'} = ['mode'];
    $self->{'_desc'} = {
        'class' => 'Device class',
        'mode' => 'Device control mode',
        'inputs' => 'number of inputs',
	'device_type' => 'Device type',
	'offset' => 'Numeric offset of the output (added to the sum of the inputs)',
        'output_device' => 'Output device (optional if current device is used in a control chain)',
        'title' => 'Optional title',
        'read_function' => 'Device function to use for read operations',
    };
    $self->{'_default'} = {
	'device_type' => 'simplechannel',
        'inputs' => 2,
	'offset' => 0,
	'read_function' => 'read',
    };
    bless $self,$class;
    $self->{'_reload_tags'} = ['class','mode','device_type','inputs','output_device'];
    $self->{'_values_class'} = [RFC::Device::list_filter()];
    $self->{'_values_device_type'} = ['',$self->rig->list_types_nofilter()];
    $self->{'_values_inputs'} = [2,3,4,5,6,7,8,9];
    $self->{'_section'} = 'filter_'.$self->name;
    $self->{'_values_read_function'} = ['read'];
    $self->load_conf();
    my @list = ();
    foreach ($self->rig->avaliable_devices($self->{'_device_type'}))
    {
        push @list,$_ unless ($_ eq $self->name()); ## Do not include self ##
    }
    $self->{'_values_output_device'} = ['',@list];
    if ($self->{'_device_type'} && $self->{'_output_device'})
    {
        ### get device if it already exist ###
        my $dev = $self->rig->get_device($self->{'_device_type'},$self->{'_output_device'});
        if ($dev)
        {
            $self->{'_dev'} = $dev;
            $dev->set_readonly();
        }
    }
    my $found_read = 0;
    for (my $x = 0; $x < $self->{'_inputs'}; $x++)
    {
        my $tag = 'input_device_'.$x;
        $self->{'_values_'.$tag} = ['',@list];
        push @{$self->{'_setup_tags'}},$tag;
        $self->{'_'.$tag} = $self->get_default($tag);
        my $conf = $self->get_conf($tag);
        $self->{'_'.$tag} = $conf if (defined($conf) && $conf =~ /\w/);
        $self->{'_desc'}->{$tag} = 'Input device '.$x;
	## Handle factors on inputs ###
	$tag = 'input_device_factor_'.$x;
	$self->{'_default'}->{$tag} = 1;
        push @{$self->{'_setup_tags'}},$tag;
        $self->{'_'.$tag} = $self->get_default($tag);
	$conf = $self->get_conf($tag);
        $self->{'_'.$tag} = $conf if (defined($conf) && $conf =~ /\d/);
        $self->{'_desc'}->{$tag} = 'Input device '.$x.' factor (multiplied on input device read value before summing)';
	next if ($found_read);
	next unless ($self->{'_dev_'.$x});
	my @arr = $self->{'_dev_'.$x}->read_names();
	$self->{'_values_read_function'} = [@arr];
	my @arr2 = $self->{'_dev_'.$x}->list_set_functions();
	$self->{'_set_fn_names'} = [@arr2];
	$found_read = 1;
    }
    $self->rig->register_device($self->{'_device_type'},$self);
    ### Summing devices can not be controlled directly, only thorugh notification from inputs.
    $self->{'_readonly'} = 1;
    $self->{'_info_string'} = 'Note that filter devices used in a control chain only works correctly if they are included (enabled)';
    ### Note, do NOT run init as part of constructor! ###
    return $self;
}


sub init()
{
    my $self = shift;
    my @list = ();
    foreach ($self->rig->avaliable_devices($self->{'_device_type'}))
    {
        push @list,$_ unless ($_ eq $self->name()); ## Do not include self ##
    }
    ### Remove any duplicates in list  ###
    my %hash = ();
    foreach (@list)
    {
        $hash{$_} = 1;
    }
    @list = ();
    foreach my $key (sort keys %hash)
    {
        push @list,$key;
    }
    my $found_read = 0;
    for (my $x = 0; $x < $self->{'_inputs'}; $x++)
    {
        my $tag = '_input_device_'.$x;
        $self->{'_values'.$tag} = ['',@list];
        next if ($self->{'_dev_'.$x}); 
        my $dev = $self->rig->get_device($self->{'_device_type'},$self->{$tag});
        $dev = $self->rig->get_device('filter',$self->{$tag}) unless ($dev);
        unless ($dev)
        {
            $dev = $self->rig->init_device($self->{'_device_type'},$self->{$tag});
            $dev = $self->rig->init_device('filter',$self->{$tag}) unless ($dev);
            $dev->readstring_ignore(1) if ($dev);
        }
        if ($dev)
        {
            $dev->debug($self->debug);
            $self->{'_dev_'.$x} = $dev; 
            $dev->attatch_observer(\$self);
        }
	next if ($found_read);
	next unless ($self->{'_dev_'.$x});
	my @arr = $self->{'_dev_'.$x}->read_names();
	$self->{'_values_read_function'} = [@arr];
	my @arr2 = $self->{'_dev_'.$x}->list_set_functions();
	$self->{'_set_fn_names'} = [@arr2];
	$found_read = 1;
    }
    unless ($self->{'_dev'})
    {
        my $tag = '_output_device';
        my $dev = $self->rig->get_device($self->{'_device_type'},$self->{$tag});
        $dev = $self->rig->get_device('filter',$self->{$tag}) unless ($dev);
        unless ($dev)
        {
            $dev = $self->rig->init_device($self->{'_device_type'},$self->{$tag});
	    $dev = $self->rig->init_device('filter',$self->{$tag}) unless ($dev);
            $dev->readstring_ignore(1) if ($dev);
        }
        if ($dev)
        {
            $dev->debug($self->debug);
            $self->{'_dev'} = $dev; 
            $dev->set_readonly();
        }
    }
}

sub Accept
{
    my $self = shift;
    my $visitor = shift;
    return undef if ($self->{'_seen_visitors'}{$visitor});
    for (my $x = 0; $x < $self->{'_inputs'}; $x++)
    {
        my $tag = '_dev_'.$x;
        $self->{$tag}->Accept($visitor) if ($self->{$tag});
    }
    $visitor->VisitComplex($self);
    $self->{'_seen_visitors'}{$visitor} = 1;
    return undef;
}

sub print_config()
{
    my $self = shift;
    $self->init();
    my @arr = ();
    push @arr,'Name: '.$self->name;
    push @arr,'Type: '.$self->type;
    push @arr,'Offset: '.$self->{'_offset'};
    push @arr,$self->{'_dev'}->print_config() if ($self->{'_dev'});
    push @arr,'Inputs: '.$self->{'_inputs'};
    for (my $x = 0; $x < $self->{'_inputs'}; $x++)
    {
        my $tag = '_dev_'.$x;
        push @arr,$self->{$tag}->print_config(@_) if ($self->{$tag});
    }
    return join(', ',@arr);
}


sub notify
{
    my $self = shift;
    $self->init();
    my @response = $self->SUPER::notify(@_);
    push @response, $self->{'_dev'}->notify(@_) if ($self->{'_dev'});
    return @response;
}

sub local_init()
{
    #### Stub function intended for loverloading for any impelmention dependent initialisations ###
    my $self = shift;
    return $self;
}

sub observe($$)
{
    my $self = shift;
    my $action = shift;
    my $value = shift;
    if ($action eq 'setflow')
    {
	$self->setflow($value,@_);
    }
    else
    {
	if ($self->{'_device_type'} eq 'tempcontrol' && $self->{'_inputs'} == 1)
	{
	    ### Specproc for using Sum as temperature offset for temperature controler ##
	    ### Deprecated from 5.6.4 and maintained for backwards compatibility only  ##
	    my $tag = '_dev_0';
            ### Transfer ramprate ###
            my $ramp = $self->{$tag}->read_named('get_ramp') if ($self->{$tag});
            $self->{'_dev'}->set_ramp($ramp,@_) if ($self->{'_dev'});
            ### Transfer setpoint ###
            my $sp = $self->{$tag}->read_named('get_temp') if ($self->{$tag});
	    $sp += $self->{'_offset'};
            $self->{'_dev'}->set_temp($sp,@_) if ($self->{'_dev'});
	}
	else
	{
	    $self->{'_dev'}->observe($action,$value,@_) if ($self->{'_dev'});
	}
    }
    return $self->SUPER::notify($action,$value,@_);
}


sub setflow
{
    my $self = shift;
    my $flow = shift;
    select(undef,undef,undef,0.1);  ### Alow other devices to settle ###
    return $self->{'_dev'}->setflow($self->read(),@_) if ($self->{'_dev'});
}

sub read
{
    my $self = shift;
    my $data = undef;
    if (@_)
    {
        $data = $_[0];
    }
    else
    {
        $data = RFC::Cache->new();
        $data->debug($self->debug) if ($self->debug);
    }
    $self->check_cache($data);
    my $sum = 0;
    my $cached = $data->get($self->type,$self->name);
    if (defined($cached))
    {
        $self->writedebug("Using already measured value for ".$self->name) if ($self->debug > 3); 
        $sum = $cached;
    }
    else
    {
	$self->init();
	$sum = $self->{'_offset'};
	for (my $x = 0; $x < $self->{'_inputs'}; $x++)
	{
	    my $tag = '_dev_'.$x;
	    my $val = $self->{$tag}->read_named($self->{'_read_function'},$data) if ($self->{$tag});
	    if ($self->{$tag}->can('cutoff_set'))
	    {
		$val = 0 if ($val < $self->{$tag}->cutoff_set());
	    }
	    $sum += $val * $self->{'_input_device_factor_'.$x} if ($self->{$tag});
	}
	$data->set($self->type,$self->name,$sum);
    }
    $self->writedebug("Sum of inputs was: $sum") if ($self->debug);
    return $sum;
}

sub test()
{
    my $self = shift;
    $self->init();
    my $res = 1;
    for (my $x = 0; $x < $self->{'_inputs'}; $x++)
    {
        my $tag = '_dev_'.$x;
	unless ($self->{$tag})
	{
	    warn "Warning: Input device $x not defined for $self!\n";
	    next;
	}
        $res = 0 unless ($self->{$tag}->test);
    }
    return $res;
}


sub test_string()
{
    my $self = shift;
    my $data = undef;
    if (@_)
    {
        $data = $_[0];
    }
    else
    {
        $data = RFC::Cache->new();
        $data->debug($self->debug) if ($self->debug);
    }
    $self->check_cache($data);
    $self->init(); 
    my $type = $self->type();
    my $val = $self->read($data);
    my @arr = ('Composite device "'.$self->name.'" of type "'.$type.'": Value: '.$val);
    for (my $x = 0; $x < $self->{'_inputs'}; $x++)
    {
        my $tag = '_dev_'.$x;
	if ($self->{$tag})
	{
	    my $str = $self->{$tag}->test_string($data);
	    chomp $str;
	    push @arr,'Input device '.$x.': '.$str;
	    if ($self->{$tag}->can('cutoff_set'))
	    {
		push @arr,'Cutoff set is: '.$self->{$tag}->cutoff_set();
	    }
	}
	else
	{
	    push @arr,'Input device '.$x.' is not defined';
	}
    }
    if ($self->{'_dev'})
    {
        my $str = $self->{'_dev'}->test_string($data);
        chomp $str;
        push @arr,'Output device: '.$str;
    }
    my $string = join("\n",@arr)."\n\n";
    return $string;
}


sub readonly_value($)
{
    my $self = shift;
    my $tag = shift;
    return undef unless ($tag);
    ### allow call to get 'mode' for undefined devices ###
    return 'Na' if ($tag eq 'mode' && !$self->{'_device'});
    $self->init();
    if ($self->device)
    {
	return $self->device->mode() if ($tag eq 'mode');
	return $self->device->readonly_value($tag);
    }
    return undef;
}

sub readonly()
{
    my $self = shift;
    $self->init();
    return $self->SUPER::readonly();
}


sub device()
{
    my $self = shift;
    $self->init();
    return $self->{'_dev'};
}

sub AUTOLOAD()
{
    ################################################################
    ### AUTOLOAD() , redirect all method calls not defined by    ###
    ### Basedevice to the underlying device                      ###
    ################################################################
    my $self = shift;
    $self->init();
    {
        no strict "refs";
        $AUTOLOAD =~ /\:\:(\w+)$/;
        return $self->{'_dev'}->$1(@_);
    }
}


package RFC::Min;

our @ISA = qw(RFC::Sum);


sub new
{
    my $class = shift;
    my $name = shift;
    my $rig = shift;
    my $self = $class->SUPER::new($name,$rig);
    $self->{'_type'} = 'Minimum device'; 
    ### Remove offset from tag list ###
    my @list = ();
    foreach (@{$self->{'_setup_tags'}})
    {
	push @list,$_ unless ($_ eq 'offset');
    }
    $self->{'_setup_tags'} = [@list];
    bless $self,$class;
    $self->load_conf();
    my $found_read = 0;
    for (my $x = 0; $x < $self->{'_inputs'}; $x++)
    {
        my $tag = 'input_device_'.$x;
        $self->{'_desc'}->{$tag} = 'Input device '.$x.' factor (multiplied on input device read value before determining minimum)';
    }
    return $self;
}

sub read
{
    my $self = shift;
    my $data = undef;
    if (@_)
    {
        $data = $_[0];
    }
    else
    {
        $data = RFC::Cache->new();
        $data->debug($self->debug) if ($self->debug);
    }
    $self->check_cache($data);
    my $min = -32768;
    my $cached = $data->get($self->type,$self->name);
    if (defined($cached))
    {
        $self->writedebug("Using already measured value for ".$self->name) if ($self->debug > 3); 
        $min = $cached;
    }
    else
    {
	$self->init();
	my @list = ();
	for (my $x = 0; $x < $self->{'_inputs'}; $x++)
	{
	    my $tag = '_dev_'.$x;
	    my $val = $self->{$tag}->read_named($self->{'_read_function'},$data) if ($self->{$tag});
	    if ($self->{$tag}->can('cutoff_set'))
	    {
		$val = 0 if ($val < $self->{$tag}->cutoff_set());
	    }
	    push @list,$val * $self->{'_input_device_factor_'.$x} if ($self->{$tag});
	}
	@list = sort {$a <=> $b} @list;
	$min = $list[0] if (@list);
	$data->set($self->type,$self->name,$min);
    }
    $self->writedebug("Minimum of inputs was: $min") if ($self->debug);
    return $min;
}


package RFC::Max;

our @ISA = qw(RFC::Sum);

sub new
{
    my $class = shift;
    my $name = shift;
    my $rig = shift;
    my $self = $class->SUPER::new($name,$rig);
    $self->{'_type'} = 'Maximum device'; 
    ### Remove offset from tag list ###
    my @list = ();
    foreach (@{$self->{'_setup_tags'}})
    {
	push @list,$_ unless ($_ eq 'offset');
    }
    $self->{'_setup_tags'} = [@list];
    bless $self,$class;
    for (my $x = 0; $x < $self->{'_inputs'}; $x++)
    {
        my $tag = 'input_device_'.$x;
        $self->{'_desc'}->{$tag} = 'Input device '.$x.' factor (multiplied on input device read value before determining maximum)';
    }
    return $self;
}

sub read
{
    my $self = shift;
    my $data = undef;
    if (@_)
    {
        $data = $_[0];
    }
    else
    {
        $data = RFC::Cache->new();
        $data->debug($self->debug) if ($self->debug);
    }
    $self->check_cache($data);
    my $max = -32768;
    my $cached = $data->get($self->type,$self->name);
    if (defined($cached))
    {
        $self->writedebug("Using already measured value for ".$self->name) if ($self->debug > 3); 
        $max = $cached;
    }
    else
    {
	$self->init();
	my @list = ();
	for (my $x = 0; $x < $self->{'_inputs'}; $x++)
	{
	    my $tag = '_dev_'.$x;
	    my $val = $self->{$tag}->read_named($self->{'_read_function'},$data) if ($self->{$tag});
	    if ($self->{$tag}->can('cutoff_set'))
	    {
		$val = 0 if ($val < $self->{$tag}->cutoff_set());
	    }
	    push @list,$val * $self->{'_input_device_factor_'.$x} if ($self->{$tag});
	}
	@list = sort {$a <=> $b} @list;
	$max = $list[-1] if (@list);
	$data->set($self->type,$self->name,$max);
    }
    $self->writedebug("Max of inputs was: $max") if ($self->debug);
    return $max;
}

return 1;
