######################################################################
# SPDX-License-Identifier: EPICS
# EPICS BASE is distributed subject to a Software License Agreement
# found in file LICENSE that is included with this distribution.
######################################################################

package DBD::Recfield;
use DBD::Base;
our @ISA = qw(DBD::Base);

use strict;

# The hash value is a regexp that matches all legal values of this field
# NB: The regexps are not currently used, and are wrong for some types.
our %field_types = (
    DBF_STRING   => qr/.{0,40}/,
    DBF_CHAR     => $RXintx,
    DBF_UCHAR    => $RXuintx,
    DBF_SHORT    => $RXintx,
    DBF_USHORT   => $RXuintx,
    DBF_LONG     => $RXintx,
    DBF_ULONG    => $RXuintx,
    DBF_INT64    => $RXintx,
    DBF_UINT64   => $RXuintx,
    DBF_FLOAT    => $RXnum,
    DBF_DOUBLE   => $RXnum,
    DBF_ENUM     => qr/.*/,
    DBF_MENU     => qr/.*/,
    DBF_DEVICE   => qr/.*/,
    DBF_INLINK   => qr/.*/,
    DBF_OUTLINK  => qr/.*/,
    DBF_FWDLINK  => qr/.*/,
    DBF_NOACCESS => qr//
);

# The hash value is a regexp that matches all legal values of this attribute
our %field_attrs = (
    asl         => qr/^ASL[01]$/,
    initial     => qr/^.*$/,
    promptgroup => qr/^.*$/,
    prompt      => qr/^.*$/,
    special     => qr/^(?:SPC_\w+|\d{3,})$/,
    pp          => qr/^(?:TRUE|FALSE)$/,
    interest    => qr/^\d+$/,
    base        => qr/^(?:DECIMAL|HEX)$/,
    size        => qr/^\d+$/,
    extra       => qr/^.*$/,
    menu        => qr/^$RXident$/,
    prop        => qr/^(?:YES|NO)$/
);

# Convert old promptgroups into new-style
my %promptgroupMap = (
    GUI_COMMON   => '10 - Common',
    GUI_ALARMS   => '70 - Alarm',
    GUI_BITS1    => '41 - Bits (1)',
    GUI_BITS2    => '42 - Bits (2)',
    GUI_CALC     => '30 - Action',
    GUI_CLOCK    => '30 - Action',
    GUI_COMPRESS => '30 - Action',
    GUI_CONVERT  => '60 - Convert',
    GUI_DISPLAY  => '80 - Display',
    GUI_HIST     => '30 - Action',
    GUI_INPUTS   => '40 - Input',
    GUI_LINKS    => '40 - Link',
    GUI_MBB      => '30 - Action',
    GUI_MOTOR    => '30 - Action',
    GUI_OUTPUT   => '50 - Output',
    GUI_PID      => '30 - Action',
    GUI_PULSE    => '30 - Action',
    GUI_SELECT   => '40 - Input',
    GUI_SEQ1     => '51 - Output (1)',
    GUI_SEQ2     => '52 - Output (2)',
    GUI_SEQ3     => '53 - Output (3)',
    GUI_SUB      => '30 - Action',
    GUI_TIMER    => '30 - Action',
    GUI_WAVE     => '30 - Action',
    GUI_SCAN     => '20 - Scan',
);

sub new {
    my ($class, $name, $type) = @_;
    dieContext("Illegal field type '$type', valid field types are:",
        sort keys %field_types) unless exists $field_types{$type};
    my $this = {};
    bless $this, "${class}::${type}";
    return $this->init($name, $type);
}

sub init {
    my ($this, $name, $type) = @_;
    $this->SUPER::init($name, "record field");
    dieContext("Illegal field type '$type', valid field types are:",
        sort keys %field_types) unless exists $field_types{$type};
    $this->{DBF_TYPE} = $type;
    $this->{ATTR_INDEX} = {};
    $this->{COMMENTS} = [];
    return $this;
}

sub dbf_type {
    return shift->{DBF_TYPE};
}

sub set_number {
    my ($this, $number) = @_;
    $this->{NUMBER} = $number;
}

sub number {
    return shift->{NUMBER};
}

