#!/usr/bin/env perl 

use strict;
use warnings;
use utf8;

use Data::Dumper;
use POSIX 'isatty';

use lib 'perllib';
use XML::Tidy;

my $hash;
my %commands;
# Command structure:
# key: Case-insensitive name of the command
# val: Array of info about the command
#      [0]: Description of the command
#      [1]: Function that is run for the command
#      [2]: Number of arguments of the command (if any)
#      [3]: True if the command has a variable number of args
%commands = (
    help => [
        'Prints help about the available commands',
        sub {
            for my $cmd (sort keys %commands) {
                printf "%-10s %s\n", $cmd, $commands{$cmd}[0];
            }
        },
    ],
    clear => [
        'Clears the internal state',
        sub {
            undef $hash;
            $hash = {};
        }
    ],
    read => [
        "Parses XML from a file into the internal state\n  input_file (- for STDIN)",
        sub {
            my $file = shift @ARGV;
            my $new;
            if ($file eq '-') {
                if (isatty *STDIN) {
                    print STDERR "Reading XML from STDIN. Press ^D to end\n";
                }
                $new = read_xml *STDIN;
            } else {
                open FH, '<', $file;
                $new = read_xml *FH;
                close FH;
            }
            # Append the new data to the current data
            for my $key (keys %$new) {
                if ($key eq '.attr' and defined $hash->{'.attr'}) {
                    for my $attr ($new->{'.attr'}) {
                        $hash->{'.attr'}->{$attr} = $new->{'.attr'}->{$attr};
                    }
                } elsif (defined $hash->{$key}) {
                    if (ref $hash->{$key} ne 'ARRAY') {
                        $hash->{$key} = [ $hash->{$key} ];
                    }
                    push @{$hash->{$key}},
                        (ref $new->{$key} eq 'ARRAY' ? @{$new->{$key}} : $new->{$key});
                } else {
                    $hash->{$key} = $new->{$key};
                }
            }
        },
        1,
    ],
    write => [
        "Writes the current internal state as XML to a file\n  output_file (- for STDOUT)",
        sub {
            my $file = shift @ARGV;
            if ($file eq '-') {
                write_xml $hash, *STDOUT;
            } else {
                open FH, '>', $file;
                write_xml $hash, *FH;
                close FH;
            }
        },
        1,
    ],
    debug => [
        'Dumps the current internal state to stdout',
        sub {
            print Dumper $hash;
        },
    ],
    count => [
        'Returns the number of servlets in the current internal state',
        sub {
            my $count = 0;
            if (defined $hash->{servlet}) {
                $count = 1;
                my $servlets = $hash->{servlet};
                if (ref $servlets eq 'ARRAY') {
                    $count = scalar @{$hash->{servlet}};
                }
            }
            print $count, "\n";
        },
    ],
    list => [
        'Lists the servlets in the current internal state',
        sub {
            if (defined $hash->{servlet}) {
                my $servlets = $hash->{servlet};
                if (ref $servlets eq 'ARRAY') {
                    for my $servlet (@{$hash->{servlet}}) {
                        print $servlet->{'servlet-name'}, "\n";
                    }
                } else {
                    print $servlets->{'servlet-name'}, "\n";
                }
            } else {
                print STDERR "No servlets found. Was valid XML provided?\n";
            }
        },
    ],
    remove => [
        'Removes a servlet from the current internal state',
        sub {
            my $name = shift @ARGV;
            if (defined $hash->{servlet}) {
                my $servlets = $hash->{servlet};
                if (ref $servlets eq 'ARRAY') {
                    my @array = grep { $_->{'servlet-name'} ne $name } @$servlets;
                    $hash->{servlet} = \@array;
                } elsif ($servlets->{'servlet-name'} eq $name) {
                    delete $hash->{servlet};
                }
            }
        },
        1,
    ],
    add => [
        "Adds a new servlet to the current internal state\n  name description class [param=value param2=value...] ;",
        sub {
            my %servlet = (
                'servlet-name' => shift @ARGV,
                'description'  => shift @ARGV,
                'servlet-class'=> shift @ARGV,
                'init-param'   => [],
            );
            while (@ARGV) {
                my $param = shift @ARGV;
                $param eq ';' and last;
                my ($key, $value) = split '=', $param, 2;
                (defined $key and defined $value) or die "Expected params in form 'param=value'\n";
                push @{$servlet{'init-param'}}, {
                    'param-name'  => $key,
                    'param-value' => $value,
                };
            }
            if (defined $hash->{servlet}) {
                unless (ref $hash->{servlet} eq 'ARRAY') {
                    $hash->{servlet} = [ $hash->{servlet} ];
                }
                push @{$hash->{servlet}}, \%servlet;
            } else {
                $hash->{servlet} = \%servlet;
            }
        },
        3,
        1,
    ],
);

# Check that all given commands are valid
my $argc = 0;
my $varargs = 0;
for my $cmd (@ARGV) {
    if ($varargs and $cmd eq ';') {
        $varargs = 0;
    } elsif ($argc > 0) {
        # skip arguments to a previous command
        $argc --;
    } elsif ($varargs) {
    } elsif (defined $commands{$cmd}) {
        # get the argument count of a valid command
        $argc = @{$commands{$cmd}}[2];
        defined $argc or ($argc = 0);
        $varargs = @{$commands{$cmd}}[3];
    } else {
        # invalid command
        print STDERR "Valid commands are:\n";
        @{$commands{help}}[1]->();
        die "Invalid command '" . $cmd . "'\n";
    }
}
$argc != 0 and die "Expected $argc more argument" . ($argc != 1 ? "s" : "") . "\n";
$varargs and die "Unclosed vararg command. Add an argument ';' to close the varargs\n";

# Run the commands
while (@ARGV) {
    @{$commands{lc shift}}[1]->();
}
