#!/usr/bin/perl

# Puppet Cleaner - parsed and reformats puppet manifests
# Copyright (C) 2012  Andrew Gaffney <andrew@agaffney.org>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

use strict;
use warnings;

package PuppetParser;

use Parse::Lex;
use Data::Dumper;

my @keywords = (
	"case",
	"class",
	"default",
	"define",
	"import",
	"if",
	"elsif",
	"else",
	"inherits",
	"node",
	"and",
	"or",
	"undef",
#	"false",
#	"true",
	"in",
	"include",
);

my @tokens = (
	"LLCOLLECT" => '<<\|',
	"RRCOLLECT" => '\|>>',
	"LCOLLECT" => '<\|',
	"RCOLLECT" => '\|>',
	"LBRACK" => '\[',
	"RBRACK" => '\]',
	"LBRACE" => '\{',
	"RBRACE" => '\}',
	"LPAREN" => '\(',
	"RPAREN" => '\)',
	"FARROW" => '=>',
	"APPENDS" => '\+=',
	"ISEQUAL" => '==',
	"GREATEREQUAL" => '>=',
	"GREATERTHAN" => '>',
	"LESSTHAN" => '<',
	"LESSEQUAL" => '<=',
	"NOTEQUAL" => '!=',
	"MATCH" => '=~',
	"NOMATCH" => '!~',
	"EQUALS" => '=',
	"NOT" => '!',
	"COMMA" => ',',
	"DOT" => '\.',
	"AT" => '@',
	"IN_EDGE" => '->',
	"OUT_EDGE" => '<-',
	"IN_EDGE_SUB" => '~>',
	"OUT_EDGE_SUB" => '<~',
	"SEMIC" => ';',
	"QMARK" => '\?',
	"BACKSLASH" => '\\\\',
	"PARROW" => '\+>',
	"PLUS" => '\+',
	"MINUS" => '-',
	new Parse::Token::Delimited(Name => 'MLCOMMENT', Start => '/[*]', End => '[*]/' ),
	"REGEX", '\/[^/\n]*\/',
	"DIV" => '/',
	"TIMES" => '\*',
	"LSHIFT" => '<<',
	"RSHIFT" => '>>',
	"CLASSREF" => '((::){0,1}[A-Z][-\w]*)+',
	"NUMBER" => '\b(?:0[xX][0-9A-Fa-f]+|0?\d+(?:\.\d+)?(?:[eE]-?\d+)?)\b',
	"NAME" => '((::)?[a-z0-9][-\w]*)(::[a-z0-9][-\w]*)*',
	"COMMENT" => '#.*',
#	"MLCOMMENT", qw(/\*(.*?)\*/), #m
	"RETURN", '\n',
	"COLON" => ':',
	"DOLLAR_VAR", '\$(::)?([-\w]+::)*[-\w]+',
	"VARIABLE", '(::)?([-\w]+::)*[-\w]+',
	new Parse::Token::Quoted(Name => 'SQUOTES', Handler => 'string', Quote => "'"),
	new Parse::Token::Quoted(Name => 'DQUOTES', Handler => 'string', Escape => '\\', Quote => '"'),
	"WHITESPACE" => '\s+',
	qw(ERROR  .*), sub {
		die "unknown token: " . $_[1] . "\n";
	}
);

my @object_classes = (
	'PuppetParser::Newline',
	'PuppetParser::Comment',
	'PuppetParser::MultilineComment',
	'PuppetParser::Class',
	'PuppetParser::FunctionCall',
	'PuppetParser::DependencyChain',
	'PuppetParser::Resource',
	'PuppetParser::IfStatement',
	'PuppetParser::Include',
	'PuppetParser::VarAssignment',
	'PuppetParser::Node',
	'PuppetParser::Define',
	'PuppetParser::CaseStatement',
#	'PuppetParser::CaseCondition',
#	'PuppetParser::ResourceRef',
);
my $default_object_class = 'PuppetParser::Simple';

sub new {
	my($class, %args) = @_;
	my $self = {
		parsed_tokens => [],
		num_parsed_tokens => 0,
		token_idx => 0,
		lexer => undef,
		buffer => '',
		indent => 0,
	};
	for(keys %args) {
		$self->{$_} = $args{$_};
	}
	bless($self, $class);
#	for(@object_classes) {
#		eval "use $_;";
#	}
	return $self;
}

sub output {
	my ($self, $output) = @_;
	my $buffer = $self->{tree}->output_children();
	$buffer =~ s/^\n+//s;
	$buffer =~ s/\n+$/\n/s;
	if(defined $output && $output ne '') {
		open FOO, "> $output" or die "Could not open output file $output: $!";
		print FOO $buffer;
		close FOO;
	} else {
		print $buffer;
	}
}

