package Urb::Topologist;

use strict;
use warnings;
use 5.010;
use Data::Dumper;
use File::DXF::Math qw /angle_vectors_3d subtract_3d plane_distance PI add_3d/;
use Graph::Directed;
use Graph::Undirected;
use Urb::Topologist::Face;
use Urb::Topologist::Cell;

sub new
{
    my $class = shift;
    $class = ref $class || $class;
    my ($nodes, $faces, $metadata) = @_;
    my $id = 0;
    for (@{$nodes})
    {
        $_->{coor}->[2] = _el($_->{coor}->[2]);
        $_->{node_id} = $id; $id++;
    }
    my $self = {nodes => $nodes, faces => $faces};
    $id = 0;
    for (@{$faces})
    {
        bless $_, 'Urb::Topologist::Face';
        $_->{topologist} = $self;
        $_->{face_id} = $id;
        say STDERR 'warning: face is neither horizontal or vertical' unless ($_->is_vertical or $_->is_horizontal);
        $id++;
    }

    say STDERR 'nodes: '. scalar @{$self->{nodes}};
    say STDERR 'faces: '. scalar @{$self->{faces}};
    bless $self, $class;
    return $self;
}

sub build_graph_nodes
{
    my $self = shift;
    $self->{graph_nodes} = Graph::Undirected->new;
    for my $face (@{$self->{faces}})
    {
        $face->front('EMPTY');
        $face->back('EMPTY');

        for (0 .. scalar @{$face->{node_id}}-1)
        {
            my $node_a = $self->{nodes}->[$face->{node_id}->[$_-1]];
            my $node_b = $self->{nodes}->[$face->{node_id}->[$_]];

            $self->{graph_nodes}->set_edge_attribute($node_a, $node_b, $face, $face);

            $node_a->{face_ref}->{$face} = $face;
            $node_b->{face_ref}->{$face} = $face;

            $face->{node_ref}->{$node_a} = $node_a;
            $face->{node_ref}->{$node_b} = $node_b;
        }
    }
    say STDERR 'edges: '. scalar $self->{graph_nodes}->edges;
}

sub build_graph_faces
{
    my $self = shift;

    $self->{graph_faces} = Graph::Undirected->new;
    for my $edge ($self->{graph_nodes}->edges)
    {
        my ($node_a, $node_b) = @{$edge};
        my @faces_adjacent;
        for my $face (@{$self->{faces}})
        {
            push @faces_adjacent, $face if defined $face->{node_ref}->{$node_a}
                              and defined $face->{node_ref}->{$node_b};
        }
        for my $id_a (0 .. scalar @faces_adjacent-2)
        {
            for my $id_b ($id_a+1 .. scalar @faces_adjacent-1)
            {
                my $face_a = $faces_adjacent[$id_a];
                my $face_b = $faces_adjacent[$id_b];
                $face_a->{edge_ref}->{$edge} = $edge;
                $face_b->{edge_ref}->{$edge} = $edge;
                $self->{graph_faces}->set_edge_attribute($face_a, $face_b, 'edge_ref', $edge);
            }
        }
    }
}

