package Urb::Topologist::Face;

use strict;
use warnings;
use 5.010;
use Data::Dumper;
use File::DXF::Math qw/triangle_normal points_to_plane average_3d point_is_in_triangle line_plane_intersection add_3d area_3d/;

sub coors
{
    my $self = shift;
    return map {$_->{coor}} $self->nodes;
}

sub nodes
{
    my $self = shift;
    return map {$self->{topologist}->{nodes}->[$_]} @{$self->{node_id}};
}

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

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

sub axis_outer
{
    my $self = shift;
    my @nodes = $self->nodes;
    push (@nodes, shift @nodes) while $nodes[0]->{coor}->[2] < $nodes[1]->{coor}->[2];
    my @axis = map {abs($_->{coor}->[2] - $self->elevation) < 0.0001 ? $_ :()} @nodes;
    @axis = ($axis[0], $axis[-1]) if scalar @axis > 2;
    return () unless scalar @axis == 2;
    return @axis if $self->front->is_external;
    reverse @axis;
}

sub axis_outer_top
{
    my $self = shift;
    my @nodes = $self->nodes;
    push (@nodes, shift @nodes) while $nodes[0]->{coor}->[2] > $nodes[1]->{coor}->[2];
    my @axis = map {abs($_->{coor}->[2] - $self->elevation - $self->height) < 0.0001 ? $_ :()} @nodes;
    @axis = ($axis[0], $axis[-1]) if scalar @axis > 2;
    return () unless scalar @axis == 2;
    return @axis if !$self->front->is_external;
    reverse @axis;
}

sub axis_by_cell
{
    my $self = shift;
    my $cell = shift;
    my @nodes = $self->nodes;
    push (@nodes, shift @nodes) while $nodes[0]->{coor}->[2] < $nodes[1]->{coor}->[2];
    my @axis = map {abs($_->{coor}->[2] - $self->elevation) < 0.0001 ? $_ :()} @nodes;
    @axis = ($axis[0], $axis[-1]) if scalar @axis > 2;
    return () unless scalar @axis == 2;
    return @axis if $self->front eq $cell;
    reverse @axis
}

sub front
{
    my $self = shift;
    $self->{cell_ref}->{front} = shift if @_;
    $self->{cell_ref}->{front};
}

sub back
{
    my $self = shift;
    $self->{cell_ref}->{back} = shift if @_;
    $self->{cell_ref}->{back};
}

sub types
{
    my $self = shift;
    return ($self->front->{type}, $self->back->{type});
}

sub type_inside
{
    my $self = shift;
    return $self->front->{type} if $self->back->{type} =~ /^o/xi;
    return $self->back->{type} if $self->front->{type} =~ /^o/xi;
    return undef;
}

sub normal
{
    my $self = shift;
    triangle_normal($self->coors);
}

sub plane
{
    my $self = shift;
    points_to_plane($self->coors);
}

sub centroid
{
    my $self = shift;
    average_3d($self->coors);
}

sub area
{
    my $self = shift;
    my $area = 0;
    $area += area_3d(@{$_}) for $self->to_triangles;
    return $area;
}

# assumes convex polygons
sub to_triangles
{
    my $self = shift;
    my @coors = $self->coors;
    map {[$coors[0], $coors[$_], $coors[$_+1]]} (1 .. scalar @coors -2);
}

sub is_vertical
{
    my $self = shift;
    return 1 if abs($self->normal->[2]) < 0.0001;
    return 0;
}

sub is_horizontal
{
    my $self = shift;
    return 1 if abs($self->normal->[2]) > 0.9999;
    return 0;
}

sub is_internal
{
    my $self = shift;
    if ($self->front eq 'EMPTY' or $self->back eq 'EMPTY') {
        say STDERR 'warning: is_internal() empty cell found!'; return 0;
    }
    return 1 if !$self->front->is_external and !$self->back->is_external;
    return 0;
}

sub is_external
{
    my $self = shift;
    if ($self->front eq 'EMPTY' or $self->back eq 'EMPTY') {
        say STDERR 'warning: is_external() empty cell found!'; return 0;
    }
    return 1 if $self->front->is_external and !$self->back->is_external;
    return 1 if !$self->front->is_external and $self->back->is_external;
    return 0;
}

sub is_world
{
    my $self = shift;
    if ($self->front eq 'EMPTY' or $self->back eq 'EMPTY') {
        say STDERR 'warning: is_world() empty cell found!'; return 0;
    }
    return 1 if $self->front->is_world and !$self->back->is_world;
    return 1 if !$self->front->is_world and $self->back->is_world;
    return 0;
}

sub is_open
{
    my $self = shift;
    if ($self->front eq 'EMPTY' or $self->back eq 'EMPTY') {
        say STDERR 'warning: is_external() empty cell found!'; return 0;
    }
    return 1 if $self->front->is_world and $self->back->is_outside;
    return 1 if $self->front->is_outside and $self->back->is_world;
    return 0;
}

sub is_face_above
{
    my $self = shift;
    return 0 unless $self->axis_outer_top;
    my $top = [$self->axis_outer_top];
    for my $candidate ($self->neighbours)
    {
        next unless $candidate->is_vertical;
        my $nodes = {};
        for ($candidate->nodes) {$nodes->{$_} = $_}
        return $candidate if defined $nodes->{$top->[0]} and defined $nodes->{$top->[1]};
    }
    return 0;
}

sub is_face_below
{
    my $self = shift;
    return 0 unless $self->axis_outer;
    my $bottom = [$self->axis_outer];
    for my $candidate ($self->neighbours)
    {
        next unless $candidate->is_vertical;
        my $nodes = {};
        for ($candidate->nodes) {$nodes->{$_} = $_}
        return $candidate if defined $nodes->{$bottom->[0]} and defined $nodes->{$bottom->[1]};
    }
    return 0;
}

sub horizontal_faces_sideways
{
    my $self = shift;
    my $bottom = [$self->axis_outer];
    my @results;
    for my $candidate ($self->neighbours)
    {
        next unless $candidate->is_horizontal;
        my $candidate_nodes = {};
        for ($candidate->nodes)
        {
            $candidate_nodes->{$_} = $_;
        }
        next if $candidate eq $self;
        push(@results, $candidate) if defined $candidate_nodes->{$bottom->[0]} and defined $candidate_nodes->{$bottom->[1]};
    }
    return @results;
}

sub neighbours
{
    my $self = shift;
    $self->{topologist}->{graph_faces}->neighbours($self);
}

sub point_is_above_below
{
    my $self = shift;
    my $point = shift;
    my $coors = [$self->coors];
    say STDERR 'warning: point_is_above_below() expects triangle or quad' if scalar @{$coors} > 4;
    return 1 if point_is_in_triangle([$coors->[0]->[0],$coors->[0]->[1],0],
                                     [$coors->[1]->[0],$coors->[1]->[1],0],
                                     [$coors->[2]->[0],$coors->[2]->[1],0],
                                     [$point->[0],$point->[1],0]);
    return 0 if scalar @{$coors} < 4;
    return 1 if point_is_in_triangle([$coors->[0]->[0],$coors->[0]->[1],0],
                                     [$coors->[2]->[0],$coors->[2]->[1],0],
                                     [$coors->[3]->[0],$coors->[3]->[1],0],
                                     [$point->[0],$point->[1],0]);
    return 0;
}

sub point_vertical_distance
{
    my $self = shift;
    my $point = shift;
    return line_plane_intersection($point, add_3d($point,[0,0,1]), $self->coors);
}

1;