sub error {
	my ($self, $msg) = @_;
	$self->show_call_stack();
	print Dumper($self->cur_token());
	print STDERR "ERROR: ${msg} on line " . $self->cur_token()->{line} . " in file " . $self->{file} . "\n";
	exit 1;
}

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

	my $file = $self->{file};
	open FOO, "< $file" or die "Can't open file $file: $!";

	#Parse::Lex->trace;
	#Parse::Lex->skip('');
	$self->{lexer} = Parse::Lex->new(@tokens);
	$self->{lexer}->from(\*FOO);

	# Read in and store tokens
	$self->read_tokens();

	# Start the process
	$self->{tree} = PuppetParser::Object->new(type => 'ROOT', level => -1, parser => $self);
	while($self->cur_token()) {
		my $foo = $self->scan_for_object($self->{tree});
		push @{$self->{tree}->{contents}}, $foo;
	}
}

sub transform_token_type {
	my ($self, $type, $text) = @_;
	if($type eq 'NAME') {
		if(grep(/^${text}$/, @keywords)) {
			$type = uc($text);
		}
	}
	return $type;
}

sub read_tokens {
	my ($self) = @_;
	while(1) {
		my $token = $self->{lexer}->next;
		if($self->{lexer}->eoi) {
			last;
		}
		push(@{$self->{parsed_tokens}}, { line => $self->{lexer}->line, type => $self->transform_token_type($token->name, $token->text), text => $token->text });
	}
	$self->{num_parsed_tokens} = scalar(@{$self->{parsed_tokens}});
	$self->{token_idx} = 0;
}

sub eof {
	my ($self) = @_;
	if(!defined $self->cur_token()) {
		return 1;
	}
	return 0;
}

sub cur_token {
	my ($self) = @_;
	return defined($self->{parsed_tokens}->[$self->{token_idx}]) ? $self->{parsed_tokens}->[$self->{token_idx}] : undef;
}

sub show_call_stack {
	my ($self) = @_;
	my ( $path, $line, $subr );
	my $max_depth = 30;
	my $i = 1;
	print "--- Begin stack trace ---\n";
	while ( (my @call_details = (caller($i++))) && ($i<$max_depth) ) {
		print "$call_details[1] line $call_details[2] in function $call_details[3]\n";
	}
	print "--- End stack trace ---\n";
}

sub next_token {
	my ($self) = @_;
	$self->{token_idx}++;
#	if($self->cur_token()->{type} eq 'COMMENT') {
#		print "next_token(): Found a comment - " . $self->cur_token()->{text} . "\n";
#		$self->show_call_stack();
#	}
	return $self->cur_token();
}

sub get_token_idx {
	my ($self) = @_;
	return $self->{token_idx};
}

sub set_token_idx {
	my ($self, $idx) = @_;
	$self->{token_idx} = $idx;
}

sub scan_for_token {
	my ($self, $types, $ignore) = @_;
	if(!defined $ignore) {
		$ignore = [];
	}
	TOKEN: while(my $token = $self->cur_token()) {
		for(@{$ignore}) {
			if($token->{type} eq $_) {
				# The next token is of a type we want to ignore
				$self->next_token();
				next TOKEN;
			}
		}
		for(@{$types}) {
			if($token->{type} eq $_) {
				return 1;
			}
		}
		# Could not find the expected token
		return 0;
	}
}

sub scan_for_object {
	my ($self, $parent) = @_;
	my $token = $self->cur_token();
#	print "Line: ", $token->{line}, ", ";
#	print "Type: ", $token->{type}, ", ";
#	print "Content:->", $token->{text}, "<-\n";
	my $orig_token = $self->get_token_idx();
	my $cur_token = undef;
	for my $package (@object_classes) {
		my $foo = $package->new(parent => $parent);
		if(!defined $foo) {
			next;
		}
		return $foo;
	}
	$self->set_token_idx($orig_token);
	# We don't know how to handle this
	if(defined $self->cur_token()) {
		$self->error("Unexpected token '" . $self->cur_token()->{text} . "'");
	}
}

package PuppetParser::Object;

use Data::Dumper;

our %defaults = (
	inner_spacing => 0,
	outer_spacing => 0,
);

sub new {
	my ($class, %args) = @_;
	my $self = { contents => [] };
	bless($self, $class);
	for(keys %args) {
		$self->{$_} = $args{$_};
	}
	$self->apply_defaults();
	my $parse = $self->parse();
	if(defined $parse && $parse == 0) {
#		print "${class}::new(): no, it's not a $class\n";
		return undef;
	}
	if(!defined $self->{type} || $self->{type} ne 'ROOT') {
		$self->{parser} = undef;
		$self->{parent} = undef;
#		$self->dump();
	}
#	print "${class}::new(): yes, it's a $class\n";
	return $self;
}