sub build_graph_cells
{
    my $self = shift;

    my $to_process;
    map {$to_process->{$_} = $_} $self->{graph_faces}->vertices;

    while (keys %{$to_process})
    {
        my $cell = Urb::Topologist::Cell->new;

        my ($face_seed) = map {$to_process->{$_}} keys %{$to_process};
        if ($face_seed->front eq 'EMPTY') {
            $face_seed->front($cell);
        }
        elsif ($face_seed->back eq 'EMPTY') {
            $face_seed->back($cell);
        }
        else {
            next;
        }
        $cell->{face_ref}->{$face_seed} = $face_seed;
        $cell->{topologist} = $self;

        my $updated = 1;
        while ($updated)
        {
            $updated = 0;

            # try all the faces and if already tagged to this cell, propagate to adjacent faces
            for my $face_main ($self->{graph_faces}->vertices)
            {
                # skip this face if it is unrelated to this cell (yet)
                next unless ($face_main->front eq $cell or $face_main->back eq $cell);
                # skip this face if it has already been processed for this cell
                next if defined $face_main->{done}->{$cell};

                # $edge is a pair of nodes with attributes: face_ref and coor
                for my $edge (map {$face_main->{edge_ref}->{$_}} keys %{$face_main->{edge_ref}})
                {
                    my @faces;
                    # $face has attributes: node_id, node_ref, edge_ref, colour
                    for my $face ($self->{graph_faces}->neighbours($face_main))
                    {
                        push (@faces, $face)
                          if (defined $face->{node_ref}->{$edge->[0]} and defined $face->{node_ref}->{$edge->[1]});
                        delete $to_process->{$face} unless $face->front eq 'EMPTY' or $face->back eq 'EMPTY';
                    }
                    # we have picked a $face_main and one $edge, @faces all connect via this edge
                    # pick the faces from @faces forming the smallest and largest crease angle with $face_main
                    my $angles_faces = {};
                    $angles_faces->{_angle_faces($face_main, $_)} = $_ for @faces;
                    my @angles = (sort {$a <=> $b} keys %{$angles_faces});
                    say STDERR 'warning: lost face' unless scalar(@angles) == scalar(@faces);

                    my $face_first = $angles_faces->{$angles[0]};
                    my $face_last = $angles_faces->{$angles[-1]};

                    say STDERR 'warning: first and last face the same, but multiple faces!'
                        if scalar(@faces) >1 and $face_first eq $face_last;

                    # propagate the cell reference from the front of $face_main
                    unless ($face_main->front eq 'EMPTY')
                    {
                        if (_shared_normals($face_main, $face_first)) {
                            $face_first->front($face_main->front);
                        }
                        else {
                            $face_first->back($face_main->front);
                        }
                        $updated = 1;
                    }
                    # propagate the cell reference from the back of $face_main
                    unless ($face_main->back eq 'EMPTY')
                    {
                        if (_shared_normals($face_main, $face_last)) {
                            $face_last->back($face_main->back);
                        }
                        else {
                            $face_last->front($face_main->back);
                        }
                        $updated = 1;
                    }
                }
                delete $to_process->{$face_main} unless $face_main->front eq 'EMPTY' or $face_main->back eq 'EMPTY';
                $cell->{face_ref}->{$face_main} = $face_main;
                $face_main->{done}->{$cell} = 1;
            }
        }
    }

    $self->{graph_cells} = Graph::Undirected->new;
    $self->{graph_circulation} = Graph::Undirected->new;
    for my $face ($self->{graph_faces}->vertices)
    {
        say STDERR 'warning: build_graph_cells() empty cell found' if $face->front eq 'EMPTY' or $face->back eq 'EMPTY';
        say STDERR 'warning: build_graph_cells() front and back cell are the same! '. join(',', @{$face->centroid})
            if $face->front eq $face->back;
        $self->{graph_cells}->set_edge_attribute($face->front, $face->back, 'face_ref', $face);
        $self->{graph_circulation}->set_edge_attribute($face->front, $face->back, 'face_ref', $face);
    }
    say STDERR 'cells: '. scalar $self->{graph_cells}->vertices;
}

sub cells
{
    my $self = shift;
    return $self->{graph_cells}->vertices;
}

sub allocate_cells
{
    my ($self, @widgets) = @_;
    my $id = 0;
    for my $cell ($self->cells)
    {
        $cell->{type} = 'Living' unless scalar @widgets;
        for (@widgets)
        {
            $cell->{type} = $_->[1] if $cell->point_is_inside(add_3d($_->[0],[0,0,0.5]));
        }
        $cell->{type} = 'Outside' unless defined $cell->{type};
        $cell->{type} = 'Outside' if $cell->is_external;
        $cell->{type} = 'Outside_world' if $cell->is_world;
        $cell->{id} = $id;
        $id++;
    }
}

sub prune_graph
{
    my $self = shift;
    for my $edge ($self->{graph_circulation}->edges)
    {
        my $face = $self->{graph_circulation}->get_edge_attribute(@{$edge}, 'face_ref');
        if ($face->is_horizontal)
        {
            $self->{graph_circulation}->delete_edge(@{$edge}) unless $edge->[0]->{type} =~ /stair/xi and $edge->[1]->{type} =~ /stair/xi;
        }
        if ($face->is_vertical)
        {
            my $delete_edge = 0;
            $delete_edge = 1 unless $edge->[0]->elevation == $edge->[1]->elevation;
            $delete_edge = 1 if $edge->[0]->{type} =~ /^(toilet|bedroom)/xi and !($edge->[1]->{type} =~ /^circulation/xi);
            $delete_edge = 1 if $edge->[1]->{type} =~ /^(toilet|bedroom)/xi and !($edge->[0]->{type} =~ /^circulation/xi);

            $self->{graph_circulation}->delete_edge(@{$edge}) if $delete_edge;
        }
    }
}

sub faces_vertical
{
    my $self = shift;
    map {$_->is_vertical ? $_ : ()} $self->{graph_faces}->vertices;
}

