#!/usr/bin/perl

use strict;
use warnings;
use 5.010;
use Carp;
use Data::Dumper;

use File::DXF;
use File::DXF::Math qw /average_3d magnitude_3d angle_2d distance_2d scale_2d add_2d/;
use File::DXF::Util qw /read_DXF/;
use Urb::Topologist;
use Graph::Directed;
use Math::Trig;
use YAML;
use Molior;

croak "usage: $0 input.dxf output.molior" unless @ARGV == 2;

my (@raw_meshes, @widgets, @meshes);

if ($ARGV[0] =~ /\.dxf$/) {
my $dxf = File::DXF->new;
$dxf->Process(read_DXF($ARGV[0], 'CP1252'));
my @report = $dxf->{ENTITIES}->Report('POLYLINE', 'VERTEX', 'SEQEND');
@raw_meshes = File::DXF::ENTITIES::polyface_to_meshes(@report);
}
else {
@raw_meshes = @{YAML::LoadFile($ARGV[0])};
}

push(@meshes, shift @raw_meshes) if scalar @raw_meshes == 1;
for (@raw_meshes)
{
    my $centre = average_3d(map {magnitude_3d($_) ? $_ :()} map {$_->{coor}} @{$_->[0]});
    my $number_faces = scalar @{$_->[1]};
    $number_faces /= 2 if scalar @{$_->[1]->[0]->{node_id}} == 3;
    push @widgets, [$centre, 'Retail'] if ($number_faces == 2);
    push @widgets, [$centre, 'Circulation'] if ($number_faces == 3);
    push @widgets, [$centre, 'Circulation_stair'] if ($number_faces == 4);
    push @widgets, [$centre, 'Toilet'] if ($number_faces == 5);
    push @widgets, [$centre, 'Bedroom'] if ($number_faces == 6);
    push @widgets, [$centre, 'Kitchen'] if ($number_faces == 7);
    push @widgets, [$centre, 'Living'] if ($number_faces == 8);
    push @widgets, [$centre, 'Sahn'] if ($number_faces == 9);
    push @meshes, $_ if ($number_faces > 9);
}

my ($first_mesh) = @meshes or die 'no meshes :(';
my ($nodes, $faces, $metadata) = @{$first_mesh};

say STDERR 'meshes: '. scalar(@meshes);
say STDERR 'widgets: '. join ', ', scalar(@widgets), map {$_->[1]} @widgets;

my $topologist = Urb::Topologist->new($nodes, $faces);

$topologist->build_graph_nodes;
$topologist->build_graph_faces;
$topologist->build_graph_cells;
$topologist->allocate_cells(@widgets);
$topologist->prune_graph;

say STDERR keys (%{$topologist->elevations}) -1 .' storeys';

open my $fh_dot, '>', 'graph.dot';
print $fh_dot $topologist->dot;
close $fh_dot;
#system('fdp', '-Tpng', '-Gratio=fill', '-Gcenter=1', '-ograph.png', 'graph.dot');

my @molior_all;

my $walls_external = $topologist->walls_external;
my $walls_internal = $topologist->walls_internal;
my $walls_eaves = $topologist->walls_eaves;
my $walls_open = $topologist->walls_open;
my $walls_external_unsupported = $topologist->walls_external_unsupported;
my $walls_internal_unsupported = $topologist->walls_internal_unsupported;
my $cells_internal = $topologist->cells_internal;

say STDERR scalar @{$_} .' rooms: '. join(', ', sort map {$_->{type}} @{$_}) for map {$cells_internal->{$_}} sort keys (%{$cells_internal});

my $style = undef;
$style = $ENV{MOLIOR_STYLE} if defined $ENV{MOLIOR_STYLE};