sub apply_defaults {
	my ($self, $def) = @_;
	if(!defined $def) {
		$def = \%defaults;
	}
	for(keys %{$def}) {
		if(!defined $self->{$_}) {
			$self->{$_} = $def->{$_};
		}
	}
	if(!defined $self->{level}) {
		if(defined $self->{parent} && defined $self->{parent}->{level}) {
			$self->{level} = $self->parent()->{level} + 1;
		}
	}
	if(!defined $self->{parser}) {
		if(defined $self->{parent} && defined $self->{parent}->{parser}) {
			$self->{parser} = $self->{parent}->{parser};
		}
	}
}

sub get_parser_data {
	return [];
}

sub dump {
	my ($self) = @_;
	my $d = Data::Dumper->new([$self], ['tree']);
	$d->Indent(4);
#	$d->Seen({ '*parser' => $self->{parser} });
	print $d->Dump;
}

sub parser_any {
	my ($self, $parser, $node) = @_;
	my $class = ref $self;
	for my $node (@{$node->{members}}) {
		my $foo = $self->check_parser_node($parser, $node);
		if(defined $foo) {
			return $foo;
		}
	}
	return undef;
}

sub parser_group {
	my ($self, $parser, $node) = @_;
	my $class = ref $self;
	my $ret = {};
	for my $node (@{$node->{members}}) {
		my $foo = $self->check_parser_node($parser, $node);
		if(!defined $foo) {
			if(!defined $node->{optional} || $node->{optional} == 0) {
				return undef;
			}
		}
		for(keys %{$foo}) {
			$ret->{$_} = $foo->{$_};
		}
	}
	return $ret;
}

sub parser_class {
	my ($self, $parser, $node) = @_;
	my $class = ref $self;
	if(ref $node->{class} ne 'ARRAY') {
		$node->{class} = [ $node->{class} ];
	}
	my $ret = {};
	my %args = ( parent => $self );
	if(defined $node->{args}) {
		for(keys %{$node->{args}}) {
			$args{$_} = $node->{args}->{$_};
		}
	}
	my $foo = undef;
	for(@{$node->{class}}) {
		$foo = $_->new(%args);
		if(defined $foo) {
			last;
		}
	}
	if(!defined $foo) {
		return undef;
	}
	if(defined $node->{name}) {
		$ret->{$node->{name}} = $foo;
	}
	return $ret;
}

sub parser_block {
	my ($self, $parser, $node) = @_;
	my $class = ref $self;
	if(!$parser->scan_for_token(['LBRACE'])) {
		return undef;
	}
	$parser->next_token();
	my $ret = { contents => [] };
	while(1) {
		if($self->{parser}->scan_for_token(['RBRACE'])) {
			$self->{parser}->next_token();
			last;
		}
		if($self->{parser}->scan_for_token(['RETURN'])) {
			$self->{parser}->next_token();
			next;
		}
		if($self->{parser}->eof()) {
			$self->{parser}->error("Unexpected end of file");
		}
		my $foo = $self->{parser}->scan_for_object($self);
		if(!defined $foo) {
			return undef;
		}
		push @{$ret->{contents}}, $foo;
	}
	return $ret;
}

sub parser_token {
	my ($self, $parser, $node) = @_;
	my $class = ref $self;
	if(ref $node->{token} ne 'ARRAY') {
		$node->{token} = [ $node->{token} ];
	}
	if($parser->scan_for_token($node->{token})) {
		my $ret = {};
		my $child = PuppetParser::Simple->new(parent => $self);
		if(defined $node->{name}) {
			$ret->{$node->{name}} = $child;
		}
		return $ret;
	}
	return undef;
}

sub check_parser_node {
	my ($self, $parser, $node) = @_;
#	use Data::Dumper;
	my $class = ref $self;
	my $parser_func = 'parser_' . $node->{type};
	if($node->{type} ne 'group' && (!defined $node->{flags} || !defined $node->{flags}->{skip_return} || $node->{flags}->{skip_return} == 1)) {
		while(1) {
			if($parser->scan_for_token(['RETURN'])) {
				$parser->next_token();
			} else {
				last;
			}
		}
	}
	if(!defined $parser->cur_token()) {
		return undef;
	}
#	my $token = $parser->cur_token();
#	print "${class}::check_parser_node(): cur_token - type=" . $token->{type} . ", text=" . $token->{text} . ", line=" . $token->{line} . "\n";
	my $orig_token = $parser->get_token_idx();
	if($self->can($parser_func)) {
		my $ret = {};
		while(1) {
#			print "Calling $parser_func()\n";
			my $foo = $self->$parser_func($parser, $node);
			if(!defined $foo) {
				if(defined $node->{many} && $node->{many} == 1) {
#					print Dumper($ret);
					if(scalar(keys %{$ret}) > 0) {
						last;
					}
				}
				if(!defined $node->{optional} || $node->{optional} == 0) {
					$parser->set_token_idx($orig_token);
					return undef;
				}
				last;
			}
			for(keys %{$foo}) {
				if(defined $ret->{$_}) {
					if(ref $ret->{$_} ne 'ARRAY') {
						$ret->{$_} = [ $ret->{$_} ];
					}
					push @{$ret->{$_}}, $foo->{$_};
				} else {
					$ret->{$_} = $foo->{$_};
				}
			}
#			print "${class}::check_parser_node(): Return now looks like:\n";
#			print Dumper($ret);
			if(!defined $node->{many} || $node->{many} == 0) {
				last;
			}
		}
		return $ret;
	}
	print STDERR "ERROR: Parsing function ${parser_func}() doesn't exist yet\n";
	return undef;
}