sub faces_vertical_internal
{
    my $self = shift;
    map {$_->is_internal ? $_ : ()} $self->faces_vertical;
}

sub faces_vertical_external
{
    my $self = shift;
    map {$_->is_external ? $_ : ()} $self->faces_vertical;
}

sub faces_vertical_world
{
    my $self = shift;
    map {$_->is_world ? $_ : ()} $self->faces_vertical;
}

sub faces_vertical_open
{
    my $self = shift;
    map {$_->is_open ? $_ : ()} $self->faces_vertical;
}

sub faces_horizontal
{
    my $self = shift;
    map {$_->is_horizontal ? $_ : ()} $self->{graph_faces}->vertices;
}

sub faces_horizontal_internal
{
    my $self = shift;
    map {$_->is_internal ? $_ : ()} $self->faces_horizontal;
}

sub faces_horizontal_external
{
    my $self = shift;
    map {$_->is_external ? $_ : ()} $self->faces_horizontal;
}

sub walls_external
{
    my $self = shift;
    my $walls_external = {};
    for my $face ($self->faces_vertical_external)
    {
        my $elevation = $face->elevation;
        my $height = $face->height;
        $walls_external->{$elevation} = {} unless defined $walls_external->{$elevation};
        $walls_external->{$elevation}->{$height} = Graph::Directed->new unless defined $walls_external->{$elevation}->{$height};
        next unless scalar $face->axis_outer;
        $walls_external->{$elevation}->{$height}->add_edge($face->axis_outer);
        $walls_external->{$elevation}->{$height}->set_edge_attribute($face->axis_outer, 'face_ref', $face);
    }
    return $walls_external;
}

sub walls_external_unsupported
{
    my $self = shift;
    my $walls_external = {};
    for my $face ($self->faces_vertical_external)
    {
        next if $face->is_face_below;
        my $elevation = $face->elevation;
        $walls_external->{$elevation} = Graph::Directed->new unless defined $walls_external->{$elevation};
        next unless scalar $face->axis_outer;
        $walls_external->{$elevation}->add_edge($face->axis_outer);
        $walls_external->{$elevation}->set_edge_attribute($face->axis_outer, 'face_ref', $face);
    }
    return $walls_external;
}

sub walls_eaves
{
    my $self = shift;
    my $walls_external = {};
    for my $face ($self->faces_vertical_world)
    {
        next if $face->is_face_above and !$face->is_face_above->is_open;
        my $elevation = $face->elevation + $face->height;
        $walls_external->{$elevation} = Graph::Directed->new unless defined $walls_external->{$elevation};
        next unless scalar $face->axis_outer_top;
        $walls_external->{$elevation}->add_edge($face->axis_outer_top);
        $walls_external->{$elevation}->set_edge_attribute($face->axis_outer_top, 'face_ref', $face);
    }
    return $walls_external;
}

sub walls_open
{
    my $self = shift;
    my $walls_external = {};
    for my $face ($self->faces_vertical_open)
    {
        my $elevation = $face->elevation;
        my $height = $face->height;
        $walls_external->{$elevation} = {} unless defined $walls_external->{$elevation};
        $walls_external->{$elevation}->{$height} = Graph::Directed->new unless defined $walls_external->{$elevation}->{$height};
        next unless scalar $face->axis_outer;
        $walls_external->{$elevation}->{$height}->add_edge($face->axis_outer);
        $walls_external->{$elevation}->{$height}->set_edge_attribute($face->axis_outer, 'face_ref', $face);
    }
    return $walls_external;
}

sub walls_internal
{
    my $self = shift;
    my $walls_internal = {};
    for my $face ($self->faces_vertical_internal)
    {
        my $elevation = $face->elevation;
        my $height = $face->height;
        $walls_internal->{$elevation} = {} unless defined $walls_internal->{$elevation};
        $walls_internal->{$elevation}->{$height} = Graph::Directed->new unless defined $walls_internal->{$elevation}->{$height};
        next unless scalar $face->axis_outer;
        $walls_internal->{$elevation}->{$height}->add_edge($face->axis_outer);
        $walls_internal->{$elevation}->{$height}->set_edge_attribute($face->axis_outer, 'face_ref', $face);
    }
    return $walls_internal;
}

