package Urb::Dom::Fitness;

use strict;
use warnings;
use 5.010;
use Urb::Misc::Sun;
use Urb::Math qw /gaussian/;

our $DEBUG = $ENV{DEBUG};

=head1 NAME

Urb::Dom::Fitness - Fitness assessor for Urb::Dom objects

=head1 SYNOPSIS

A class that can be used to calculate fitness for domestic spaces.

=head1 DESCRIPTION

The eventual intention is to make this a Fitness assessor for L<Algorithm::Evolutionary>

=head1 USAGE

=over

=item new

Create a new Urb::Dom::Fitness object:

  $fitness = new Urb::Dom::Fitness;

..or

  $fitness = new Urb::Dom::Fitness ($patterns_hashref, $costs_hashref);

=cut

sub new
{
    my $class = shift;
    $class = ref $class || $class;
    my $self = {};
    $self->{_conf} = shift || {};
    $self->{_cost} = shift || {};
    bless $self, $class;
    $self->{_sun} = Urb::Misc::Sun->new ($self->Conf ('latitude'));
    $self->initialize();
    return $self;
}

# default parameters, can be overridden by supplying a hashref to new() method
# all double values are a 'target' and a variation falloff sigma
our $CONF = { value_inside => 300.0,
         value_circulation => 50.0,
             value_outside => 100.0,
           value_supported => 300.0,
              storey_limit => 4,
                  latitude => 53.3814,
                plot_ratio => [2.00, 0.50],
             ratio_outside => [0.33, 0.15],
         ratio_circulation => [0.00, 0.20],
              ratio_toilet => [0.10, 0.06],
             ratio_kitchen => [0.15, 0.10],
              ratio_living => [0.30, 0.25],
             ratio_bedroom => [0.45, 0.15],
      ratio_public_outside => [0.25, 0.15],
     ratio_private_outside => [0.5, 10.0],
             uncrinkliness => [5.0/6, 1.1/3],
 uncrinkliness_circulation => [5.0/6, 1.1/3],
          size_circulation => [0.0, 14.0],
               size_inside => [16.0, 3.5],
              size_kitchen => [16.0, 4.0],
               size_living => [20.0, 6.0],
              size_bedroom => [12.0, 4.0],
               size_toilet => [7.0, 2.5],
        proportion_outside => [1.5, 50],
    proportion_circulation => [1.5, 0.5],
         proportion_toilet => [1.5, 0.5],
         proportion_inside => [1.5, 0.5],
             width_outside => [3.0, 0.3],
         width_circulation => [2.4, 0.2],
             width_kitchen => [4.0, 1.0],
              width_living => [4.0, 0.5],
             width_bedroom => [4.0, 1.0],
              width_toilet => [3.0, 0.7],
              width_inside => [4.0, 1.0],
      perpendicular_inside => 0.3,
     perpendicular_outside => 10.0,
    allow_sahn_circulation => 0,
   allow_bedroom_on_street => 0,
       evaluate_room_types => 1 };

our $FAIL_THRESHOLD = 0.1;

# default parameters, can be overridden by supplying a hashref to new() method
our $COST = {            plot => 10.0,
    outside_covered_supported => 210.0,
              outside_covered => 110.0,
            outside_supported => 110.0,
                      outside => 10.0,
                       inside => 200.0,
                interior_wall => 200.0/3,
                exterior_wall => 100.0,
                     boundary => 50.0/3,
                boundary_wall => 400.0/3};
               
=item Conf

Access parameters from the pattern configuration or use defaults

    my $plot_ratio = $fitness->Conf ('plot_ratio');

=item Cost

Access cost rates for floor areas and wall areas, or use defaults:

    my $interior_wall = $fitness->Cost ('interior_wall');

=cut

sub Conf
{
    my $self = shift;
    my $item = shift;
    return $self->{_conf}->{$item} if defined $self->{_conf}->{$item};
    return $CONF->{$item} if defined $CONF->{$item};
    debug ("Error: No config defined for $item");
    return 0;
}

sub Cost
{
    my $self = shift;
    my $item = shift;
    return $self->{_cost}->{$item} if defined $self->{_cost}->{$item};
    return $COST->{$item} if defined $COST->{$item};
    debug ("Error: No cost defined for $item");
    return 0;
}