sub parse {
	my ($self) = @_;
	my $foo = $self->check_parser_node($self->{parser}, { type => 'group', members => $self->get_parser_data() });
	if(!defined $foo) {
		return 0;
	}
	for(keys %{$foo}) {
		$self->{$_} = $foo->{$_};
	}
	return 1;
}

sub parent {
	my ($self) = @_;
	return $self->{parent};
}

sub indent {
	my ($self, $level) = @_;
	if(!defined $level) {
		$level = $self->{level};
	}
	return '  ' x $level;
}

sub nl {
	return "\n";
}

sub output {
	my ($self) = @_;
	return $self->indent() . "<Placeholder for " . $self . ">" . $self->nl();
}

sub output_children {
	my ($self) = @_;
	my $buf = '';
	if($self->{inner_spacing}) {
		$buf .= "\n";
	}
	if(!defined $self->{contents}) {
		return '';
	}
	for(@{$self->{contents}}) {
		my $child_output .= $_->output();
		if($_->{outer_spacing}) {
			$child_output = "\n" . $child_output . "\n";
		}
		$buf .= $child_output;
	}
	if($self->{inner_spacing}) {
		$buf .= "\n";
	}
	# Formatting fixups
	$buf =~ s/\n{3,}/\n\n/gs;
	$buf =~ s/}\n\n\s*(else|elsif) /} $1 /gs;
	return $buf;
}

package PuppetParser::Simple;

our @ISA = 'PuppetParser::Object';

sub parse {
	my ($self) = @_;
	my $token = $self->{parser}->cur_token();
	$self->{type} = $token->{type};
	$self->{text} = $token->{text};
	$self->{parser}->next_token();
}

sub output {
	# This is a shell for simple objects. This will be overriden in more complex types
	my ($self) = @_;
	return defined $self->{text} ? $self->{text} : '';
}

package PuppetParser::Newline;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'token', token => 'RETURN', flags => { skip_return => 0 } },
	];
	return $parser_data;
}

sub output {
	return '';
}

package PuppetParser::Comment;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'token', token => 'COMMENT', name => 'comment' },
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	my $comment = $self->{comment}->output();
	$comment =~ s/^[#\s]+//;
	return $self->indent() . '# ' . $comment . $self->nl();
}

package PuppetParser::MultilineComment;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'token', token => 'MLCOMMENT', name => 'comment' },
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	return $self->indent() . $self->{comment}->output() . $self->nl();
}

package PuppetParser::ArrayRef;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'token', token => 'DOLLAR_VAR', name => 'varname' },
		{ type => 'token', token => 'LBRACK' },
		{ type => 'class', class => 'PuppetParser::Expression', args => { term => ['RBRACK'] }, name => 'inner' },
		{ type => 'token', token => 'RBRACK' },
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	my $buf = $self->{varname}->output() . '[' . $self->{inner}->output() . ']';
	return $buf;
}

package PuppetParser::ResourceRef;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'token', token => ['CLASSREF'], name => 'restype' },
		{ type => 'any', members => [
			{ type => 'group', members => [
				{ type => 'token', token => ['LBRACK'] },
				{ type => 'class', class => 'PuppetParser::Expression', args => { term => ['RBRACK'] }, name => 'inner' },
				{ type => 'token', token => ['RBRACK'] },
			]},
			{ type => 'class', class => 'PuppetParser::Collect', name => 'collect' },
		]},
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	my $buf = $self->{restype}->output();
	if(defined $self->{collect}) {
		$buf .= ' ' . $self->{collect}->output();
	} else {
		$buf .= '[' . $self->{inner}->output() . ']';
	}
	return $buf;
}

package PuppetParser::List;

our @ISA = 'PuppetParser::Object';

sub apply_defaults {
	my ($self) = @_;
	$self->SUPER::apply_defaults({ start => 'LBRACK', end => 'RBRACK' });
}

