# RowFixture.pm
#
# Copyright (c) 2002 Cunningham & Cunningham, Inc.
# Released under the terms of the GNU General Public License version 2 or later.
#
# Perl translation by Dave W. Smith <dws@postcognitive.com>

package RowFixture;

use base qw(ColumnFixture);

use strict;
use TypeAdapter;

sub new {
    my $pkg = shift;

    return $pkg->SUPER::new(results => [],
                            missing => [],
                            surplus => [],
                            @_);
}

sub doRows {
    my $self = shift;
    my($rows) = @_;

    eval {
	$self->bind($rows->parts());
	$self->{'results'} = $self->query();
	$self->match($self->rowsToArray($rows->more()),
                     $self->{'results'},
                     0);
	my $last = $rows->last();
	$last->more($self->buildRows($self->{'surplus'}));
	$self->markRows($last->more(), "surplus");
	$self->markList($self->{'missing'}, "missing");
    };
    if ( $@ ) {
        $self->exception($rows->leaf(), $@);
    }
}

sub match {
    my $self = shift;
    my($expected, $computed, $col) = @_;

    my $ncols = @{$self->{'columnBindings'}};
    if ( $col >= $ncols ) {
	$self->checkLists($expected, $computed);
    }
    elsif ( not defined($self->{'columnBindings'}->[$col]) ) {
	$self->match($expected, $computed, $col + 1)
    }
    else {
	my $eMap = $self->eSort($expected, $col);
	my $cMap = $self->cSort($computed, $col);
        my $keys = $self->union(keys %$eMap, keys %$cMap);
	foreach my $key ( @$keys ) {
	    my $eList = $$eMap{$key};
	    my $cList = $$cMap{$key};
	    if ( ! $eList ) {
		push @{$self->{'surplus'}}, @$cList;
	    }
	    elsif ( ! $cList ) {
		push @{$self->{'missing'}}, @$eList;
	    }
	    elsif ( 1 == @$eList && 1 == @$cList ) {
		$self->checkLists($eList, $cList);
	    }
	    else {
		$self->match($eList, $cList, $col+1);
	    }
	}
    }
}

sub rowsToArray {
    my $self = shift;
    my($rows) = @_;

    my @results = ();
    while ( $rows ) {
	push @results, $rows;
	$rows = $rows->more();
    }
    return \@results;
}

sub eSort {
    my $self = shift;
    my($list, $col) = @_;

    my $adapter = $self->{'columnBindings'}->[$col];
    my %result = ();
    
    foreach my $row ( @$list ) {
	my $cell = $row->parts()->at($col);
	eval {
	    my $key = $adapter->parse($cell->text());
	    push @{$result{$key}}, $row;
	};
	if ( $@ ) {
	    $self->exception($cell, $@);
	    while ( $cell = $cell->more() ) {
		$self->ignore($cell);
	    }
	}
    }

    return \%result;
}

sub cSort {
    my $self = shift;
    my($list, $col) = @_;

    my $adapter = $self->{'columnBindings'}->[$col];
    my %result = ();
    foreach my $row ( @$list ) {
	eval {
	    $adapter->target($row);
	    my $key = $adapter->get();
	    push @{$result{$key}}, $row;
	};
	if ( $@ ) {
	    push @{$self->{'surplus'}}, $row;
	}
    }

    return \%result;
}


sub union {
    my $self = shift;

    my %merged = ();
    $merged{$_}++ foreach @_;

    return [keys %merged]
}

sub checkLists {
    my $self = shift;
    my($eList, $cList) = @_;

    if ( 0 == @$eList ) {
	push @{$self->{'surplus'}}, @$eList;
	return;
    }
    if ( 0 == @$cList ) {
	push @{$self->{'missing'}}, @$cList;
	return;
    }
    my $row = shift @$eList;
    my $cell = $row->parts();
    my $obj = shift @$cList;
    foreach my $adapter ( @{$self->{'columnBindings'}} ) {
	last if not defined($cell);
	if ( $adapter ) {
	    $adapter->target($obj);
	}
	$self->check($cell, $adapter);
	$cell = $cell->more();
    }
    $self->checkLists($eList, $cList);
}

sub markRows {
    my $self = shift;
    my($rows, $message) = @_;

    my $annotation = Fixture::label($message);
    while ( $rows ) {
	$self->wrong($rows->parts());
	$rows->parts()->addToBody($annotation);
	$rows = $rows->more();
    }
}

sub markList {
    my $self = shift;
    my($rows, $message) = @_;

    my $annotation = Fixture::label($message);
    foreach my $row ( @$rows ) {
	$self->wrong($row->parts());
	$row->parts()->addToBody($annotation);
    }
}

sub buildRows {
    my $self = shift;
    my($rowsref) = @_;

    my $root = Parse->from("", undef, undef, undef);
    my $next = $root;
    foreach my $row ( @$rowsref ) {
	$next = $next->more(Parse->from("tr", undef, $self->buildCells($row), undef));
    }
    return $root->more();
}

sub buildCells {
    my $self = shift;
    my($row) = @_;
    my $ncols = $self->{'columnBindings'};

    if ( ! $row ) {
	my $nil = Parse->from("td", "nul", undef, undef);
        $nil->addToTag(" colspan=$ncols");
        return $nil;
    }
    my $root = Parse->from("", undef, undef, undef);
    my $next = $root;
    foreach my $adapter ( @{$self->{'columnBindings'}} ) {
	$next = $next->more(Parse->from("td", "&nbsp;", undef, undef));
	if ( ! $adapter ) {
	    $self->ignore($next);
	} else {
	    eval {
		$adapter->target($row);
		my $s = $adapter->toString($adapter->get());
		$next->body($self->gray(Fixture::escape($s)));
            };
            if ( $@ ) {
		$self->exception($next, $@);
            }
        }
	return $root->more();
    }
}