=item initialize

The initialize method is unused until/if this gets ported to L<Algorithm::Evolutionary>

=cut

# FIXME test needed
sub initialize
{
    my $self = shift;
    $self->{_counter} = 0; 
    $self->{_cache} = {};
    return;
}

=item _apply apply

Calculate fitness for an Urb::Dom object:

  $score = $fitness->_apply ($dom);

apply() is an alias for _apply().

=cut

sub apply
{
    my ($self, $dom) = @_;
    return $self->_apply ($dom);
}

# FIXME test needed
sub _apply
{
    my $self = shift;
    my $dom = shift;
    $dom->Fail_Reset;
    $dom->Clean_Cache;
    #$dom->Merge_Divided;
    $self->{_occlusion}->{_walls} = [$dom->Walls];

    # some initial values for whole multilevel
    my $has_public_access_outside = 0;
    my $has_public_access_inside = 0;
    my $has_public_private = 0;
    my $has_living_access = 1;
    my $has_useless_outdoor_space = 0;
    my $public_length_all = 0.0;
    my $public_length_outside = 0.0;
    my $private_length_all = 0.0;
    my $private_length_outside = 0.0;

    my $value = 0.0;
    my $cost = $self->Cost ('plot') * $dom->Area;
    debug ('initial cost:', $cost);

    # loop to do some surgery to leafs before we start
    for my $level ($dom, $dom->Levels_Above)
    {
        for my $leaf ($level->Leafs)
        {
            # circulation through outdoor space
            $leaf->Type ('O') if (!$self->Conf ('allow_sahn_circulation') and $leaf->Type =~ /^s/xi);
        }
    }

    my @stair_fit;
    my $graph_base;
    my @graph_circ;
    for my $level ($dom, $dom->Levels_Above)
    {
        my $graph = $level->Graph (1.2);
        push @{$graph_base}, $graph;
        my $graph_clone = $graph->clone;
        my $level_id = scalar $level->Levels_Below;
        $dom->Fail ("$level_id inaccessible usable space") unless $level->Has_Circulation ($graph_clone);
        push @graph_circ, $graph_clone;
    }

    # calculation recurses all levels
    for my $level ($dom, $dom->Levels_Above)
    {
    my $level_id = scalar $level->Levels_Below;

    my $graph = $graph_base->[$level->Level];

    for my $leaf ($level->Leafs)
        { $cost += $self->cost_leaf ($leaf) }
    debug ('cost with leafs:', $cost);

    for my $edge ($graph->edges)
        { $cost += $self->cost_edge ($graph, $edge) }
    debug ('cost with edges:', $cost);

    for my $leaf ($level->Leafs)
        { $cost += $self->cost_edge_outside ($leaf) }
    debug ('cost with outside edges:', $cost);

    my $has_outdoor_space = 0;

    for my $leaf ($level->Leafs)
    {
        my $id = $leaf->Id;
        $dom->Fail ("$level_id/$id unsupported covered outside") if ($leaf->Is_Outside and $leaf->Is_Covered and $leaf->Level and not $leaf->Is_Supported);
        $dom->Fail ("$level_id/$id covered outside above ground") if ($leaf->Is_Outside and $leaf->Is_Covered and $leaf->Level);
        next unless $leaf->Is_Usable;
        my $quality = 1;
        my $rate;
        my $case = $leaf->Type;
           if ($case =~ /^[os]/xi and $leaf->Level == 0) { $rate = $self->Conf ('value_outside') }
        elsif ($case =~ /^[os]/xi) { $rate = $self->Conf ('value_supported') }
        elsif ($case =~ /^c/xi) { $rate = $self->Conf ('value_circulation') }
                          else { $rate = $self->Conf ('value_inside') }
        debug ('leaf level:', $leaf->Level, 'id:', $leaf->Id, 'type:',
                $leaf->Type, 'rate:', $rate, 'area:', $leaf->Area,
                'width:', $leaf->Length_Narrowest, 'proportion:', $leaf->Aspect) if $DEBUG;

        # 195 STAIRCASE VOLUME
        my $stair_fit = 0;
        if ($level_id == 0)
        {
            my @corners_in_use = $leaf->Stack_Corners_In_Use (@graph_circ);

            my $entrances = $level->Entrances($graph);
            # does this leaf have an entrance door?
            if (defined $entrances->{$leaf->Id})
            {
                my @entrance_corners;
                for (0 .. 3)
                {
                     # this will only catch external abcd boundaries
                     push (@entrance_corners, $_, $_+1) if ($leaf->Boundary_Id($_) eq $entrances->{$leaf->Id})
                }
                for my $entrance_corner (@entrance_corners)
                {
                     push (@corners_in_use, $entrance_corner) unless grep /$entrance_corner/, @corners_in_use;
                }
            }

            if (scalar @corners_in_use and $leaf->Is_Covered)
            {
                $stair_fit = $leaf->Stair_Fit ($leaf->Stair_Riser, $leaf->Stair_Width, @corners_in_use);
                push @stair_fit, $stair_fit;
                debug ('  stair fit:', $leaf->Level, 'id:', $leaf->Id, 'fit:', $stair_fit)
                    if $DEBUG;
            }
        }

        # 191 THE SHAPE OF INDOOR SPACE
        my $factor = $self->quality_perpendicular ($leaf);
        $dom->Fail ("$level_id/$id perpendicular") if $factor < $FAIL_THRESHOLD;
        $quality *= $factor;
        debug ('  quality perpendicular:', $factor);

        $factor = $self->quality_proportion ($leaf);
        $dom->Fail ("$level_id/$id proportion") if $factor < $FAIL_THRESHOLD;
        $quality *= $factor;
        debug ('  quality proportion:', $factor);

        $factor = $self->quality_size ($leaf);
        $dom->Fail ("$level_id/$id size") if $factor < $FAIL_THRESHOLD;
        $quality *= $factor;
        debug ('  quality size:', $factor);

        $factor = $self->quality_width ($leaf);
        $dom->Fail ("$level_id/$id width") if $factor < $FAIL_THRESHOLD;
        $quality *= $factor;
        debug ('  quality width:', $factor);

        # 159 LIGHT ON TWO SIDES OF EVERY ROOM
        $factor = $self->quality_uncrinkliness ($leaf, $graph, $self->{_occlusion});
        $dom->Fail ("$level_id/$id crinkliness") if $factor < $FAIL_THRESHOLD;
        $quality *= $factor;
        debug ('  quality crinkliness:', $factor);

        # 161 SUNNY PLACE **
        $factor = $self->quality_daylight ($leaf, $self->{_occlusion});
        $factor = 1 if $self->Conf('allow_sahn_circulation') and $leaf->Type =~ /^[s]/xi; # direct sunlight is not required in Sahn spaces
        $dom->Fail ("$level_id/$id daylight") if $factor < $FAIL_THRESHOLD;
        $quality *= $factor;
        debug ('  quality daylight:', $factor);

        $has_public_access_outside = 1 if $leaf->Public_Access_Outside ($graph);
        $has_public_access_inside = 1
            if (!$stair_fit and $leaf->Type =~ /^c/xi and $leaf->Public_Access);
        $has_public_private = 1 if $leaf->Type =~ /^b/xi and $leaf->Public_Access and $leaf->Level == 0;
        $has_living_access = 0 if $leaf->Type =~ /^k/xi and not $leaf->Access_Living ($graph);
        $has_outdoor_space = 1 if $leaf->Is_Outside;

        my $public_length = $leaf->Public_Length;
        $public_length_all += $public_length;
        $public_length_outside += $public_length if $leaf->Is_Outside;

        my $private_length = $leaf->Private_Length;
        $private_length_all += $private_length;
        $private_length_outside += $private_length if $leaf->Is_Outside;

        # is this connected to circulation space, or connected to anything usable if itself circulation
        my $count_access = scalar ($leaf->Access ($graph));
        if ($count_access > 0)
        {
            $factor = 1;
        }
        elsif (!$leaf->Level and $leaf->Is_Outside)
        {
            $factor = 1;
        }
        else
        {
            $factor = 0.01;
            $dom->Fail ("$level_id/$id access");
        }
        $quality *= $factor;
        debug ('  quality access:', $factor);

        $value += $quality * $rate * $leaf->Area;
    }

    # Returns true if all /^[cs]/ nodes are connected to each other
    # In the process $graph is stripped of all but /^[cs]/ nodes

    $dom->Fail ("level $level_id not connected") unless ($level->Connected_Circulation ($graph));

    # This rule requires every level to have outside space 118 ROOF GARDEN *
    $dom->Fail ("level $level_id no outside space") unless $has_outdoor_space;

    }

    # Ratios() does recurse through all levels
    my $ratios = $dom->Ratios;
    debug ('ratios:', map {$_ .':'. $ratios->{$_}} keys %{$ratios}) if $DEBUG;

    # 96 NUMBER OF STORIES - 50% of usable floorspace should be outdoor/garden
    # note that 0.33 && plot ratio 2 indicates amount of outdoor spce is equal to plot size
    my $factor = ratio_o ($ratios, @{$self->Conf ('ratio_outside')});
    $value *= $factor;
    debug ('ratio_o:', $factor);

    $factor = ratio_type ($ratios, 'c', @{$self->Conf ('ratio_circulation')});
    $value *= $factor;
    debug ('ratio_c:', $factor);

    if ($self->Conf ('evaluate_room_types'))
    {
    $factor = ratio_type ($ratios, 't', @{$self->Conf ('ratio_toilet')});
    $value *= $factor;
    debug ('ratio_t:', $factor);

    $factor = ratio_type ($ratios, 'k', @{$self->Conf ('ratio_kitchen')});
    $value *= $factor;
    debug ('ratio_k:', $factor);

    $factor = ratio_type ($ratios, 'l', @{$self->Conf ('ratio_living')});
    $value *= $factor;
    debug ('ratio_l:', $factor);

    $factor = ratio_type ($ratios, 'b', @{$self->Conf ('ratio_bedroom')});
    $value *= $factor;
    debug ('ratio_b:', $factor);
    }

    # 21 FOUR STORY LIMIT
    $dom->Fail ('storey limit') if scalar ($dom->Levels_Above) >= $self->Conf ('storey_limit');

    # 195 STAIRCASE VOLUME
    if (scalar ($dom->Levels_Above))
    {
        $factor = $self->quality_staircase_volume (@stair_fit);
        $dom->Fail ('staircase volume') if $factor < $FAIL_THRESHOLD;
        $value *= $factor;
    }

    if (scalar @stair_fit > 1)
    {
        $dom->Fail ('more than one stair');
    }

    # Count() does recurse through all levels
    my $count = $dom->Count;
    if ($self->Conf ('evaluate_room_types'))
    {
        $dom->Fail ('no b') if ((!defined $count->{b}) or scalar @{$count->{b}} < 1);
        $dom->Fail ('no l') if ((!defined $count->{l}) or scalar @{$count->{l}} < 1);
        $dom->Fail ('no k') if ((!defined $count->{k}) or scalar @{$count->{k}} < 1);
        $dom->Fail ('no t') if ((!defined $count->{t}) or scalar @{$count->{t}} < 1);
        $dom->Fail ('too many k') if (defined $count->{k} and scalar @{$count->{k}} > 1);
    }

    $dom->Fail ('no outside') if ((!defined $count->{o} or scalar @{$count->{o}} < 1)
                              and (!defined $count->{s} or scalar @{$count->{s}} < 1));
    $dom->Fail ('all outside') if ((defined $count->{o} and scalar keys %{$count} == 1)
                                or (defined $count->{s} and scalar keys %{$count} == 1)
                                or (defined $count->{o} and defined $count->{s} and scalar keys %{$count} == 2));

    # only trigger these once
    # 112 ENTRANCE TRANSITION
    # transition can be an single outdoor node connected to a [klc] or a [c] that isn't a stair
    $dom->Fail ('no outside public access')
        unless ($has_public_access_outside or $has_public_access_inside);

    if ($self->Conf ('evaluate_room_types'))
    {
        $dom->Fail ('useless outdoor space') if $has_useless_outdoor_space;
        $dom->Fail ('bedroom on street') if ($has_public_private and !$self->Conf ('allow_bedroom_on_street'));
        $dom->Fail ('kitchen not next to living') unless $has_living_access;
    }

    my $public_ratio = 0.0;
    $public_ratio = $public_length_outside / $public_length_all if $public_length_all;
    $factor = gaussian ($public_ratio, 1.0, @{$self->Conf ('ratio_public_outside')});
    $value *= $factor;
    debug ('ratio_public_outside:', $factor);

    my $private_ratio = 0.0;
    $private_ratio = $private_length_outside / $private_length_all if $private_length_all;
    $factor = gaussian ($private_ratio, 1.0, @{$self->Conf ('ratio_private_outside')});
    $value *= $factor;
    debug ('ratio_private_outside:', $factor);

    # 96 NUMBER OF STORIES - Plot ratio should be less than 2
    $factor = gaussian ($dom->Plot_Ratio, 1.0, @{$self->Conf ('plot_ratio')});
    $dom->Fail ('plot ratio') if $factor < $FAIL_THRESHOLD;
    $value *= $factor;
    debug ('plot ratio:', $factor);

    # final calculation after recursing through all levels
    $value *= 0.1**(scalar $dom->Failures);
    if ($DEBUG) {debug ('quality fail:', $_) for ($dom->Failures)};
    debug ('quality value:', $value);

    return $value / $cost;
}