sub walls_internal_unsupported
{
    my $self = shift;
    my $walls_internal = {};
    for my $face ($self->faces_vertical_internal)
    {
        next if $face->is_face_below;
        my $elevation = $face->elevation;
        $walls_internal->{$elevation} = Graph::Directed->new unless defined $walls_internal->{$elevation};
        next unless scalar $face->axis_outer;
        $walls_internal->{$elevation}->add_edge($face->axis_outer);
        $walls_internal->{$elevation}->set_edge_attribute($face->axis_outer, 'face_ref', $face);
    }
    return $walls_internal;
}

sub cells_internal
{
    my $self = shift;
    my $cells_internal = {};
    for my $cell ($self->cells)
    {
        if ($cell eq 'EMPTY') {
            say STDERR 'warning: cells_internal() empty cell found!'; next;
        }
        next if $cell->is_world;
        push @{$cells_internal->{$cell->elevation}}, $cell;
    }
    return $cells_internal;
}

sub dot
{
    my $self = shift;
    my $string = "strict graph G {\n";
    $string .= "graph [overlap=false];\n";

    for my $cell ($self->{graph_circulation}->vertices)
    {
        if ($cell eq 'EMPTY') {
            say STDERR 'warning: dot() empty cell found!'; next;
        }
        next if $cell->is_world;
        my $text = $cell->{type}.'.'.$cell->{id};
        my $fill = '#666666';
        $fill = '#66EE66' if ($cell->{type} =~ /^s/xi);
        $fill = '#88EEDD' if ($cell->{type} =~ /^t/xi);
        $fill = '#99CCEE' if ($cell->{type} =~ /^k/xi);
        $fill = '#EE5599' if ($cell->{type} =~ /^b/xi);
        $fill = '#CC77EE' if ($cell->{type} =~ /^r/xi);
        $fill = '#EE5555' if ($cell->{type} =~ /^c/xi);
        $fill = '#EE8844' if ($cell->{type} =~ /^c.*_s/xi);
        $fill = '#EEDD66' if ($cell->{type} =~ /^l/xi);
        $string .= '"'. $text .'" [color="'. $fill .'",style=filled];'. "\n";
    }
    for my $edge ($self->{graph_circulation}->edges)
    {
        if ($edge->[0] eq 'EMPTY' or $edge->[1] eq 'EMPTY') {
            say STDERR 'warning: dot() empty cell found!'; next;
        }
        next if $edge->[0]->is_world or $edge->[1]->is_world;
        my ($text0, $text1) = ($edge->[0]->{type}.'.'.$edge->[0]->{id}, $edge->[1]->{type}.'.'.$edge->[1]->{id});

        $string .= '"'. $text0 .'"--"'. $text1 .'"'."\n";
    }

    $string .= '}';
    return $string;
}

sub elevations
{
    my $self = shift;
    my $elevations = {};
    for ($self->{graph_faces}->vertices)
    {
        $elevations->{$_->elevation} = 1;
    }
    my $level = 0;
    for (sort {$a <=> $b} (keys %{$elevations}))
    {
        $elevations->{$_} = $level;
        $level++;
    }
    return $elevations;
}

sub _angle_faces
{
    my ($face_a, $face_b) = @_;

    my ($normal_a, $normal_b) = ($face_a->normal, $face_b->normal);
    $normal_b = subtract_3d([0,0,0], $normal_b) unless _shared_normals($face_a, $face_b);

    my $angle_normals = angle_vectors_3d($normal_a, $normal_b);

    my $distance = plane_distance($face_a->plane, $face_b->centroid);
    if ($distance > 0.0001)
    {
        #second face is above first face
        return PI() - $angle_normals;
    }
    elsif ($distance < -0.0001)
    {
        #second face is behind first face
        return PI() + $angle_normals;
    }
    else
    {
        #second face is co-planar
        return PI();
    }
}

sub _shared_normals
{
    my ($face_a, $face_b) = @_;
    for my $u (0 .. scalar @{$face_a->{node_id}}-1)
    {
        for my $v (0 .. scalar @{$face_b->{node_id}}-1)
        {
            return 0 if $face_a->{node_id}->[$u-1] == $face_b->{node_id}->[$v-1] and $face_a->{node_id}->[$u] == $face_b->{node_id}->[$v];
            return 1 if $face_a->{node_id}->[$u-1] == $face_b->{node_id}->[$v] and $face_a->{node_id}->[$u] == $face_b->{node_id}->[$v-1];
        }
    }
    say STDERR 'warning: faces don\'t share edges';
}

# rounds elevation to nearest millimeter (assuming units are meters)
sub _el
{
    my $elevation = shift;
    return int(($elevation*1000) +0.5)/1000 if $elevation >= 0.0;
    return int(($elevation*1000) -0.5)/1000 if $elevation < 0.0;
}

1;
