package Slic3r::ExPolygon;
use strict;
use warnings;

# an ExPolygon is a polygon with holes

use Math::Geometry::Voronoi;
use Slic3r::Geometry qw(X Y A B point_in_polygon);
use Slic3r::Geometry::Clipper qw(union_ex JT_MITER);

# the constructor accepts an array of polygons 
# or a Math::Clipper ExPolygon (hashref)
sub new {
    my $class = shift;
    my $self;
    if (@_ == 1 && ref $_[0] eq 'HASH') {
        $self = [
            Slic3r::Polygon->new($_[0]{outer}),
            map Slic3r::Polygon->new($_), @{$_[0]{holes}},
        ];
    } else {
        $self = [ map Slic3r::Polygon->new($_), @_ ];
    }
    bless $self, $class;
    $self;
}

sub clone {
    my $self = shift;
    return (ref $self)->new(map $_->clone, @$self);
}

sub contour {
    my $self = shift;
    return $self->[0];
}

sub holes {
    my $self = shift;
    return @$self[1..$#$self];
}

sub lines {
    my $self = shift;
    return map $_->lines, @$self;
}

sub clipper_expolygon {
    my $self = shift;
    return {
        outer => $self->contour,
        holes => [ $self->holes ],
    };
}

sub offset {
    my $self = shift;
    my ($distance, $scale, $joinType, $miterLimit) = @_;
    $scale      ||= $Slic3r::resolution * 1000000;
    $joinType   = JT_MITER if !defined $joinType;
    $miterLimit ||= 2;
    
    my $offsets = Math::Clipper::offset($self, $distance, $scale, $joinType, $miterLimit);
    return @$offsets;
}

sub safety_offset {
    my $self = shift;
    
    # we're offsetting contour and holes separately
    # because Clipper doesn't return polygons in the same order as 
    # we feed them to it
    
    return (ref $self)->new(
        $self->contour->safety_offset,
        @{ Slic3r::Geometry::Clipper::safety_offset([$self->holes]) },
    );
}

sub offset_ex {
    my $self = shift;
    my @offsets = $self->offset(@_);
    
    # apply holes to the right contours
    return @{ union_ex(\@offsets) };
}

sub encloses_point {
    my $self = shift;
    my ($point) = @_;
    return $self->contour->encloses_point($point)
        && (!grep($_->encloses_point($point), $self->holes)
            || grep($_->point_on_segment($point), $self->holes));
}

sub point_on_segment {
    my $self = shift;
    my ($point) = @_;
    for (@$self) {
        my $line = $_->point_on_segment($point);
        return $line if $line;
    }
    return undef;
}

sub bounding_box {
    my $self = shift;
    return Slic3r::Geometry::bounding_box($self->contour);
}

sub bounding_box_polygon {
    my $self = shift;
    my @bb = $self->bounding_box;
    return Slic3r::Polygon->new([
        [ $bb[0], $bb[1] ],
        [ $bb[2], $bb[1] ],
        [ $bb[2], $bb[3] ],
        [ $bb[0], $bb[3] ],
    ]);
}

sub clip_line {
    my $self = shift;
    my ($line) = @_;  # line must be a Slic3r::Line object
    
    my @intersections = grep $_, map $_->intersection($line, 1), map $_->lines, @$self;
    my @dir = (
        $line->[B][X] <=> $line->[A][X],
        $line->[B][Y] <=> $line->[A][Y],
    );
    
    @intersections = sort {
        (($a->[X] <=> $b->[X]) == $dir[X]) && (($a->[Y] <=> $b->[Y]) == $dir[Y]) ? 1 : -1
    } @intersections, @$line;
    
    shift @intersections if $intersections[0]->coincides_with($intersections[1]);
    pop @intersections if $intersections[-1]->coincides_with($intersections[-2]);
    
    shift @intersections
        if !$self->encloses_point($intersections[0])
        && !$self->point_on_segment($intersections[0]);
    
    my @lines = ();
    while (@intersections) {
        # skip tangent points
        my @points = splice @intersections, 0, 2;
        next if !$points[1];
        next if $points[0]->coincides_with($points[1]);
        push @lines, [ @points ];
    }
    return [@lines];
}

sub simplify {
    my $self = shift;
    $_->simplify(@_) for @$self;
}

sub translate {
    my $self = shift;
    $_->translate(@_) for @$self;
}

sub rotate {
    my $self = shift;
    $_->rotate(@_) for @$self;
}

sub area {
    my $self = shift;
    my $area = $self->contour->area;
    $area -= $_->area for $self->holes;
    return $area;
}

# this method only works for expolygons having only a contour or
# a contour and a hole, and not being thicker than the supplied 
# width. it returns a polyline or a polygon
sub medial_axis {
    my $self = shift;
    my ($width) = @_;
    
    my @self_lines = map $_->lines, @$self;
    my $expolygon = $self->clone;
    my @points = ();
    foreach my $polygon (@$expolygon) {
        Slic3r::Geometry::polyline_remove_short_segments($polygon, $width / 2);
        
        # subdivide polygon segments so that we don't have anyone of them
        # being longer than $width / 2
        $polygon->subdivide($width/2);
        
        push @points, @$polygon;
    }
    
    my $voronoi = Math::Geometry::Voronoi->new(points => \@points);
    $voronoi->compute;
    
    my @skeleton_lines = ();
    
    my $vertices = $voronoi->vertices;
    my $edges = $voronoi->edges;
    foreach my $edge (@$edges) {
        # ignore lines going to infinite
        next if $edge->[1] == -1 || $edge->[2] == -1;
        
        my ($a, $b);
        $a = $vertices->[$edge->[1]];
        $b = $vertices->[$edge->[2]];
        
        next if !$self->encloses_point($a) || !$self->encloses_point($b);
        
        push @skeleton_lines, [$edge->[1], $edge->[2]];
    }
    
    # remove leafs (lines not connected to other lines at one of their endpoints)
    {
        my %pointmap = ();
        $pointmap{$_}++ for map @$_, @skeleton_lines;
        @skeleton_lines = grep {
            $pointmap{$_->[A]} >= 2 && $pointmap{$_->[B]} >= 2
        } @skeleton_lines;
    }
    return undef if !@skeleton_lines;
    
    return undef if !@skeleton_lines;
    
    # now build a single polyline
    my $polyline = [];
    {
        my %pointmap = ();
        foreach my $line (@skeleton_lines) {
            foreach my $point_id (@$line) {
                $pointmap{$point_id} ||= [];
                push @{$pointmap{$point_id}}, $line;
            }
        }
        
        # start from a point having only one line
        foreach my $point_id (keys %pointmap) {
            if (@{$pointmap{$point_id}} == 1) {
                push @$polyline, grep $_ ne $point_id, map @$_, shift @{$pointmap{$point_id}};
                last;
            }
        }
        
        # if no such point is found, pick a random one
        push @$polyline, shift @{ +(values %pointmap)[0][0] } if !@$polyline;
        
        my %visited_lines = ();
        while (1) {
            my $last_point_id = $polyline->[-1];
            
            shift @{ $pointmap{$last_point_id} }
                while @{ $pointmap{$last_point_id} } && $visited_lines{$pointmap{$last_point_id}[0]};
            my $next_line = shift @{ $pointmap{$last_point_id} } or last;
            $visited_lines{$next_line} = 1;
            push @$polyline, grep $_ ne $last_point_id, @$next_line;
        }
    }
    
    # now replace point indexes with coordinates
    @$polyline = map $vertices->[$_], @$polyline;
    
    # cleanup
    Slic3r::Geometry::polyline_remove_short_segments($polyline, $width / 2);
    $polyline = Slic3r::Geometry::douglas_peucker($polyline, $width / 7);
    Slic3r::Geometry::polyline_remove_parallel_continuous_edges($polyline);
    
    if (Slic3r::Geometry::same_point($polyline->[0], $polyline->[-1])) {
        return undef if @$polyline == 2;
        return Slic3r::Polygon->new(@$polyline[0..$#$polyline-1]);
    } else {
        return Slic3r::Polyline->new($polyline);
    }
}

1;