=back

=head2 Methods for calculating general scores

=over

=item quality_staircase_volume

  $factor = $fitness->quality_staircase_volume (@stair_fit);

=back

=cut

sub uniq { my %seen; grep !$seen{$_}++, @_ }

sub quality_staircase_volume
{
    my ($self, @stair_fit) = @_;
    my $factor = 0.09;
    for my $stair_fit (@stair_fit)
    {
        my $factor_temp;
        if ($stair_fit < 1) # space is too small score tightly
        {
            $factor_temp = gaussian ($stair_fit, 1.2, 1.0, 0.1);
        }
        else # space is too big score loosely
        {
            $factor_temp = gaussian ($stair_fit, 1.2, 1.0, 0.5);
        }
        $factor = $factor_temp if $factor_temp > $factor;
    }
    return $factor;
}

=head2 Misc functions

These operate on the table output of the Ratios() method:

  my $ratios = $dom->Ratios;

=over

=item ratio_o

Score based on the proportion of outdoor space:

  my $score = ratio_o ($ratios, 0.2, 0.2);

Parameters are the preferred proportion and sigma variation.

=cut

sub ratio_o
{
    my ($ratios, $ratio, $sigma) = @_;
    my $proportion_o;

    my @out = grep {/^[os]/xi} keys %{$ratios};
    my $out = shift @out;
    if (defined $out) { $proportion_o = $ratios->{$out} }
    else { $proportion_o = 0 }

    return gaussian ($proportion_o, 1.0, $ratio, $sigma);
}