sub add_attribute {
    my ($this, $attr, $value) = @_;
    $value = $promptgroupMap{$value}
        if $attr eq 'promptgroup' && exists $promptgroupMap{$value};
    my $match = $field_attrs{$attr};
    if (defined $match) {
        dieContext("Bad value '$value' for field attribute '$attr'")
            unless $value =~ m/$match/;
    }
    else {
        warnContext("Unknown field attribute '$attr' with value '$value'; " .
            "known attributes are:",
            join(", ", sort keys %field_attrs));
    }
    $this->{ATTR_INDEX}->{$attr} = $value;
}

sub attributes {
    return shift->{ATTR_INDEX};
}

sub attribute {
    my ($this, $attr) = @_;
    return $this->attributes->{$attr};
}

sub equals {
    my ($l, $r) = @_;
    return 1 if $l eq $r;
    return 0 if
        $l->{NAME} ne $r->{NAME} ||
        $l->{DBF_TYPE} ne $r->{DBF_TYPE};
    my ($la, $ra) = ($l->{ATTR_INDEX}, $r->{ATTR_INDEX});
    my @keys = sort keys %$la;
    return 0 if join(',', @keys) ne join(',', sort keys %$ra);
    foreach my $k (@keys) {
        return 0 if $la->{$k} ne $ra->{$k};
    }
    return 1;
}

sub check_valid {
    my ($this) = @_;
    my $name = $this->name;
    my $default = $this->attribute("initial");
    dieContext("Default value '$default' is invalid for field '$name'")
        if (defined($default) and !$this->legal_value($default));
}

sub add_comment {
    my $this = shift;
    push @{$this->{COMMENTS}}, @_;
}

sub comments {
    return @{shift->{COMMENTS}};
}


# The C structure member name is usually the field name converted to
# lower-case.  However if that is a reserved word, use the original.
sub C_name {
    my ($this) = @_;
    my $name = lc $this->name;
    $name = $this->name
        if is_reserved($name);
    return $name;
}

sub toDeclaration {
    my ($this, $ctype) = @_;
    my $name = $this->C_name;
    my $result = sprintf "    %-19s %-12s", $ctype, "$name;";
    my $prompt = $this->attribute('prompt');
    $result .= "/* $prompt */" if defined $prompt;
    return $result;
}


################################################################################

package DBD::Recfield::DBF_STRING;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    my ($this, $value) = @_;
    return (length $value < $this->attribute('size'));
    # NB - we use '<' to allow space for the terminating nil byte
}

sub check_valid {
    my ($this) = @_;
    my $name = $this->name;
    dieContext("Size missing for DBF_STRING field '$name'")
        unless exists $this->attributes->{'size'};
    $this->SUPER::check_valid;
}

sub toDeclaration {
    my ($this) = @_;
    my $name = lc $this->name;
    my $size = $this->attribute('size');
    my $result = sprintf "    %-19s %-12s", 'char', "${name}[${size}];";
    my $prompt = $this->attribute('prompt');
    $result .= "/* $prompt */" if defined $prompt;
    return $result;
}


################################################################################

package DBD::Recfield::DBF_CHAR;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    my ($this, $value) = @_;
    $value =~ s/^ ( $RXhex | $RXoct ) $/ oct($1) /xe;
    return ($value =~ m/^ $RXint $/x and
            $value >= -128 and
            $value <= 127);
}

sub toDeclaration {
    return shift->SUPER::toDeclaration("epicsInt8");
}


################################################################################

package DBD::Recfield::DBF_UCHAR;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    my ($this, $value) = @_;
    $value =~ s/^ ( $RXhex | $RXoct ) $/ oct($1) /xe;
    return ($value =~ m/^ $RXuint $/x and
            $value >= 0 and
            $value <= 255);
}

sub toDeclaration {
    return shift->SUPER::toDeclaration("epicsUInt8");
}


################################################################################

package DBD::Recfield::DBF_SHORT;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    my ($this, $value) = @_;
    $value =~ s/^ ( $RXhex | $RXoct ) $/ oct($1) /xe;
    return ($value =~ m/^ $RXint $/x and
            $value >= -32768 and
            $value <= 32767);
}

sub toDeclaration {
    return shift->SUPER::toDeclaration("epicsInt16");
}


################################################################################

package DBD::Recfield::DBF_USHORT;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    my ($this, $value) = @_;
    $value =~ s/^ ( $RXhex | $RXoct ) $/ oct($1) /xe;
    return ($value =~ m/^ $RXuint $/x and
            $value >= 0 and
            $value <= 65535);
}