sub get_parser_data {
	my $parser_data = [
		{ type => 'token', token => 'LBRACK' },
		{ type => 'class', class => ['PuppetParser::ListElement', 'PuppetParser::Comment'], many => 1, name => 'items' },
		{ type => 'token', token => ['RBRACK'] },
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	my $buf = '[' . $self->nl();
	if(ref $self->{items} ne 'ARRAY') {
		$self->{items} = [ $self->{items} ];
	}
	for(@{$self->{items}}) {
		if($_->isa('PuppetParser::Comment')) {
			$buf .= $_->output();
		} else {
			$buf .= $self->indent($self->{level} + 1) . $_->output() . $self->nl();
		}
	}
	$buf .= $self->indent() . ']';
	return $buf;
}

package PuppetParser::Hash;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'token', token => 'LBRACE' },
		{ type => 'class', class => ['PuppetParser::KeyValuePair', 'PuppetParser::Comment'], name => 'items', many => 1 },
		{ type => 'token', token => 'RBRACE' },
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	my $max_key_len = 0;
	if(ref $self->{items} ne 'ARRAY') {
		$self->{items} = [ $self->{items} ];
	}
	for(@{$self->{items}}) {
		if($_->can('key_len')) {
			my $key_len = $_->key_len();
			if($key_len > $max_key_len) {
				$max_key_len = $key_len;
			}
		}
	}
	my $buf = '{';
	if(scalar(@{$self->{items}}) > 0) {
		$buf .= $self->nl();
	}
	for(@{$self->{items}}) {
		if($_->can('set_max_key_len')) {
			$_->set_max_key_len($max_key_len);
		}
		$buf .= $_->output();
	}
	$buf .= (scalar(@{$self->{items}}) > 0 ? $self->indent() : ' ') . '}' . (!defined $self->{embed} || $self->{embed} == 0 ? $self->nl() : '');
	return $buf;
}

package PuppetParser::Expression;

our @ISA = 'PuppetParser::Object';

sub parse {
	my ($self) = @_;
	my $packages = ['PuppetParser::ResourceRef', 'PuppetParser::ArrayRef', 'PuppetParser::FunctionCall', 'PuppetParser::List', 'PuppetParser::Hash', 'PuppetParser::Selector'];
	$self->{parts} = [];
	TOKEN: while(1) {
		if($self->{parser}->scan_for_token($self->{term})) {
			last;
		}
		if($self->{parser}->scan_for_token(['RBRACE'])) {
			# This is a total hack, but it works
			return 0;
		}
		if($self->{parser}->scan_for_token(['RETURN'])) {
			$self->{parser}->next_token();
			next;
		}
		for my $package (@{$packages}) {
			my $foo = $package->new(parent => $self, embed => 1, level => $self->{level} - 1);
			if(defined $foo) {
				push @{$self->{parts}}, $foo;
				next TOKEN;
			}
		}
		push @{$self->{parts}}, PuppetParser::Simple->new(parent => $self);
	}
	if(scalar(@{$self->{parts}}) == 0) {
		return 0;
	}
	return 1;
}

sub output {
	my ($self) = @_;
	my $buf = join(' ', map { $_->output() } @{$self->{parts}});
	return $buf;
}

package PuppetParser::Selector;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'token', token => 'DOLLAR_VAR', name => 'varname' },
		{ type => 'token', token => 'QMARK' },
		{ type => 'token', token => 'LBRACE' },
		{ type => 'class', class => ['PuppetParser::KeyValuePair', 'PuppetParser::Comment'], name => 'items', many => 1 },
		{ type => 'token', token => 'RBRACE' },
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	my $max_key_len = 0;
	if(ref $self->{items} ne 'ARRAY') {
		$self->{items} = [ $self->{items} ];
	}
	for(@{$self->{items}}) {
		if($_->can('key_len')) {
			my $key_len = $_->key_len();
			if($key_len > $max_key_len) {
				$max_key_len = $key_len;
			}
		}
	}
	my $buf = $self->{varname}->output() . ' ? {';
	if(scalar(@{$self->{items}}) > 0) {
		$buf .= $self->nl();
	}
	for(@{$self->{items}}) {
		if($_->can('set_max_key_len')) {
			$_->set_max_key_len($max_key_len);
		}
		$buf .= $_->output();
	}
	$buf .= (scalar(@{$self->{items}}) > 0 ? $self->indent() : ' ') . '}';
	return $buf;
}


package PuppetParser::CaseStatement;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'token', token => 'CASE' },
		{ type => 'token', token => ['DOLLAR_VAR', 'SQUOTES', 'DQUOTES'], name => 'casevar' },
		{ type => 'token', token => 'LBRACE' },
		{ type => 'class', class => ['PuppetParser::CaseCondition', 'PuppetParser::Comment'], many => 1, name => 'conditions' },
		{ type => 'token', token => 'RBRACE' },
	];
	return $parser_data;
}

sub apply_defaults {
	my ($self) = @_;
	$self->SUPER::apply_defaults({ inner_spacing => 0, outer_spacing => 1 });
}

sub output {
	my ($self) = @_;
	my $buf = $self->indent() . 'case ' . $self->{casevar}->output() . ' {' . $self->nl();
	if(ref $self->{conditions} ne 'ARRAY') {
		$self->{conditions} = [ $self->{conditions} ];
	}
	for(@{$self->{conditions}}) {
		$buf .= $_->output();
	}
	$buf .= $self->indent() . '}' . $self->nl();
	return $buf;
}