=item ratio_type

Score based on the proportion of a named type of non-outdoor space:

  my $score = ratio_type ($ratios, 'k', 0.1, 0.05);

Paraeters are the type, preferred proportion and sigma variation.

=cut

sub ratio_type
{
    my ($ratios, $type, $ratio, $sigma) = @_;
    my $proportion_type;
    my $proportion_non_o;

    my @key = grep {/^$type/xi} keys %{$ratios};
    my $key = shift @key;
    if (defined $key) { $proportion_type = $ratios->{$key} }
    else { $proportion_type = 0 }

    my @out = grep {/^[os]/xi} keys %{$ratios};
    my $out = shift @out;
    if (defined $out) { $proportion_non_o = 1 - $ratios->{$out} }
    else { $proportion_non_o = 1 }

    $proportion_non_o = 1 unless $proportion_non_o;
    return gaussian ($proportion_type / $proportion_non_o, 1.0, $ratio, $sigma);
}

=back

=head2 Methods for calculating quality of leaf nodes

=over

=item quality_perpendicular

  $factor = $fitness->quality_perpendicular ($leaf);

=cut

# FIXME test needed
sub quality_perpendicular
{
    my $self = shift;
    my $leaf = shift;
    # parameter is sigma variation in radians from pi/2
    my $case = $leaf->Type;
    if ($leaf->Is_Outside) { return $leaf->Perpendicular ($self->Conf ('perpendicular_outside')) }
                      else { return $leaf->Perpendicular ($self->Conf ('perpendicular_inside')) }
}

