package robliterator::board;
use strict;
use robliterator::robot;
use Fcntl ':flock'; # import LOCK_* constants

our $tileNothing = '.';
our $tileWall = 'W';
our $maxErrors = 3;

1;

sub new {
    my($class, $filename) = @_;
    my $self = {
        map => [],
        robots => {},
    };
    bless $self, $class;
    $self->load($filename);
    return $self;
}

sub load {
    my($self, $filename) = @_;
    open(BOARD, '<', $filename) or die;
    flock(BOARD, LOCK_SH) or die;
    my $line;

    # read robot manifest
    while (defined($line = <BOARD>) and chomp $line, $line ne '') {
        my($id, $url, $lastAction, $lastResult, @values) = split(' ', $line);
        my $robot = new robliterator::robot($id, $url, {
            lastAction => $lastAction,
            lastResult => $lastResult,
        }, @values);
        $self->{robots}->{$id} = $robot;
    }

    # read board state
    my $y = 0;
    while (defined($line = <BOARD>) and chomp $line, $line ne '') {
        my $x = 0;
        my @cells = split(' ', $line);
        foreach my $cell (@cells) {
            if ($cell =~ m/^[0-9]+$/os) {
                $self->{robots}->{$cell}->setXY($x, $y);
                $cell = $self->{robots}->{$cell};
            }
            $x += 1;
        }
        push(@{$self->{map}}, \@cells);
        $y += 1;
    }

    $line = <BOARD>;
    chomp $line;
    die unless $line eq 'END';

    flock(BOARD, LOCK_UN);
    close(BOARD);
}

sub save {
    my($self, $filename) = @_;
    open(BOARD, '>', "$filename.new") or die;

    local $" = ' ';

    # write robot manifest
    foreach my $robot (values %{$self->{robots}}) {
        my $id = $robot->id;
        my $url = $robot->url;
        my $data = $robot->data;
        my $lastAction = $data->{lastAction};
        my $lastResult = $data->{lastResult};
        my @values = $robot->values;
        print BOARD "$id $url $lastAction $lastResult @values\n";
    }

    print BOARD "\n";

    # write board
    foreach my $row (@{$self->{map}}) {
        print BOARD "@$row\n";
    }

    print BOARD "\nEND\n";
    close(BOARD);

    # finalise
    open(BOARD, '>>', $filename) or die;
    flock(BOARD, LOCK_EX) or die;
    my $time = (stat BOARD)[9];
    rename $filename, "$filename.$time";
    rename "$filename.new", $filename;
    flock(BOARD, LOCK_UN);
    close(BOARD);
}

sub getStateMessage {
    my($self, $target) = @_;

    local $" = ' ';

    # robot manifest
    my $robots = '';
    my $idMap = {};
    my $nextMappedId = 1;
    if (defined($target)) {
        $idMap->{$target} = $nextMappedId;
        $nextMappedId += 1;
    }
    foreach my $id (sort keys %{$self->{robots}}) {
        my $robot = $self->{robots}->{$id};
        my $domain = $robot->domain;
        my $data = $robot->data;
        my $lastAction = $data->{lastAction};
        my $lastResult = $data->{lastResult};
        my @values = $robot->values;
        my $mappedId;
        if (exists $idMap->{$id}) {
            $mappedId = $idMap->{$id};
        } else {
            $mappedId = $nextMappedId;
            $idMap->{$id} = $mappedId;
            $nextMappedId += 1;
        }
        $robots .= "$mappedId $domain $lastAction $lastResult @values\n";
    }

    # board
    my $board = '';
    foreach my $row (@{$self->{map}}) {
        my $space = '';
        foreach my $cell (@$row) {
            my $value = $cell;
            if (exists $idMap->{$value}) {
                $value = $idMap->{$value};
            }
            $board .= "$space$value";
            $space = ' ';
        }
        $board .= "\n";
    }

    my $header = '';
    if (defined($target)) {
        $header = $idMap->{$target} . "\n\n";
    }

    return "$header$robots\n$board\n";
}

