#*************************************************************************
# Copyright (c) 2010 UChicago Argonne LLC, as Operator of Argonne
#     National Laboratory.
# 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 EPICS::macLib::entry;

sub new ($$) {
    my $class = shift;
    my $this = {
        name => shift,
        type => shift,
        raw => '',
        val => '',
        visited => 0,
        error => 0,
    };
    bless $this, $class;
    return $this;
}

sub report ($) {
    my ($this) = @_;
    return unless defined $this->{raw};
    printf "%1s %-16s %-16s %s\n",
        ($this->{error} ? '*' : ' '), $this->{name}, $this->{raw}, $this->{val};
}


package EPICS::macLib;

use Carp;

sub new ($@) {
    my $proto = shift;
    my $class = ref($proto) || $proto;
    my $this = {
        dirty => 0,
        noWarn => 0,
        macros => [{}], # [0] is current scope, [1] is parent etc.
    };
    bless $this, $class;
    $this->installList(@_);
    return $this;
}

sub installList ($@) {
    # Argument is a list of strings which are arguments to installMacros
    my $this = shift;
    while (@_) {
        $this->installMacros(shift);
    }
}

sub installMacros ($$) {
    # Argument is a string: a=1,b="2",c,d='hello'
    my $this = shift;
    $_ = shift;
    until (defined pos($_) and pos($_) == length($_)) {
        m/\G \s* /xgc;    # Skip whitespace
        if (m/\G ( [A-Za-z0-9_-]+ ) \s* /xgc) {
            my ($name, $val) = ($1);
            if (m/\G = \s* /xgc) {
                # The value follows, handle quotes and escapes
                until (pos($_) == length($_)) {
                    if (m/\G , /xgc) { last; }
                    elsif (m/\G ' ( ( [^'] | \\ ' )* ) ' /xgc) { $val .= $1; }
                    elsif (m/\G " ( ( [^"] | \\ " )* ) " /xgc) { $val .= $1; }
                    elsif (m/\G \\ ( . ) /xgc) { $val .= $1; }
                    elsif (m/\G ( . ) /xgc) { $val .= $1; }
                    else { die "How did I get here?"; }
                }
                $this->putValue($name, $val);
            } elsif (m/\G , /xgc or (pos($_) == length($_))) {
                $this->putValue($name, undef);
            } else {
                warn "How did I get here?";
            }
        } elsif (m/\G ( .* )/xgc) {
            croak "Can't find a macro definition in '$1'";
        } else {
            last;
        }
    }
}

sub putValue ($$$) {
    my ($this, $name, $raw) = @_;
    if (exists $this->{macros}[0]{$name}) {
        if (!defined $raw) {
            delete $this->{macros}[0]{$name};
        } else {
            $this->{macros}[0]{$name}{raw} = $raw;
        }
    } else {
        my $entry = EPICS::macLib::entry->new($name, 'macro');
        $entry->{raw} = $raw;
        $this->{macros}[0]{$name} = $entry;
    }
    $this->{dirty} = 1;
}

sub pushScope ($) {
    my ($this) = @_;
    unshift @{$this->{macros}}, {};
}

sub popScope ($) {
    my ($this) = @_;
    shift @{$this->{macros}};
}

sub suppressWarning($$) {
    my ($this, $suppress) = @_;
    $this->{noWarn} = $suppress;
}

sub expandString($$) {
    my ($this, $src) = @_;
    $this->_expand;
    (my $name = $src) =~ s/^ (.{20}) .* $/$1.../xs;
    my $entry = EPICS::macLib::entry->new($name, 'string');
    my $result = $this->_translate($entry, 0, $src);
    return $result unless $entry->{error};
    return $this->{noWarn} ? $result : undef;
}

sub reportMacros ($) {
    my ($this) = @_;
    $this->_expand;
    print "Macro report\n============\n";
    foreach my $scope (@{$this->{macros}}) {
        foreach my $name (keys %{$scope}) {
            my $entry = $scope->{$name};
            $entry->report;
        }
    } continue {
        print " -- scope ends --\n";
    }
}