=item quality_uncrinkliness

  $factor = $fitness->quality_uncrinkliness ($leaf, $graph, $occlusion);

=cut

# FIXME test needed
sub quality_uncrinkliness
{
    my $self = shift;
    my $leaf = shift;
    my $graph = shift;
    my $occlusion = shift;
    # parameter is distance in metres and sigma variation
    my $case = $leaf->Type;
       if ($leaf->Is_Outside and (!$leaf->Is_Covered)) { return 1 }
    elsif ($leaf->Is_Circulation)
    { return $leaf->Uncrinkliness ($graph, $occlusion, @{$self->Conf ('uncrinkliness_circulation')}) }
    else { return $leaf->Uncrinkliness ($graph, $occlusion, @{$self->Conf ('uncrinkliness')}) }
}

=item quality_daylight

  $factor = $fitness->quality_daylight ($leaf, $occlusion);

=cut

sub quality_daylight
{
    my $self = shift;
    my $leaf = shift;
    my $occlusion = shift;
    # only do sunlight minutes calculation for outdoor spaces, indoor spaces use CIE overcast sky (for now)
    return 1 unless $leaf->Is_Outside;
=cut
    my $coor = [@{$leaf->Centroid},$leaf->Elevation];
    # range is 0 - 2.44346095279206
    my $illumination = $occlusion->CIEsky_horizontal ($coor);
    return $illumination / 2.44346095279206;
=cut
    my $minutes = 0;
    for my $wall (0 .. 3)
    {
        my $coor = $leaf->Middle ($wall);
        $minutes += $occlusion->Sun_horizontal ($self->{_sun}, $coor);
    }
    $minutes /= 4;
    return $minutes / 262980;
}