1;

__END__

// Copyright (c) 2002 Cunningham & Cunningham, Inc.
// Released under the terms of the GNU General Public License version 2 or later.

package fit;

import java.util.*;

abstract public class RowFixture extends ColumnFixture {

    public Object results[];
    public List missing = new LinkedList();
    public List surplus = new LinkedList();


    public void doRows(Parse rows) {
        try {
            bind(rows.parts);
            results = query();
            match(list(rows.more), list(results), 0);
            Parse last = rows.last();
            last.more = buildRows(surplus.toArray());
            mark(last.more, "surplus");
            mark(missing.iterator(), "missing");
        } catch (Exception e) {
            exception (rows.leaf(), e);
        }
    }

    abstract public Object[] query() throws Exception;  // get rows to be compared
    abstract public Class getTargetClass();             // get expected type of row

    protected void match(List expected, List computed, int col) {
        if (col >= columnBindings.length) {
            check (expected, computed);
        } else if (columnBindings[col] == null) {
            match (expected, computed, col+1);
        } else {
            Map eMap = eSort(expected, col);
            Map cMap = cSort(computed, col);
            Set keys = union(eMap.keySet(),cMap.keySet());
            for (Iterator i=keys.iterator(); i.hasNext(); ) {
                Object key = i.next();
                List eList = (List)eMap.get(key);
                List cList = (List)cMap.get(key);
                if (eList == null) {
                    surplus.addAll(cList);
                } else if (cList == null) {
                    missing.addAll(eList);
                } else if (eList.size()==1 && cList.size()==1) {
                    check(eList, cList);
                } else {
                    match(eList, cList, col+1);
                }
            }
        }
    }

    protected List list (Parse rows) {
        List result = new LinkedList();
        while (rows != null) {
            result.add(rows);
            rows = rows.more;
        }
        return result;
    }

    protected List list (Object[] rows) {
        List result = new LinkedList();
        for (int i=0; i<rows.length; i++) {
            result.add(rows[i]);
        }
        return result;
    }

    protected Map eSort(List list, int col) {
        TypeAdapter a = columnBindings[col];
        Map result = new HashMap(list.size());
        for (Iterator i=list.iterator(); i.hasNext(); ) {
            Parse row = (Parse) i.next();
            Parse cell = row.parts.at(col);
            try {
                Object key = a.parse(cell.text());
                bin(result, key, row);
            } catch (Exception e) {
                exception(cell, e);
                for (Parse rest=cell.more; rest!=null; rest=rest.more) {
                    ignore(rest);
                }
            }
        }
        return result;
    }

    protected Map cSort(List list, int col) {
        TypeAdapter a = columnBindings[col];
        Map result = new HashMap(list.size());
        for (Iterator i=list.iterator(); i.hasNext(); ) {
            Object row = i.next();
            try {
                a.target = row;
                Object key = a.get();
                bin(result, key, row);
            } catch (Exception e) {
                // surplus anything with bad keys, including null
                surplus.add(row);
            }
        }
        return result;
    }

    protected void bin (Map map, Object key, Object row) {
        if (map.containsKey(key)) {
            ((List)map.get(key)).add(row);
        } else {
            List list = new LinkedList();
            list.add(row);
            map.put(key, list);
        }
    }

    protected Set union (Set a, Set b) {
        Set result = new HashSet();
        result.addAll(a);
        result.addAll(b);
        return result;
    }

    protected void check (List eList, List cList) {
        if (eList.size()==0) {
            surplus.addAll(cList);
            return;
        }
        if (cList.size()==0) {
            missing.addAll(eList);
            return;
        }
        Parse row = (Parse)eList.remove(0);o
        Parse cell = row.parts;
        Object obj = cList.remove(0);
        for (int i=0; i<columnBindings.length && cell!=null; i++) {
            TypeAdapter a = columnBindings[i];
            if (a != null) {
                a.target = obj;
            }
            check(cell, a);
            cell = cell.more;
        }
        check (eList, cList);
    }

    protected void mark(Parse rows, String message) {
        String annotation = label(message);
        while (rows != null) {
            wrong(rows.parts);
            rows.parts.addToBody(annotation);
            rows = rows.more;
        }
    }

    protected void mark(Iterator rows, String message) {
        String annotation = label(message);
        while (rows.hasNext()) {;
            Parse row = (Parse)rows.next();
            wrong(row.parts);
            row.parts.addToBody(annotation);
        }
    }

    protected Parse buildRows(Object[] rows) {
        Parse root = new Parse(null ,null, null, null);
        Parse next = root;
        for (int i=0; i<rows.length; i++) {
            next = next.more = new Parse("tr", null, buildCells(rows[i]), null);
        }
        return root.more;
    }

    protected Parse buildCells(Object row) {
        if (row == null) {
            Parse nil = new Parse("td", "null", null, null);
            nil.addToTag(" colspan="+columnBindings.length);
            return nil;
        }
        Parse root = new Parse(null, null, null, null);
        Parse next = root;
        for (int i=0; i<columnBindings.length; i++) {
            next = next.more = new Parse("td", "&nbsp;", null, null);
            TypeAdapter a = columnBindings[i];
            if (a == null) {
                ignore (next);
            } else {
                try {
                    a.target = row;
                    next.body = gray(escape(a.toString(a.get())));
                } catch (Exception e) {
                    exception(next, e);
                }
            }
        }
        return root.more;
    }
}