for my $elevation (sort {$a <=> $b} keys %{$topologist->elevations})
{
    my $level = $topologist->elevations->{$elevation};
    for my $height (sort {$a <=> $b} keys %{$walls_external->{$elevation}})
    {
        for my $source_vertex ($walls_external->{$elevation}->{$height}->source_vertices)
        {
            my @chain = $walls_external->{$elevation}->{$height}->_chain($source_vertex);

            my $molior = Molior->new ({type => 'molior-wall', guid => 'my building', name => 'exterior',
                                  elevation => $elevation,
                                       path => [map {[$_->{coor}->[0],$_->{coor}->[1]]} @chain],
                                     height => $height,
                                    ceiling => 0.35,
                                      floor => 0.02,
                                      inner => 0.08,
                                      outer => 0.25,
                                  extension => 0.25,
                                     closed => 0,
                                      level => $level,
                                       plot => 'my plot',
                                      style => $style,
                                   openings => [],
                                     bounds => []});

            # size the list of openings to number of segments in path
            $molior->{openings}->[scalar @chain -2] = [];

            for (0 .. scalar @chain -2)
            {
                 my $face = $walls_external->{$elevation}->{$height}->get_edge_attribute($chain[$_], $chain[$_+1], 'face_ref');
                 push @{$molior->{openings}->[$_]}, {name => 'living outside window', along => '0.5', size => 0} if $face->type_inside =~ /^[lr]/xi;
                 push @{$molior->{openings}->[$_]}, {name => 'toilet outside window', along => '0.5', size => 0} if $face->type_inside =~ /^t/xi;
                 push @{$molior->{openings}->[$_]}, {name => 'kitchen outside window', along => '0.5', size => 0} if $face->type_inside =~ /^k/xi;
                 push @{$molior->{openings}->[$_]}, {name => 'bedroom outside window', along => '0.5', size => 0} if $face->type_inside =~ /^b/xi;
                 push @{$molior->{openings}->[$_]}, {name => 'circulation outside window', along => '0.5', size => 0} if $face->type_inside =~ /^c/xi;
                 push @{$molior->{openings}->[$_]}, {name => 'retail entrance', along => '0.5', size => 0} if $level == 0 and $face->type_inside =~ /^r/xi;
                 push @{$molior->{openings}->[$_]}, {name => 'house entrance', along => '0.5', size => 0} if $level == 0 and $face->type_inside =~ /^c.*n$/xi;
                 push @{$molior->{openings}->[$_]}, {name => 'living outside door', along => '0.5', size => 0}
                     if $face->type_inside =~ /^[lkbcr]/xi and scalar($face->horizontal_faces_sideways) == 2;
            }
            push(@molior_all, $molior->Serialise);
        }
        while ($walls_external->{$elevation}->{$height}->has_a_cycle)
        {
            my @chain = $walls_external->{$elevation}->{$height}->find_a_cycle;

            my $molior = Molior->new ({type => 'molior-wall', guid => 'my building', name => 'exterior',
                                  elevation => $elevation,
                                       path => [map {[$_->{coor}->[0],$_->{coor}->[1]]} @chain],
                                     height => $height,
                                    ceiling => 0.35,
                                      floor => 0.02,
                                      inner => 0.08,
                                      outer => 0.25,
                                  extension => 0.25,
                                     closed => 1,
                                      level => $level,
                                       plot => 'my plot',
                                      style => $style,
                                   openings => [],
                                     bounds => []});

            # size the list of openings to number of segments in path
            $molior->{openings}->[scalar @chain -1] = [];

            for (0 .. scalar @chain -1)
            {
                 my $next = $_+1; $next = 0 if $next == scalar @chain;
                 my $face = $walls_external->{$elevation}->{$height}->get_edge_attribute($chain[$_], $chain[$next], 'face_ref');
                 push @{$molior->{openings}->[$_]}, {name => 'living outside window', along => '0.5', size => 0} if $face->type_inside =~ /^[lr]/xi;
                 push @{$molior->{openings}->[$_]}, {name => 'toilet outside window', along => '0.5', size => 0} if $face->type_inside =~ /^t/xi;
                 push @{$molior->{openings}->[$_]}, {name => 'kitchen outside window', along => '0.5', size => 0} if $face->type_inside =~ /^k/xi;
                 push @{$molior->{openings}->[$_]}, {name => 'bedroom outside window', along => '0.5', size => 0} if $face->type_inside =~ /^b/xi;
                 push @{$molior->{openings}->[$_]}, {name => 'circulation outside window', along => '0.5', size => 0} if $face->type_inside =~ /^c/xi;
                 push @{$molior->{openings}->[$_]}, {name => 'retail entrance', along => '0.5', size => 0} if $level == 0 and $face->type_inside =~ /^r/xi;
                 push @{$molior->{openings}->[$_]}, {name => 'house entrance', along => '0.5', size => 0} if $level == 0 and $face->type_inside =~ /^c.*n$/xi;
                 push @{$molior->{openings}->[$_]}, {name => 'living outside door', along => '0.5', size => 0}
                     if $face->type_inside =~ /^[lkbcr]/xi and scalar($face->horizontal_faces_sideways) == 2;
            }
            $walls_external->{$elevation}->{$height}->delete_vertices (@chain);
            push(@molior_all, $molior->Serialise);
        }
    }

    for my $height (sort {$a <=> $b} keys %{$walls_internal->{$elevation}})
    {
        for my $edge ($walls_internal->{$elevation}->{$height}->edges)
        {
            my @chain = @{$edge};
            my $molior = Molior->new ({type => 'molior-wall', guid => 'my building', name => 'interior',
                                  elevation => $elevation,
                                       path => [map {[$_->{coor}->[0],$_->{coor}->[1]]} @chain],
                                     height => $height,
                                    ceiling => 0.2,
                                      floor => 0.02,
                                      inner => 0.08,
                                      outer => 0.08,
                                  extension => 0.0,
                                     closed => 0,
                                      level => $level,
                                       plot => 'my plot',
                                      style => $style,
                                   openings => [],
                                     bounds => []});

            $molior->{openings}->[0] = [];
            my $face = $walls_internal->{$elevation}->{$height}->get_edge_attribute(@chain, 'face_ref');
            my @types = $face->types;
            if ($topologist->{graph_circulation}->has_edge($face->front, $face->back))
            {
                push @{$molior->{openings}->[0]}, {name => 'living inside door', along => '0.5', size => 0};
            }

            push(@molior_all, $molior->Serialise);
        }
    }

    if (defined $walls_eaves->{$elevation})
    {
        for my $source_vertex ($walls_eaves->{$elevation}->source_vertices)
        {
            my @chain = $walls_eaves->{$elevation}->_chain($source_vertex);

            my $molior = Molior->new ({type => 'molior-extrusion', guid => 'my building', name => 'parapet',
                                  elevation => $elevation,
                                       path => [map {[$_->{coor}->[0],$_->{coor}->[1]]} @chain],
                                     height => 0.0,
                                     closed => 0,
                                      level => $level,
                                       plot => 'my plot',
                                      style => $style});

            push(@molior_all, $molior->Serialise);
            $molior->{name} = 'parapet internal'; push(@molior_all, $molior->Serialise);
        }
        while ($walls_eaves->{$elevation}->has_a_cycle)
        {
            my @chain = $walls_eaves->{$elevation}->find_a_cycle;
            $walls_eaves->{$elevation}->delete_vertices (@chain);

            my $molior = Molior->new ({type => 'molior-extrusion', guid => 'my building', name => 'eaves',
                                  elevation => $elevation,
                                       path => [map {[$_->{coor}->[0],$_->{coor}->[1]]} @chain],
                                     height => 0.0,
                                     closed => 1,
                                      level => $level,
                                       plot => 'my plot',
                                      style => $style});

            push(@molior_all, $molior->Serialise);
            $molior->{name} = 'roofline'; push(@molior_all, $molior->Serialise);

            use Urb::Polygon;
            my $chain = [map {[$_->{coor}->[0],$_->{coor}->[1]]} @chain];
            my $polygon = Urb::Polygon->new (points => [@{$chain}, $chain->[0]]);

            my @quads = $polygon->to_quads(());
            my @roof_elements;
            for my $quad (@quads)
            {
                my @points = $quad->points;
                pop @points;
                push @roof_elements, [[@points],'0000'];
            }

               $molior = Molior->new ({type => 'molior-roof', guid => 'my building',
                                  elevation => $elevation,
                                   elements => [@roof_elements],
                                     height => 0.0,
                                       name => 'roof',
                                      outer => 0.25,
                                     closed => 1,
                                      level => $level,
                                       plot => 'my plot',
                                      style => $style,
                                     colour => 250});
            push(@molior_all, $molior->Serialise) if scalar @roof_elements;
        }
    }

    for my $height (sort {$a <=> $b} keys %{$walls_open->{$elevation}})
    {
        for my $source_vertex ($walls_open->{$elevation}->{$height}->source_vertices)
        {
            my @chain = $walls_open->{$elevation}->{$height}->_chain($source_vertex);

            my $molior = Molior->new ({type => 'molior-extrusion', guid => 'my building', name => 'external beam',
                                  elevation => $elevation,
                                       path => [map {[$_->{coor}->[0],$_->{coor}->[1]]} @chain],
                                     height => $height,
                                     closed => 0,
                                      level => $level,
                                       plot => 'my plot',
                                      style => $style});

            push(@molior_all, $molior->Serialise);

            # corner columns
            for my $corner_id (1 .. scalar(@chain) -2)
            {
                my $rotation = rad2deg (angle_2d($chain[$corner_id]->{coor}, $chain[$corner_id +1]->{coor}));
                my $molior = Molior->new ({type => 'molior-insert', guid => 'my building', name => 'beam column',
                                          style => $style,
                                      elevation => $elevation,
                                         height => 0.0,
                                          level => $level,
                                           plot => 'my plot',
                                          space => [[-0.2,-0.2,0.0],[0.2,0.2,$height -0.37]],
                                       location => [map {$chain[$corner_id]->{coor}->[$_]} (0, 1)],
                                       rotation => $rotation});

                push(@molior_all, $molior->Serialise);
            }
            # intermediate columns
            for my $corner_id (0 .. scalar(@chain) -2)
            {
                my $rotation = rad2deg(angle_2d($chain[$corner_id]->{coor}, $chain[$corner_id +1]->{coor}));
                my $distance = distance_2d($chain[$corner_id]->{coor}, $chain[$corner_id +1]->{coor});
                my $segments = int ($distance / 1.7);
                next if $segments < 2;
                for my $segment_id (1 .. $segments -1)
                {
                    my $inc = $segment_id / $segments;
                    my $location = add_2d(scale_2d($inc, $chain[$corner_id]->{coor}), scale_2d(1-$inc, $chain[$corner_id +1]->{coor}));
                    my $molior = Molior->new ({type => 'molior-insert', guid => 'my building', name => 'beam column',
                                              style => $style,
                                          elevation => $elevation,
                                             height => 0.0,
                                              level => $level,
                                               plot => 'my plot',
                                              space => [[-0.2,-0.2,0.0],[0.2,0.2,$height -0.37]],
                                           location => $location,
                                           rotation => $rotation});

                    push(@molior_all, $molior->Serialise);
                }
            }
        }

        while ($walls_open->{$elevation}->{$height}->has_a_cycle)
        {
            my @chain = $walls_open->{$elevation}->{$height}->find_a_cycle;
            $walls_open->{$elevation}->{$height}->delete_vertices (@chain);

            my $molior = Molior->new ({type => 'molior-extrusion', guid => 'my building', name => 'external beam',
                                  elevation => $elevation,
                                       path => [map {[$_->{coor}->[0],$_->{coor}->[1]]} @chain],
                                     height => $height,
                                     closed => 1,
                                      level => $level,
                                       plot => 'my plot',
                                      style => $style});

            push(@molior_all, $molior->Serialise);

            # corner columns
            for my $corner_id (-1 .. scalar(@chain) -2)
            {
                my $rotation = rad2deg (angle_2d($chain[$corner_id]->{coor}, $chain[$corner_id +1]->{coor}));
                my $molior = Molior->new ({type => 'molior-insert', guid => 'my building', name => 'beam column',
                                          style => $style,
                                      elevation => $elevation,
                                         height => 0.0,
                                          level => $level,
                                           plot => 'my plot',
                                          space => [[-0.2,-0.2,0.0],[0.2,0.2,$height -0.37]],
                                       location => [map {$chain[$corner_id]->{coor}->[$_]} (0, 1)],
                                       rotation => $rotation});

                push(@molior_all, $molior->Serialise);
            }
            # intermediate columns
            for my $corner_id (-1 .. scalar(@chain) -2)
            {
                my $rotation = rad2deg(angle_2d($chain[$corner_id]->{coor}, $chain[$corner_id +1]->{coor}));
                my $distance = distance_2d($chain[$corner_id]->{coor}, $chain[$corner_id +1]->{coor});
                my $segments = int ($distance / 1.7);
                next if $segments < 2;
                for my $segment_id (1 .. $segments -1)
                {
                    my $inc = $segment_id / $segments;
                    my $location = add_2d(scale_2d($inc, $chain[$corner_id]->{coor}), scale_2d(1-$inc, $chain[$corner_id +1]->{coor}));
                    my $molior = Molior->new ({type => 'molior-insert', guid => 'my building', name => 'beam column',
                                              style => $style,
                                          elevation => $elevation,
                                             height => 0.0,
                                              level => $level,
                                               plot => 'my plot',
                                              space => [[-0.2,-0.2,0.0],[0.2,0.2,$height -0.37]],
                                           location => $location,
                                           rotation => $rotation});

                    push(@molior_all, $molior->Serialise);
                }
            }
        }
    }

    if (defined $walls_external_unsupported->{$elevation})
    {
        for my $source_vertex ($walls_external_unsupported->{$elevation}->source_vertices)
        {
            my @chain = $walls_external_unsupported->{$elevation}->_chain($source_vertex);

            my $molior = Molior->new ({type => 'molior-extrusion', guid => 'my building', name => 'ground beam',
                                  elevation => $elevation,
                                       path => [map {[$_->{coor}->[0],$_->{coor}->[1]]} @chain],
                                     height => 0.0,
                                     closed => 0,
                                      level => $level,
                                       plot => 'my plot',
                                      style => $style});

            push(@molior_all, $molior->Serialise);
        }
        while ($walls_external_unsupported->{$elevation}->has_a_cycle)
        {
            my @chain = $walls_external_unsupported->{$elevation}->find_a_cycle;
            $walls_external_unsupported->{$elevation}->delete_vertices (@chain);

            my $molior = Molior->new ({type => 'molior-extrusion', guid => 'my building', name => 'ground beam',
                                  elevation => $elevation,
                                       path => [map {[$_->{coor}->[0],$_->{coor}->[1]]} @chain],
                                     height => 0.0,
                                     closed => 1,
                                      level => $level,
                                       plot => 'my plot',
                                      style => $style});

            push(@molior_all, $molior->Serialise);
        }
    }

    if (defined $walls_internal_unsupported->{$elevation})
    {
        for my $edge ($walls_internal_unsupported->{$elevation}->edges)
        {
            my @chain = @{$edge};

            my $molior = Molior->new ({type => 'molior-extrusion', guid => 'my building', name => 'ground beam',
                                  elevation => $elevation,
                                       path => [map {[$_->{coor}->[0],$_->{coor}->[1]]} @chain],
                                     height => 0.0,
                                     closed => 0,
                                      level => $level,
                                       plot => 'my plot',
                                      style => $style});

            push(@molior_all, $molior->Serialise);
        }
    }

    my $id = 0;
    for my $cell (@{$cells_internal->{$elevation}})
    {
        my @chain = reverse $cell->perimeter;

        my $colour = 0;
        if ($cell->{type} =~ /^[lktbcr]/xi)
        {
            my $crinkliness = $cell->crinkliness;
            # red 10, green 90, blue 170, red 250
            # 0.5 red, 1.0 green, 1.5 blue
            $colour = (int($crinkliness*16)-7)*10;
            $colour = 170 if $colour > 170;
            $colour = 10 if $colour < 10;
        }

        my $molior = Molior->new ({type => 'molior-space', guid => 'my building', name => 'my room',
                                     id => $id,
                              elevation => $elevation,
                                   path => [map {[$_->{coor}->[0],$_->{coor}->[1]]} @chain],
                                 height => $cell->height,
                                ceiling => 0.2,
                                  floor => 0.02,
                                  inner => 0.08,
                                 closed => 1,
                                  level => $level,
                                   plot => 'my plot',
                                  style => $style,
                                 colour => $colour,
                                  usage => $cell->{type}});
        push(@molior_all, $molior->Serialise);

           $molior = Molior->new ({type => 'molior-floor', guid => 'my building', name => 'my room',
                                     id => $id,
                              elevation => $elevation,
                                   path => [map {[$_->{coor}->[0],$_->{coor}->[1]]} @chain],
                                  floor => 0.02,
                                  inner => 0.08,
                                 closed => 1,
                                  level => $level,
                                   plot => 'my plot',
                                  style => $style});
        push(@molior_all, $molior->Serialise) unless ($cell->{type} =~ /^c.*_s/xi and scalar $cell->cells_below);

           $molior = Molior->new ({type => 'molior-ceiling', guid => 'my building', name => 'my room',
                                     id => $id,
                              elevation => $elevation,
                                   path => [map {[$_->{coor}->[0],$_->{coor}->[1]]} @chain],
                                 height => $cell->height,
                                ceiling => 0.2,
                                  inner => 0.08,
                                 closed => 1,
                                  level => $level,
                                   plot => 'my plot',
                                  style => $style});
        push(@molior_all, $molior->Serialise) unless ($cell->{type} =~ /^c.*_s/xi and scalar $cell->cells_above);

           $molior = Molior->new ({type => 'molior-stair', guid => 'my building', id => $id,
                                   name => $id,
                              elevation => $elevation,
                                   path => [map {[$_->{coor}->[0],$_->{coor}->[1]]} @chain],
                                 height => $cell->height,
                                ceiling => 0.2,
                                  floor => 0.02,
                                  inner => 0.08,
                                 closed => 1,
                                  level => $level,
                                   plot => 'my plot',
                                  style => $style,
                                 risers => int($cell->height/0.19)+1,
                                  going => 0.25,
                                  width => 1.0,
                         corners_in_use => []});
        push(@molior_all, $molior->Serialise) if ($cell->{type} =~ /^c.*_s/xi and scalar @chain == 4 and scalar($cell->cells_above));

        $id++;
    }
}


YAML::DumpFile ($ARGV[1], @molior_all);

sub Graph::_chain
{
    my ($g, @list) = @_;
    my ($successor) = $g->successors ($list[-1]);
    return @list if (defined $successor and grep {/$successor/x} @list);
    @list = $g->_chain (@list, $successor) if defined $successor;
    return @list;
}

0;