=item quality_proportion

  $factor = $fitness->quality_proportion ($leaf);

=cut

# FIXME test needed
sub quality_proportion
{
    my $self = shift;
    my $leaf = shift;
    # parameters are preferred aspect and sigma variation
    my $case = $leaf->Type;
       if ($case =~ /^[os]/xi) { return $leaf->Proportion (@{$self->Conf ('proportion_outside')}) }
    elsif ($case =~ /^c/xi) { return $leaf->Proportion (@{$self->Conf ('proportion_circulation')}) }
    elsif (!$self->Conf ('evaluate_room_types')) {return $leaf->Proportion (@{$self->Conf ('proportion_inside')}) }
    elsif ($case =~ /^t/xi) { return $leaf->Proportion (@{$self->Conf ('proportion_toilet')}) }
                      else { return $leaf->Proportion (@{$self->Conf ('proportion_inside')}) }
}

=item quality_size

  $factor = $fitness->quality_size ($leaf);

=cut

# FIXME test needed
sub quality_size
{
    my $self = shift;
    my $leaf = shift;
    # parameters are preferred area and sigma variation
    my $case = $leaf->Type;
       if ($case =~ /^[os]/xi) { return 1 }
    elsif ($case =~ /^c/xi) { return $leaf->Size (@{$self->Conf ('size_circulation')}) }
    elsif (!$self->Conf ('evaluate_room_types')) {return $leaf->Size (@{$self->Conf ('size_inside')}) }
    elsif ($case =~ /^k/xi) { return $leaf->Size (@{$self->Conf ('size_kitchen')}) }
    elsif ($case =~ /^l/xi) { return $leaf->Size (@{$self->Conf ('size_living')}) }
    elsif ($case =~ /^b/xi) { return $leaf->Size (@{$self->Conf ('size_bedroom')}) }
    elsif ($case =~ /^t/xi) { return $leaf->Size (@{$self->Conf ('size_toilet')}) }
                      else { return 1 }
}

=item quality_width

  $factor = $fitness->quality_width ($leaf);

=cut