sub toDeclaration {
    return shift->SUPER::toDeclaration("epicsUInt16");
}


################################################################################

package DBD::Recfield::DBF_LONG;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    my ($this, $value) = @_;
    $value =~ s/^ ( $RXhex | $RXoct ) $/ oct($1) /xe;
    return ($value =~ m/^ $RXint $/x);
}

sub toDeclaration {
    return shift->SUPER::toDeclaration("epicsInt32");
}


################################################################################

package DBD::Recfield::DBF_ULONG;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    my ($this, $value) = @_;
    $value =~ s/^ ( $RXhex | $RXoct ) $/ oct($1) /xe;
    return ($value =~ m/^ $RXuint $/x and
            $value >= 0);
}

sub toDeclaration {
    return shift->SUPER::toDeclaration("epicsUInt32");
}


################################################################################

package DBD::Recfield::DBF_INT64;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    my ($this, $value) = @_;
    $value =~ s/^ ( $RXhex | $RXoct ) $/ oct($1) /xe;
    return ($value =~ m/^ $RXint $/x);
}

sub toDeclaration {
    return shift->SUPER::toDeclaration("epicsInt64");
}


################################################################################

package DBD::Recfield::DBF_UINT64;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    my ($this, $value) = @_;
    $value =~ s/^ ( $RXhex | $RXoct ) $/ oct($1) /xe;
    return ($value =~ m/^ $RXuint $/x and
            $value >= 0);
}

sub toDeclaration {
    return shift->SUPER::toDeclaration("epicsUInt64");
}


################################################################################

package DBD::Recfield::DBF_FLOAT;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    my ($this, $value) = @_;
    return ($value =~ m/^ $RXnum $/x);
}

sub toDeclaration {
    return shift->SUPER::toDeclaration("epicsFloat32");
}


################################################################################

package DBD::Recfield::DBF_DOUBLE;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    my ($this, $value) = @_;
    return ($value =~ m/^ $RXnum $/x);
}

sub toDeclaration {
    return shift->SUPER::toDeclaration("epicsFloat64");
}


################################################################################

package DBD::Recfield::DBF_ENUM;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    return 1;
}

sub toDeclaration {
    return shift->SUPER::toDeclaration("epicsEnum16");
}


################################################################################

package DBD::Recfield::DBF_MENU;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    # FIXME: If we know the menu name and the menu exists, check further
    return 1;
}

sub check_valid {
    my ($this) = @_;
    my $name = $this->name;
    dieContext("Menu name missing for DBF_MENU field '$name'")
        unless defined($this->attribute("menu"));
    $this->SUPER::check_valid;
}

sub toDeclaration {
    return shift->SUPER::toDeclaration("epicsEnum16");
}


################################################################################

package DBD::Recfield::DBF_DEVICE;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    return 1;
}

sub toDeclaration {
    return shift->SUPER::toDeclaration("epicsEnum16");
}


################################################################################

package DBD::Recfield::DBF_INLINK;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    return 1;
}

sub toDeclaration {
    return shift->SUPER::toDeclaration("DBLINK");
}


################################################################################

package DBD::Recfield::DBF_OUTLINK;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    return 1;
}

sub toDeclaration {
    return shift->SUPER::toDeclaration("DBLINK");
}


################################################################################

package DBD::Recfield::DBF_FWDLINK;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    return 1;
}

sub toDeclaration {
    return shift->SUPER::toDeclaration("DBLINK");
}


################################################################################

package DBD::Recfield::DBF_NOACCESS;

use DBD::Base;
our @ISA = qw(DBD::Recfield);

sub legal_value {
    my ($this, $value) = @_;
    return ($value eq '');
}

sub check_valid {
    my ($this) = @_;
    my $name = $this->name;
    dieContext("Type information missing for DBF_NOACCESS field '$name'")
        unless defined($this->attribute("extra"));
    $this->SUPER::check_valid;
}

sub toDeclaration {
    my ($this) = @_;
    my $extra = $this->attribute('extra');
    my $result = sprintf "    %-31s ", "$extra;";
    my $prompt = $this->attribute('prompt');
    $result .= "/* $prompt */" if defined $prompt;
    return $result;
}

1;
