# $Id: Parse.pm,v 1.3 2006/11/03 21:00:13 mike Exp $

# Parse.pm - parsing functions for Scott Adams game files.

package Games::ScottAdams::Game;
use strict;

use Games::ScottAdams::File;


sub parse {
    my $this = shift();
    my($filename) = @_;

    #warn "parsing '$filename'";
    my $fh = new Games::ScottAdams::File($filename)
	or die "can't open '$filename': $!";

    while (defined ($_ = $fh->getline(1))) {
	s/^\s+%/%/;		# Skip any whitespace before leading %
	if (/^%room (.*)/i) {
	    $this->_parse_room($fh, $1);
	} elsif (/^%exit\s+(.*)\s+(.*)/i) {
	    $this->_parse_exit($fh, $1, $2);
	} elsif (/^%item\s+(.*)/i) {
	    $this->_parse_item($fh, $1);
	} elsif (/^%getdrop\s+(.*)/i) {
	    $this->_parse_getdrop($fh, $1);
	} elsif (/^%(at|nowhere)\s*(.*)/i) {
	    $this->_parse_at($fh, $2);
	} elsif (/^%start\s+(.*)/i) {
	    $this->_parse_start($fh, $1);
	} elsif (/^%treasury\s+(.*)/i) {
	    $this->_parse_treasury($fh, $1);
	} elsif (/^%maxload\s+(.*)/i) {
	    $this->_parse_maxload($fh, $1);
	} elsif (/^%lighttime\s+(.*)/i) {
	    $this->_parse_lighttime($fh, $1);
	} elsif (/^%ident\s+(.*)/i) {
	    $this->_parse_ident($fh, $1);
	} elsif (/^%version\s+(.*)/i) {
	    $this->_parse_version($fh, $1);
	} elsif (/^%wordlen\s+(.*)/i) {
	    $this->_parse_wordlen($fh, $1);
	} elsif (/^%lightsource\s+(.*)/i) {
	    $this->_parse_lightsource($fh, $1);
	} elsif (/^%action\s+(.*)\s+(.*)/i) {
	    $this->_parse_action($fh, $1, $2);
	} elsif (/^%action\s+(.*)/i) {
	    $this->_parse_action($fh, $1, undef);
	} elsif (/^%occur\s*(.*)/i) {
	    $this->_parse_action($fh, undef, $1);
	} elsif (/^%result/i) {
	    $this->_parse_result($fh);
	} elsif (/^%comment\s+(.*)/i) {
	    $this->_parse_comment($fh, $1);
	} elsif (/^%([nv])alias\s+(.*)\s+(.*)/i) {
	    $this->_parse_alias($fh, lc($1), $2, $3);
	} elsif (/^%include\s+(.*)/i) {
	    my $newfile = $1;
	    #warn "%include '$newfile'";
	    # Interpret filenames relative to directory of current file
	    if ($filename =~ m@/@) {
		my $prefix = $filename;
		$prefix =~ s@(.*)/.*@$1@;
		$newfile = "$prefix/$newfile";
	    }
	    $this->parse($newfile);
	} elsif (!/^%/) {
	    $fh->warn("expected directive, got '$_' (ignored)");
	} else {
	    $fh->warn("unrecognised directive (ignored): '$_'");
	}
    }

    $this->_coalesce_aliases();
    return 1;
}


# PRIVATE to the parse() method
sub _parse_room {
    my $this = shift();
    my($fh, $name) = @_;

    my $desc = '';
    while (defined (my $line = $fh->getline(1))) {
	if ($line =~ /^\s*%/) {
	    $fh->ungetline($line);
	    last;
	}

	$desc .= "$line\n";
    }

    my $num = @{ $this->{rooms} }; # 0-based index of room to be added
    my $room = new Games::ScottAdams::Room($name, $desc, $num);
    push @{ $this->{rooms} }, $room;
    if (defined $this->{roomname}->{$name}) {
	$fh->warn("discarding old room '$name'");
    }

    $this->{roomname}->{$name} = $room;
    $this->{_room} = $room;
    $this->{_roomname1} = $name
	if !defined $this->{_roomname1};
}