package PuppetParser::Node;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'token', token => 'NODE' },
#		{ type => 'token', token => ['NAME', 'DEFAULT', 'REGEX', 'SQUOTES', 'DQUOTES'], name => 'nodename' },
		{ type => 'class', class => 'PuppetParser::Expression', args => { term => ['LBRACE'] }, name => 'nodename' },
		{ type => 'group', optional => 1, members => [
			{ type => 'token', token => 'INHERITS' },
			{ type => 'token', token => ['NAME', 'DEFAULT', 'REGEX', 'SQUOTES', 'DQUOTES'], name => 'inherits' },
		]},
		{ type => 'block', name => 'contents' },
	];
	return $parser_data;
}

sub apply_defaults {
	my ($self) = @_;
	$self->SUPER::apply_defaults({ outer_spacing => 1 });
}

sub output {
	my ($self) = @_;
	my $buf = $self->indent() . 'node ' . $self->{nodename}->output() . ' {' . $self->nl();
	$buf .= $self->output_children();
	$buf .= $self->indent() . '}' . $self->nl();
	return $buf;
}

package PuppetParser::CaseCondition;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'class', class => 'PuppetParser::Expression', args => { term => ['COLON'] }, name => 'casecondition' },
		{ type => 'token', token => 'COLON' },
		{ type => 'block', name => 'contents' },
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	my $buf = $self->indent() . $self->{casecondition}->output() . ': {' . $self->nl();
	$buf .= $self->output_children();
	$buf .= $self->indent() . '}' . $self->nl();
	return $buf;
}

package PuppetParser::KeyValuePair;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'token', token => ['NAME', 'DEFAULT', 'REGEX', 'SQUOTES', 'DQUOTES', 'NUMBER', 'CLASSREF'], name => 'key' },
		{ type => 'token', token => ['FARROW', 'PARROW'], name => 'arrow' },
		{ type => 'class', class => 'PuppetParser::Expression', args => { term => ['COMMA', 'SEMIC', 'RBRACE'] }, name => 'value' },
		{ type => 'token', token => ['COMMA', 'SEMIC'], optional => 1 },
	];
	return $parser_data;
}

sub key_len {
	my ($self) = @_;
	return length($self->{key}->output());
}

sub set_max_key_len {
	my ($self, $len) = @_;
	$self->{max_key_len} = $len;
}

sub output {
	my ($self) = @_;
	my $buf = $self->indent() . sprintf('%-' . $self->{max_key_len} . 's', $self->{key}->output()) . ' => ' . $self->{value}->output() . ',' . $self->nl();
	return $buf;
}

package PuppetParser::ArgumentList;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my ($self) = @_;
	my $parser_data = [
		{ type => 'token', token => 'LPAREN' },
		{ type => 'class', class => 'PuppetParser::Expression', args => { term => ['RPAREN'] }, optional => 1, name => 'args' },
		{ type => 'token', token => 'RPAREN' },
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	my $buf = '(';
	if(defined $self->{args}) {
		$buf .= $self->{args}->output();
	}
	$buf .= ')';
	return $buf;
}

package PuppetParser::ListElement;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'class', class => 'PuppetParser::Expression', args => { term => ['COMMA', 'RBRACK'] }, name => 'value' },
		{ type => 'token', token => 'COMMA', optional => 1 },
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	my $buf = $self->{value}->output() . ',';
	return $buf;
}

package PuppetParser::Class;

use Data::Dumper;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my ($self) = @_;
	my $parser_data = [
		{ type => 'token', token => 'CLASS' },
		{ name => 'classname', type => 'token', token => 'NAME' },
		{ name => 'args', type => 'class', class => 'PuppetParser::ArgumentList', optional => 1 },
		{ type => 'group', optional => 1, members => [
			{ type => 'token', token => 'INHERITS' },
			{ name => 'inherits', type => 'token', token => 'NAME' },
			], },
		{ name => 'contents', type => 'block' },
	];
	return $parser_data;
}

sub apply_defaults {
	my ($self) = @_;
	$self->SUPER::apply_defaults({ inner_spacing => 1, outer_spacing => 1 });
}

sub output {
	my ($self) = @_;
	my $buf = $self->indent() . 'class ' . $self->{classname}->output();
	if(defined $self->{args}) { #scalar(@{$self->{args}}) > 0) {
		$buf .= $self->{args}->output();
	}
	if(defined $self->{inherits}) {
		$buf .= ' inherits ' . $self->{inherits}->output();
	}
	$buf .= ' {' . $self->nl();

	$buf .= $self->output_children();
	$buf .= '}' . $self->nl();
	return $buf;
}

package PuppetParser::Define;