# Private routines, not intended for public use

sub _expand ($) {
    my ($this) = @_;
    return unless $this->{dirty};
    foreach my $scope (@{$this->{macros}}) {
        foreach my $name (keys %{$scope}) {
            my $entry = $scope->{$name};
            $entry->{val} = $this->_translate($entry, 1, $entry->{raw});
        }
    }
    $this->{dirty} = 0;
}

sub _lookup ($$$$$) {
    my ($this, $name) = @_;
    foreach my $scope (@{$this->{macros}}) {
        if (exists $scope->{$name}) {
            return undef   # Macro marked as deleted
                unless defined $scope->{$name}{raw};
            return $scope->{$name};
        }
    }
    return undef;
}

sub _translate ($$$$) {
    my ($this, $entry, $level, $str) = @_;
    return $this->_trans($entry, $level, '', \$str);
}

sub _trans ($$$$$) {
    my ($this, $entry, $level, $term, $R) = @_;
    return $$R
        if (!defined $$R or
            $$R =~ m/\A [^\$]* \Z/x);   # Short-circuit if no macros
    my $quote = 0;
    my $val;
    until (defined pos($$R) and pos($$R) == length($$R)) {
        if ($term and ($$R =~ m/\G (?= [$term] ) /xgc)) {
            last;
        }
        if ($$R =~ m/\G \$ ( [({] ) /xgc) {
            my $macEnd = $1;
            $macEnd =~ tr/({/)}/;
            my $name2 = $this->_trans($entry, $level+1, "=$macEnd", $R);
            my $entry2 = $this->_lookup($name2);
            if (!defined $entry2) {             # Macro not found
                if ($$R =~ m/\G = /xgc) {       # Use default value given
                    $val .= $this->_trans($entry, $level+1, $macEnd, $R);
                } else {
                    unless ($this->{noWarn}) {
                        $entry->{error} = 1;
                        printf STDERR "macLib: macro '%s' is undefined (expanding %s '%s')\n",
                            $name2, $entry->{type}, $entry->{name};
                    }
                    $val .= "\$($name2)";
                }
                $$R =~ m/\G [$macEnd] /xgc;     # Discard close bracket
            } else {                            # Macro found
                if ($entry2->{visited}) {
                    $entry->{error} = 1;
                    printf STDERR "macLib: %s '%s' is recursive (expanding %s '%s')\n",
                        $entry->{type}, $entry->{name}, $entry2->{type}, $entry2->{name};
                    $val .= "\$($name)";
                } else {
                    if ($$R =~ m/\G = /xgc) {   # Discard default value
                        local $this->{noWarn} = 1; # Temporarily kill warnings
                        $this->_trans($entry, $level+1, $macEnd, $R);
                    }
                    $$R =~ m/\G [$macEnd] /xgc; # Discard close bracket
                    if ($this->{dirty}) {       # Translate raw value
                        $entry2->{visited} = 1;
                        $val .= $this->_trans($entry, $level+1, '', \$entry2->{raw});
                        $entry2->{visited} = 0;
                    } else {
                        $val .= $entry2->{val}; # Here's one I made earlier...
                    }
                }
            }
        } elsif ($level > 0) {  # Discard quotes and escapes
            if ($quote and $$R =~ m/\G $quote /xgc) {
                $quote = 0;
            } elsif ($$R =~ m/\G ( ['"] ) /xgc) {
                $quote = $1;
            } elsif ($$R =~ m/\G \\? ( . ) /xgc) {
                $val .= $1;
            } else {
                warn "How did I get here? level=$level";
            }
        } else {                # Level 0
            if ($$R =~ m/\G \\ ( . ) /xgc) {
                $val .= "\\$1";
            } elsif ($$R =~ m/\G ( [^\\\$'")}]* ) /xgc) {
                $val .= $1;
            } elsif ($$R =~ m/\G ( . ) /xgc) {
                $val .= $1;
            } else {
                warn "How did I get here? level=$level";
            }
        }
    }
    return $val;
}

1;