# PRIVATE to the parse() method
sub _parse_exit {
    my $this = shift();
    my($fh, $dir, $dest) = @_;

    my $room = $this->{_room};
    if (!defined $room) {
	$fh->warn("ignoring %exit '$dir'->'$dest' before first room");
	return;
    }

    my $roomname = $room->name();
    $dir = lc(substr($dir, 0, 1));
    if ($dir !~ /^[nsewud]$/) {
	$fh->warn("ignoring %exit '$dir'->'$dest' at '$roomname'");
	return;
    }

    my $old = $room->exit($dir);
    if (defined $old) {
	$fh->warn("discarding old exit '$dir'->'$old' at '$roomname'");
    }

    $room->exit($dir, $dest);
}


# PRIVATE to the parse() method
sub _parse_item {
    my $this = shift();
    my($fh, $name) = @_;

    my $where = undef;		# item is initially nowhere
    my $room = $this->{_room};
    if (defined $room) {
	$where = $room->name();
    }

    my $desc = $fh->getline(1);
    while (defined (my $line = $fh->getline(1))) {
	if ($line =~ /^\s*%/) {
	    $fh->ungetline($line);
	    last;
	}

	$desc .= "\n$line";
    }

    my $num = @{ $this->{items} }; # 0-based index of item to be added
    if (0) {
	### No need to do this, is there?
	if ($num == 9) {
	    # Leave slot 9 free for the light-source
	    my $nothing = new Games::ScottAdams::Item('', '', $num++);
	    push @{ $this->{items} }, $nothing;
	}
    }

    my $item = new Games::ScottAdams::Item($name, $desc, $num, $where);
    push @{ $this->{items} }, $item;
    if (defined $this->{itemname}->{$name}) {
	$fh->warn("discarding old item '$name'");
    }

    $this->{itemname}->{$name} = $item;
    $this->{_item} = $item;
    if ($desc =~ /^\*/) {
	$this->{ntreasures}++;
    }
}


# PRIVATE to the parse() method
sub _parse_getdrop {
    my $this = shift();
    my($fh, $name) = @_;

    my $item = $this->{_item};
    if (!defined $item) {
	$fh->warn("ignoring %getdrop '$name' before first item");
	return;
    }

    my $itemname = $item->name();
    my $old = $item->getdrop();
    if (defined $old) {
	$fh->warn("discarding old getdrop '$old' for '$itemname'");
    }

    $item->getdrop($name);
    $this->_parse_alias($fh, 'n', $name);
}


# PRIVATE to the parse() method
sub _parse_at {
    my $this = shift();
    my($fh, $where) = @_;

    my $item = $this->{_item};
    if (!defined $item) {
	$fh->warn("ignoring %at '$where' before first item");
	return;
    }

    my $itemname = $item->name();
    my $old = $item->where();
    if (defined $old && $where ne '') {
	#$fh->warn("replacing location '$old' with '$where' for '$itemname'");
    }

    $item->where($where);
}


# All the following wrappers are PRIVATE to the parse() method
sub _parse_start {
    return _parse_param(@_, 'start', 'start room'); }
sub _parse_treasury {
    return _parse_param(@_, 'treasury', 'treasury room'); }
sub _parse_maxload {
    return _parse_param(@_, 'maxload', 'maximum load'); }
sub _parse_lighttime {
    return _parse_param(@_, 'lighttime', 'light duration'); }
sub _parse_ident {
    return _parse_param(@_, 'ident', 'adventure identifier'); }
sub _parse_version {
    return _parse_param(@_, 'version', 'version number'); }
sub _parse_wordlen {
    return _parse_param(@_, 'wordlen', 'word length'); }
sub _parse_lightsource {
    return _parse_param(@_, 'lightsource', 'light source'); }