our @ISA = 'PuppetParser::Object';

sub apply_defaults {
	my ($self) = @_;
	$self->SUPER::apply_defaults({ inner_spacing => 1, outer_spacing => 1 });
}

sub get_parser_data {
	my ($self) = @_;
	my $parser_data = [
		{ type => 'token', token => 'DEFINE' },
		{ name => 'defname', type => 'token', token => 'NAME' },
		{ name => 'args', type => 'class', class => 'PuppetParser::ArgumentList', optional => 1 },
		{ name => 'contents', type => 'block' },
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	my $buf = $self->indent() . 'define ' . $self->{defname}->output();
	if(defined $self->{args}) {
		$buf .= $self->{args}->output();
	}
	$buf .= ' {' . $self->nl();
	$buf .= $self->output_children();
	$buf .= $self->indent() . '}' . $self->nl();
	return $buf;
}

package PuppetParser::FunctionCall;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'token', token => ['NAME'], name => 'funcname' },
		{ type => 'class', class => 'PuppetParser::ArgumentList', name => 'args' },
	];
	return $parser_data;
}

#sub apply_defaults {
#	my ($self) = @_;
#	$self->SUPER::apply_defaults({ outer_spacing => 1 });
#}

sub output {
	my ($self) = @_;
	my $buf = ($self->{embed} ? '' : $self->indent()) . $self->{funcname}->output();
	if(defined $self->{args}) {
		$buf .= $self->{args}->output();
	}
	$buf .= ($self->{embed} ? '' : $self->nl());
	return $buf;
}

package PuppetParser::IfStatement;

our @ISA = 'PuppetParser::Object';

sub apply_defaults {
	my ($self) = @_;
	$self->SUPER::apply_defaults({ inner_spacing => 0, outer_spacing => 1 });
}

sub get_parser_data {
	my $parser_data = [
		{ type => 'token', token => ['IF', 'ELSE', 'ELSIF'], name => 'variant' },
		{ type => 'class', class => 'PuppetParser::Expression', args => { term => ['LBRACE'] }, name => 'condition', optional => 1 },
		{ type => 'block', name => 'contents' },
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	my $buf = $self->indent() . $self->{variant}->output() . (defined $self->{condition} ? (' ' . $self->{condition}->output()) : '') . ' {' . $self->nl();
	$buf .= $self->output_children();
	$buf .= $self->indent() . '}' . $self->nl();
	return $buf;
};

package PuppetParser::Include;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'token', token => ['INCLUDE', 'IMPORT', 'NAME'], name => 'funcname' },
		{ type => 'any', members => [
			{ type => 'token', token => ['NAME', 'SQUOTES', 'DQUOTES', 'DOLLAR_VAR'], name => 'arg' },
			{ type => 'class', class => 'PuppetParser::ResourceRef', name => 'arg' },
		]},
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	return $self->indent() . $self->{funcname}->output() . ' ' . $self->{arg}->output() . $self->nl();
}

package PuppetParser::VarAssignment;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'token', token => 'DOLLAR_VAR', name => 'varname' },
		{ type => 'token', token => 'EQUALS' },
		{ type => 'class', class => 'PuppetParser::Expression', args => { term => ['RETURN', 'RBRACE'] }, name => 'value' },
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	return $self->indent() . $self->{varname}->output() . ' = ' . $self->{value}->output() . $self->nl();
}

package PuppetParser::DependencyChainPiece;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'class', class => ['PuppetParser::Resource', 'PuppetParser::ResourceRef'], name => 'ref' },
		{ type => 'token', token => ['IN_EDGE', 'OUT_EDGE', 'IN_EDGE_SUB', 'OUT_EDGE_SUB'], name => 'arrow' },
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	my $buf = $self->{ref}->output() . ' ' . $self->{arrow}->output();
	return $buf;
}

package PuppetParser::DependencyChain;

our @ISA = 'PuppetParser::Object';

sub apply_defaults {
	my ($self) = @_;
	$self->SUPER::apply_defaults({ outer_spacing => 1 });
}

sub get_parser_data {
	my $parser_data = [
		{ type => 'class', class => 'PuppetParser::DependencyChainPiece', name => 'items', many => 1 },
		{ type => 'class', class => ['PuppetParser::Resource', 'PuppetParser::ResourceRef'], name => 'lastitem' },
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	my $buf = $self->indent();
	if(ref $self->{items} ne 'ARRAY') {
		$self->{items} = [ $self->{items} ];
	}
	for(@{$self->{items}}) {
		$buf .= $_->output() . ' ';
	}
	$buf .= $self->{lastitem}->output();
	if(defined $self->{lastcollect}) {
		$buf .= ' ' . $self->{lastcollect}->output();
	}
	$buf .= $self->nl();
	return $buf;
}

package PuppetParser::ResourceTitle;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'any', members => [
			{ type => 'class', class => ['PuppetParser::ResourceRef', 'PuppetParser::ArrayRef', 'PuppetParser::List'], name => 'restitle' },
			{ type => 'token', token => ['NAME', 'SQUOTES', 'DQUOTES', 'DOLLAR_VAR'], name => 'restitle' },
		]},
		{ type => 'token', token => 'COLON' },
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	my $buf = $self->{restitle}->output() . ':';
	return $buf;
}