# FIXME test needed
sub quality_width
{
    my $self = shift;
    my $leaf = shift;
    # parameters are preferred minimum width and sigma variation
    my $case = $leaf->Type;
       if ($case =~ /^[os]/xi and (!$leaf->Is_Covered) and (!$leaf->Is_Supported) and ($leaf->Level)) { return 1 }
    elsif ($case =~ /^[os]/xi) { return $leaf->Width (@{$self->Conf ('width_outside')}) }
    elsif ($case =~ /^c/xi) { return $leaf->Width (@{$self->Conf ('width_circulation')}) }
    elsif (!$self->Conf ('evaluate_room_types')) {return $leaf->Width (@{$self->Conf ('width_inside')}) }
    elsif ($case =~ /^k/xi) { return $leaf->Width (@{$self->Conf ('width_kitchen')}) }
    elsif ($case =~ /^l/xi) { return $leaf->Width (@{$self->Conf ('width_living')}) }
    elsif ($case =~ /^b/xi) { return $leaf->Width (@{$self->Conf ('width_bedroom')}) }
    elsif ($case =~ /^t/xi) { return $leaf->Width (@{$self->Conf ('width_toilet')}) }
                      else { return $leaf->Width (@{$self->Conf ('width_inside')}) }
}

=back

=head2 Methods for calculating cost of leaf nodes

=over

=item cost_leaf

  $cost = $fitness->cost_leaf ($leaf);

=cut

sub cost_leaf
{
    my $self = shift;
    my $leaf = shift;
    my $rate;
    if ($leaf->Is_Outside)
    {
           if ($leaf->Is_Covered and $leaf->Is_Supported)
                                  { $rate = $self->Cost ('outside_covered_supported') }
        elsif ($leaf->Is_Covered) { $rate = $self->Cost ('outside_covered') }
        elsif ($leaf->Is_Supported) { $rate = $self->Cost ('outside_supported') }
         else { $rate = $self->Cost ('outside') }
    }
    else { $rate = $self->Cost ('inside') }

    return $rate * $leaf->Area;
}

=item cost_edge

  $cost = $fitness->cost_edge ($leaf, $graph, $edge);

=cut

sub cost_edge
{
    my $self = shift;
    my $graph = shift;
    my $edge = shift;
    my $rate;
    my $height = $edge->[0]->Height;

    if ($edge->[0]->Is_Outside and $edge->[1]->Is_Outside)
    { $rate = 0.0 }

    elsif ((!$edge->[0]->Is_Outside) and (!$edge->[1]->Is_Outside))
    { $rate = $self->Cost ('interior_wall') }

    else
    { $rate = $self->Cost ('exterior_wall') }

    my $length = $graph->get_edge_attribute (@{$edge}, 'width');
    #$edge->[0]->Root->Lowest->Fail (scalar ($edge->[0]->Root->Levels_Below) .'/'. $edge->[0]->Id .' '. $edge->[1]->Id .' edge too short') if ($length < 1.25);
    $edge->[0]->Root->Lowest->Fail (scalar ($edge->[0]->Root->Levels_Below) .'/'. $edge->[0]->Id .' '. $edge->[1]->Id .' edge too long') if ($length > 8.0 and $rate > 0.0);
    return $rate * $length * $height;
}

=item cost_edge_outside

  $cost = $fitness->cost_edge_outside ($leaf);

=cut

sub cost_edge_outside
{
    my $self = shift;
    my $leaf = shift;
    my $rate;
    my $height = $leaf->Height;
    if ($leaf->Is_Outside) { $rate = $self->Cost ('boundary') }
                      else { $rate = $self->Cost ('boundary_wall') }

    my $length = 0.0;
    for my $index (0 .. 3)
    {
        if ($leaf->Boundary_Id ($index) =~ /^[abcd]$/x)
        {
            my $length_temp = $leaf->Length ($index);
            $length += $length_temp;
#            $leaf->Root->Lowest->Fail (scalar ($leaf->Root->Levels_Below) .'/'. $leaf->Id .' outside edge too short') if ($length_temp < 1.25);
            next if ($leaf->Is_Outside);
            $leaf->Root->Lowest->Fail (scalar ($leaf->Root->Levels_Below) .'/'. $leaf->Id .' outside edge too long') if ($length_temp > 8.0);
        }
    }

    return $rate * $length * $height;
}

=item debug

Any text sent to debug() gets printed to STDERR if $ENV{DEBUG} is set

=back

=cut

sub debug
{
    my @text = @_;
    return unless $DEBUG;
    say STDERR join ' ', @text;
    return 1;
}

1;