# PRIVATE to the _parse_{start...wordlen}() methods
sub _parse_param {
    my $this = shift();
    my($fh, $value, $param, $caption) = @_;

    if (defined $this->{$param}) {
	$fh->warn("discarding old $caption '", $this->{$param}, "'");
    }

    $this->{$param} = $value;
}


# PRIVATE to the parse() method
sub _parse_action {
    my $this = shift();
    my($fh, $verb, $noun) = @_;

    my $num = @{ $this->{actions} }; # 0-based index of action to be added
    my $action = new Games::ScottAdams::Action($verb, $noun, $num);
    push @{ $this->{actions} }, $action;
    $this->{_action} = $action;

    # Register noun and verb
    $this->_parse_alias($fh, 'v', $verb) if $verb;
    $this->_parse_alias($fh, 'n', $noun) if $noun;

    while (defined (my $line = $fh->getline(1))) {
	if ($line =~ /^\s*%/) {
	    $fh->ungetline($line);
	    last;
	}
	$action->add_cond($line);
    }

    return;
}


# PRIVATE to the parse() method
sub _parse_result {
    my $this = shift();
    my($fh) = @_;

    my $action = $this->{_action};
    if (!defined $action) {
	$fh->warn("ignoring %result before first action");
	return;
    }

    while (defined (my $line = $fh->getline(1))) {
	if ($line =~ /^\s*%/) {
	    $fh->ungetline($line);
	    last;
	}
	$action->add_result($line);
    }
}


# PRIVATE to the parse() method
sub _parse_comment {
    my $this = shift();
    my($fh, $comment) = @_;

    my $action = $this->{_action};
    if (!defined $action) {
	$fh->warn("ignoring %comment before first %action");
	return;
    }

    my $old = $action->comment();
    if (defined $old) {
	$fh->warn("discarding old comment '$old'");
    }

    $action->comment($comment);
}


# PRIVATE to the parse() method
sub _parse_alias {
    my $this = shift();
    my $fh = shift();
    my $type = lc(shift());
    my @words = map { uc() } @_;

    my $href = $this->{$type . 'vocab'};
    for my $word (@words) {
	$fh->fatal("empty word") if !$word;
	push @{ $href->{$word} }, grep { $_ ne $word } @words;
    }
}


# PRIVATE to the parse() method
#
# At this point, we have a bunch of equivalence classes for each
# vocabulary set, we we need to coalesce them.  For example, if we
# have the following in the source --
#	%valias enter go
#	%valias run go
#	%valias walk go
# We'll have a {vvocab} hash that looks like this --
#	RUN -> GO
#	GO -> ENTER,RUN,WALK
#	ENTER -> GO
#	WALK -> GO
# But we want a single list of all four words

sub _coalesce_aliases {
    my $this = shift();

    $this->{vvocab} = _extend_lists($this->{vvocab});
    $this->{nvocab} = _extend_lists($this->{nvocab});
}

# PRIVATE to the _coalesce_aliases() method
sub _extend_lists {
    my($vocab) = @_;

    my @keys = keys %$vocab;	# we're going to change this
    foreach my $key (@keys) {
	#warn "considering aliases for '$key'";
	next if !exists $vocab->{$key};
	#warn "thinking about '$key'";
	my @list = _equivalents($vocab, $key, { $key => 1});
	#warn "\t" . join(' ', @list);
	foreach my $used (@list) {
	    delete $vocab->{$used};
	    #warn "deleted '$used'";
	}
	$vocab->{$key} = [ @list ];
    }

    return $vocab;
}

# PRIVATE to the _extend_lists method()
sub _equivalents {
    my($vocab, $word, $seen) = @_;

    my @equivalents;
    my $cref = $vocab->{$word};
    foreach my $candidate (@$cref) {
	next if exists $seen->{$candidate};
	$seen->{$candidate} = 1;
	my @sub = _equivalents($vocab, $candidate, $seen);
	push @equivalents, $candidate, @sub;
    }

    return @equivalents;
}


1;
