package Urb::Topologist::Cell;

use strict;
use warnings;
use 5.010;
use Data::Dumper;
use Graph::Directed;
use File::DXF::Math qw/average_3d/;

sub new
{
    my $class = shift;
    $class = ref $class || $class;
    my $self = {};
    bless $self, $class;
    return $self;
}

sub faces
{
    my $self = shift;
    return map {$self->{face_ref}->{$_}} keys %{$self->{face_ref}};
}

sub faces_top
{
    my $self = shift;
    map {($_->elevation == $self->elevation + $self->height and $_->height == 0) ? $_ : ()} $self->faces;
}

sub faces_bottom
{
    my $self = shift;
    map {($_->elevation == $self->elevation and $_->height == 0) ? $_ : ()} $self->faces;
}

sub cells_above
{
    my $self = shift;
    my $results = {};
    for ($self->faces_top)
    {
        $results->{$_->front} = $_->front unless $_->front eq $self or $_->front->is_world;
        $results->{$_->back} = $_->back unless $_->back eq $self or $_->front->is_world;
    }
    map {$results->{$_}} keys %{$results};
}

sub cells_below
{
    my $self = shift;
    my $results = {};
    for ($self->faces_bottom)
    {
        $results->{$_->front} = $_->front unless $_->front eq $self or $_->front->is_world;
        $results->{$_->back} = $_->back unless $_->back eq $self or $_->back->is_world;
    }
    map {$results->{$_}} keys %{$results};
}

sub nodes
{
    my $self = shift;
    my $node_ref = {};
    for my $face ($self->faces)
    {
        $node_ref->{$_} = $_ for ($face->nodes);
    }
    map {$node_ref->{$_}} keys %{$node_ref};
}

sub centroid
{
    my $self = shift;
    average_3d(map {$_->{coor}} $self->nodes);
}

sub is_external
{
    my $self = shift;
    return 1 if $self->is_outside;
    return 1 if $self->is_world;
    return 0;
}

sub is_outside
{
    my $self = shift;
    return 1 if defined $self->{type} and $self->{type} =~ /^o/xi;
    return 0;
}

# assumes that the outer cell always has the most faces
sub is_world
{
    my $self = shift;
    return 1 if defined $self->{type} and $self->{type} =~ /world/xi;
    my $outer_candidate = $self;
    for ($self->{topologist}->cells)
    {
        $outer_candidate = $_ if scalar($_->faces) > scalar($outer_candidate->faces);
    }
    $self->{type} = 'World' if $outer_candidate eq $self;
    return 1 if $outer_candidate eq $self;
    return 0;
}

sub elevation
{
    my $self = shift;
    my $lowest = 9999999;
    for (map {$_->{coor}} $self->nodes)
    {
        $lowest = $_->[2] if $_->[2] < $lowest;
    }
    return $lowest;
}

sub height
{
    my $self = shift;
    my $highest = -9999999;
    for (map {$_->{coor}} $self->nodes)
    {
        $highest = $_->[2] if $_->[2] > $highest;
    }
    return $highest - $self->elevation;
}

sub plan_area
{
    my $self = shift;
    my $area = 0;
    $area += $_->area for $self->faces_bottom;
    return $area;
}

sub external_wall_area
{
    my $self = shift;
    my $area = 0;
    for my $face ($self->faces)
    {
        next unless $face->is_vertical;
        next unless $face->is_external;
        $area += $face->area;
    }
    return $area;
}

sub crinkliness
{
    my $self = shift;
    return $self->external_wall_area / $self->plan_area;
}

sub perimeter
{
    my $self = shift;
    my $graph = Graph::Directed->new;
    for (map {$_->is_vertical ? $_ :()} $self->faces)
    {
        next unless $_->elevation == $self->elevation;
        next unless $_->axis_by_cell($self);
        $graph->add_edge($_->axis_by_cell($self));
    }
    my @cycle = $graph->find_a_cycle;
    say STDERR 'Warning: zero size perimeter' unless scalar @cycle;
    return @cycle;
}

# this assumes a convex hull, but should work for any normal room
sub point_is_inside
{
    my $self = shift;
    my $point = shift;
    my ($above, $below) = (0, 0);
    for ($self->faces)
    {
        if ($_->point_is_above_below($point))
        {
            $above = 1 if $_->point_vertical_distance($point) > 0;
            $below = 1 if $_->point_vertical_distance($point) < 0;
        }
    }
    return 1 if $above and $below;
    return 0;
}

1;