# from http://docstore.mik.ua/orelly/perl/cookbook/ch04_18.htm
sub fisher_yates_shuffle {
    my $array = shift;
    my $i;
    for ($i = @$array; --$i; ) {
        my $j = int rand ($i+1);
        next if $i == $j;
        @$array[$i,$j] = @$array[$j,$i];
    }
}

sub addRobots {
    my($self, $id, @urls) = @_;
    my %domains = ();
    foreach my $robot (values %{$self->{robots}}) {
        if ($robot->value('alive')) {
            $domains{$robot->domain} = $robot;
        } elsif ($robot->data->{lastResult} ne $robliterator::robot::actionSuicide) {
            push(@urls, $robot->url);
        }
    }
    my @options = ();
    my $height = scalar(@{$self->{map}});
    my $width = scalar(@{$self->{map}->[0]});
    for (my $y = 0; $y < $height; $y += 1) {
        for (my $x = 0; $x < $width; $x += 1) {
            if ($self->{map}->[$y]->[$x] eq $tileNothing) {
                push(@options, [$x, $y]);
            }
        }
    }
    fisher_yates_shuffle(\@options);
    my @added = ();
  url: while (@urls) {
        last if scalar @options <= 0; # no empty spaces
        my $url = shift @urls;
        my $robot = new robliterator::robot($id, $url, {
            lastAction => $robliterator::robot::firstAction,
            lastResult => $robliterator::robot::actionAppeared,
        }, @robliterator::robot::firstValues);
        if (exists($domains{$robot->domain})) {
            next url; # only one live bot per domain
        }
        $self->{robots}->{$id} = $robot;
        $domains{$robot->domain} = $robot;
        my $x = $options[$#options]->[0];
        my $y = $options[$#options]->[1];
        pop @options;
        $self->{map}->[$y]->[$x] = $robot;
        $robot->setXY($x, $y);
        push(@added, $robot);
        $id += 1;
    }
    return @added;
}

sub robot {
    return $_[0]->{robots}->{$_[1]};
}

sub robots {
    return values %{$_[0]->{robots}};
}

sub hasWork {
    return 1 if scalar keys %{$_[0]->{robots}};
    return 0;
}

sub normalise {
    my($i, $min, $length) = @_;
    $$i += $length while $$i < $min;
    $$i -= $length while $$i >= $min + $length;
}

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

    # disassociate dead bodies
    my @robots = ();
    foreach my $robot (values %{$self->{robots}}) {
        my $x = $robot->x;
        my $y = $robot->y;
        $self->{map}->[$y]->[$x] = $tileNothing;
        if ($robot->value('alive')) {
            push(@robots, $robot);
            $robot->setValue('lifetime', $robot->value('lifetime') + 1);
        }
    }

    $self->runMovement(@robots);
    $self->runShooting(@robots);
    $self->runSpecialActions(@robots);

    my $robots = {};
    foreach my $robot (@robots) {
        $robots->{$robot->id} = $robot;
    }
    $self->{robots} = $robots;
}

sub runMovement {
    my($self, @robots) = @_;
    my $newMap = {};
    foreach my $robot (@robots) {
        my $x = $robot->x;
        my $y = $robot->y;
        if ($robot->dx != 0 or $robot->dy != 0) {
            $x += $robot->dx;
            $y += $robot->dy;
            normalise(\$x, 0, scalar(@{$self->{map}->[0]}));
            normalise(\$y, 0, scalar(@{$self->{map}}));
            $robot->data->{lastResult} = $robliterator::robot::actionMoved;
        }
        if (not exists $newMap->{$y}) {
            $newMap->{$y} = {};
        }
        if (not exists $newMap->{$y}->{$x}) {
            $newMap->{$y}->{$x} = [];
        }
        push(@{$newMap->{$y}->{$x}}, $robot);
    }
    my @collisions = ();
    foreach my $y (keys %{$newMap}) {
        foreach my $x (keys %{$newMap->{$y}}) {
            if (scalar(@{$newMap->{$y}->{$x}}) == 1) {
                if ($self->{map}->[$y]->[$x] eq $tileNothing) {
                    next;
                } else {
                    # we know we don't have a robot-robot collision,
                    # because only one robot tried to come here; and
                    # no robot was here before, since if there was a
                    # robot here before, it would be $tileNothing.
                    # so this is either a wall or a pickup.
                    # handle pickups
                    # ...
                    # if it was a pickup:
                    #    next;
                    # otherwise, fall through, must be a wall
                }
            }
            my $n = scalar @{$newMap->{$y}->{$x}};
            push(@collisions, @{$newMap->{$y}->{$x}});
            delete $newMap->{$y}->{$x};
        }
    }
    while (@collisions) {
        my $robot = pop @collisions;
        my $x = $robot->x;
        my $y = $robot->y;
        if (not exists $newMap->{$y}) {
            $newMap->{$y} = {};
        } elsif (exists $newMap->{$y}->{$x}) {
            if (scalar(@{$newMap->{$y}->{$x}}) == 1 and $newMap->{$y}->{$x}->[0] = $robot) {
                next;
            }
            if (exists $newMap->{$y}->{$x}) {
                push(@collisions, @{$newMap->{$y}->{$x}});
                delete $newMap->{$y}->{$x};
            }
        }
        $newMap->{$y}->{$x} = [];
        if ($robot->dx != 0 or $robot->dy != 0) {
            $robot->setMovement(0,0);
            $robot->data->{lastResult} = $robliterator::robot::actionCollided;
        }
        push(@{$newMap->{$y}->{$x}}, $robot);
    }
    foreach my $y (keys %{$newMap}) {
        foreach my $x (keys %{$newMap->{$y}}) {
            my $robot = $newMap->{$y}->{$x}->[0];
            $self->{map}->[$y]->[$x] = $robot;
            $robot->setXY($x, $y);
            $robot->setValue('distance', $robot->value('distance') + sqrt($robot->dx ** 2 + $robot->dy ** 2));
        }
    }
}

sub runShooting {
    my($self, @robots) = @_;
    foreach my $robot (@robots) {
        my $x = $robot->x;
        my $y = $robot->y;
        my $fx = $robot->fx;
        my $fy = $robot->fy;
        if ($fx != 0 or $fy != 0) {
            my $target;
            do {
                $x += $fx;
                $y += $fy;
                normalise(\$x, 0, scalar(@{$self->{map}->[0]}));
                normalise(\$y, 0, scalar(@{$self->{map}}));
                $target = $self->{map}->[$y]->[$x];
            } while (not ref $target and $target eq $tileNothing);
            if (ref $target) {
                $target->setValue('alive', 0);
                $robot->setValue('kills', $robot->value('kills') + 1);
                $robot->data->{lastResult} = $robliterator::robot::actionKilled;
            } else {
                # handle destructable walls
                # ...
                $robot->data->{lastResult} = $robliterator::robot::actionMissed;
            }
        }
    }
}

sub runSpecialActions {
    my($self, @robots) = @_;
    foreach my $robot (@robots) {
        if ($robot->data->{lastAction} eq 'error') {
            if ($robot->value('errors') > $maxErrors) {
                $robot->setValue('alive', 0);
                $robot->data->{lastResult} = $robliterator::robot::actionSuicide;
            }
            $robot->setValue('errors', $robot->value('errors') + 1);
        } else {
            $robot->setValue('errors', 0);
            if ($robot->data->{lastAction} eq 'suicide') {
                $robot->setValue('alive', 0);
                $robot->data->{lastResult} = $robliterator::robot::actionSuicide;
            }
        }
    }
}