package PuppetParser::Collect;

our @ISA = 'PuppetParser::Object';

sub get_parser_data {
	my $parser_data = [
		{ type => 'token', token => ['LCOLLECT', 'LLCOLLECT'], name => 'lcollect' },
		{ type => 'class', class => 'PuppetParser::Expression', args => { term => ['RCOLLECT', 'RRCOLLECT'] }, optional => 1, name => 'tags' },
		{ type => 'token', token => ['RCOLLECT', 'RRCOLLECT'], name => 'rcollect' },
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	my $buf = $self->{lcollect}->output() . (defined $self->{tags} ? ' ' . $self->{tags}->output() . ' ' : '') . $self->{rcollect}->output();
	return $buf;
}

package PuppetParser::Resource;

our @ISA = 'PuppetParser::Object';

sub apply_defaults {
	my ($self) = @_;
	$self->SUPER::apply_defaults({ inner_spacing => 1, outer_spacing => 1 });
}

sub get_parser_data {
	my ($self) = @_;
	my $parser_data = [
		{ type => 'token', token => 'AT', optional => 1, name => 'virtual' },
		{ type => 'token', token => 'AT', optional => 1, name => 'exported' },
		{ type => 'any', name => 'restype', members => [
			{ type => 'class', class => 'PuppetParser::ResourceRef', name => 'restype' },
			{ type => 'token', token => ['NAME', 'CLASSREF', 'CLASS'], name => 'restype' },
		]},
		{ type => 'token', token => 'LBRACE' },
		{ type => 'class', class => ['PuppetParser::KeyValuePair', 'PuppetParser::ResourceTitle', 'PuppetParser::Comment'], name => 'items', many => 1 },
		{ type => 'token', token => 'RBRACE' },
	];
	return $parser_data;
}

sub output {
	my ($self) = @_;
	my @resources;
	my $res = {};
	if(ref $self->{items} ne 'ARRAY') {
		$self->{items} = [ $self->{items} ];
	}
	for(@{$self->{items}}) {
		if($_->isa('PuppetParser::ResourceTitle')) {
			if(defined $res->{title}) {
				push @resources, $res;
				$res = {};
				$res->{title} = $_;
				next;
			}
			$res->{title} = $_;
			next;
		}
		if(!defined $res->{items}) {
			$res->{items} = [];
		}
		push @{$res->{items}}, $_;
	}
	push @resources, $res;
	my $buf = '';
	for my $resource (@resources) {
		$buf .= $self->indent() . (defined $self->{virtual} ? $self->{virtual}->output() : '') . (defined $self->{exported} ? $self->{exported}->output() : '') . $self->{restype}->output();
		if(defined $self->{collect}) {
			$buf .= ' ' . $self->{collect}->output();
		}
		$buf .= ' { ';
		if(defined $resource->{title}) {
			$buf .= $resource->{title}->output();
		}
		if(defined $resource->{items}) {
			my $max_key_len = 0;
			for(@{$resource->{items}}) {
				if($_->can('key_len')) {
					my $key_len = $_->key_len();
					if($key_len > $max_key_len) {
						$max_key_len = $key_len;
					}
				}
			}
			$buf .= $self->nl();
			for(@{$resource->{items}}) {
				if($_->can('set_max_key_len')) {
					$_->set_max_key_len($max_key_len);
				}
				$buf .= $_->output();
			}
		}
		$buf .= (defined $resource->{items} ? $self->indent() : ' ') . '}' . $self->nl() . $self->nl();
	}
	return $buf;
}

package main;

sub usage {
	print "Usage: " . $0 . " [-dh] [-o <output>] <input>\n\n";
	print "Options:\n\n";
	print "  -d              Enables some debug output\n";
	print "  -o <output>     Specifies the file to output to (defaults to STDOUT)\n";
	print "  -h              Displays this help\n";
}

$| = 1;

use Getopt::Std;

# Flags
my %options;
my $output = '';
my $debug = 0;

# Parse options
my $result = getopts('dho:', \%options);
if(defined $options{'o'}) {
	$output = $options{'o'};
}
if(defined $options{'d'}) {
	$debug = $options{'d'};
}
if(defined $options{'h'}) {
	usage();
	exit;
}
my $file = shift;
if(!defined $file) {
	print "No input file specified!\n\n";
	usage();
	exit 1;
}

print "Parsing $file\n";
my $parser = PuppetParser->new(file => $file, debug => $debug);
$parser->parse();
$parser->output($output);
