# DLP Projector screen for the SLA (stereolitography) print process

package Slic3r::GUI::Projector;
use strict;
use warnings;
use File::Basename qw(basename dirname);
use Wx qw(:dialog :id :misc :sizer :systemsettings :bitmap :button :icon :filedialog wxTheApp);
use Wx::Event qw(EVT_BUTTON EVT_CLOSE EVT_TEXT_ENTER EVT_SPINCTRL EVT_SLIDER);
use base qw(Wx::Dialog Class::Accessor);
use utf8;

__PACKAGE__->mk_accessors(qw(config config2 manual_control_config screen controller _optgroups));

sub new {
    my ($class, $parent) = @_;
    my $self = $class->SUPER::new($parent, -1, "Projector for DLP", wxDefaultPosition, wxDefaultSize);
    $self->config2({
        display                 => 0,
        show_bed                => 1,
        invert_y                => 0,
        zoom                    => 100,
        exposure_time           => 2,
        bottom_exposure_time    => 7,
        settle_time             => 1.5,
        bottom_layers           => 3,
        z_lift                  => 5,
        z_lift_speed            => 8,
        offset                  => [0,0],
    });
    $self->manual_control_config({
        xy_travel_speed         => 130,
        z_travel_speed          => 10,
        temperature             => '',
        bed_temperature         => '',
    });
    
    my $ini = eval { Slic3r::Config->read_ini("$Slic3r::GUI::datadir/DLP.ini") };
    if ($ini) {
        foreach my $opt_id (keys %{$ini->{_}}) {
            my $value = $ini->{_}{$opt_id};
            if ($opt_id eq 'offset') {
                $value = [ split /,/, $value ];
            }
            $self->config2->{$opt_id} = $value;
        }
    }
    
    my $sizer = Wx::BoxSizer->new(wxVERTICAL);
    
    $self->config(Slic3r::Config->new_from_defaults(
        qw(serial_port serial_speed bed_shape start_gcode end_gcode z_offset)
    ));
    $self->config->apply(wxTheApp->{mainframe}->{slaconfig});
    
    my @optgroups = ();
    {
        push @optgroups, my $optgroup = Slic3r::GUI::ConfigOptionsGroup->new(
            parent      => $self,
            title       => 'USB/Serial connection',
            config      => $self->config,
            label_width => 200,
        );
        $sizer->Add($optgroup->sizer, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
        
        {
            my $line = Slic3r::GUI::OptionsGroup::Line->new(
                label => 'Serial port',
            );
            my $serial_port = $optgroup->get_option('serial_port');
            $serial_port->side_widget(sub {
                my ($parent) = @_;
            
                my $btn = Wx::BitmapButton->new($self, -1, Wx::Bitmap->new($Slic3r::var->("arrow_rotate_clockwise.png"), wxBITMAP_TYPE_PNG),
                    wxDefaultPosition, wxDefaultSize, &Wx::wxBORDER_NONE);
                $btn->SetToolTipString("Rescan serial ports")
                    if $btn->can('SetToolTipString');
                EVT_BUTTON($self, $btn, sub {
                    $optgroup->get_field('serial_port')->set_values([ wxTheApp->scan_serial_ports ]);
                });
            
                return $btn;
            });
            $line->append_option($serial_port);
            $line->append_option($optgroup->get_option('serial_speed'));
            $line->append_button("Test", "wrench.png", sub {
                my $sender = Slic3r::GCode::Sender->new;
                my $res = $sender->connect(
                    $self->{config}->serial_port,
                    $self->{config}->serial_speed,
                );
                if ($res && $sender->wait_connected) {
                    Slic3r::GUI::show_info($self, "Connection to printer works correctly.", "Success!");
                } else {
                    Slic3r::GUI::show_error($self, "Connection failed.");
                }
            }, \$self->{serial_test_btn});
            $optgroup->append_line($line);
        }
    }
    
    {
        push @optgroups, my $optgroup = Slic3r::GUI::ConfigOptionsGroup->new(
            parent      => $self,
            title       => 'G-code',
            config      => $self->config,
            label_width => 200,
        );
        $sizer->Add($optgroup->sizer, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
        
        {
            my $option = $optgroup->get_option('start_gcode');
            $option->height(50);
            $option->full_width(1);
            $optgroup->append_single_option_line($option);
        }
        {
            my $option = $optgroup->get_option('end_gcode');
            $option->height(50);
            $option->full_width(1);
            $optgroup->append_single_option_line($option);
        }
    }
    
    my $on_change = sub {
        my ($opt_id, $value) = @_;
        
        $self->config2->{$opt_id} = $value;
        $self->screen->reposition;
        $self->show_print_time;
        
        my $serialized = {};
        foreach my $opt_id (keys %{$self->config2}) {
            my $value = $self->config2->{$opt_id};
            if (ref($value) eq 'ARRAY') {
                $value = join ',', @$value;
            }
            $serialized->{$opt_id} = $value;
        }
        Slic3r::Config->write_ini(
            "$Slic3r::GUI::datadir/DLP.ini",
            { _ => $serialized });
    };
    
    {
        push @optgroups, my $optgroup = Slic3r::GUI::OptionsGroup->new(
            parent      => $self,
            title       => 'Projection',
            on_change   => $on_change,
            label_width => 200,
        );
        $sizer->Add($optgroup->sizer, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
        
        {
            my $line = Slic3r::GUI::OptionsGroup::Line->new(
                label => 'Display',
            );
        
            my @displays = 0 .. (Wx::Display::GetCount()-1);
            $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
                opt_id      => 'display',
                type        => 'select',
                label       => 'Display',
                tooltip     => '',
                labels      => [@displays],
                values      => [@displays],
                default     => $self->config2->{display},
            ));
            $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
                opt_id      => 'zoom',
                type        => 'percent',
                label       => 'Zoom %',
                tooltip     => '',
                default     => $self->config2->{zoom},
                min         => 0.1,
                max         => 100,
            ));
            $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
                opt_id      => 'offset',
                type        => 'point',
                label       => 'Offset',
                tooltip     => '',
                default     => $self->config2->{offset},
            ));
            $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
                opt_id      => 'invert_y',
                type        => 'bool',
                label       => 'Invert Y',
                tooltip     => '',
                default     => $self->config2->{invert_y},
            ));
            $optgroup->append_line($line);
        }
        
        $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
            opt_id      => 'show_bed',
            type        => 'bool',
            label       => 'Show bed',
            tooltip     => '',
            default     => $self->config2->{show_bed},
        ));
    }
    
    {
        push @optgroups, my $optgroup = Slic3r::GUI::OptionsGroup->new(
            parent      => $self,
            title       => 'Print',
            on_change   => $on_change,
            label_width => 200,
        );
        $sizer->Add($optgroup->sizer, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
        
        {
            my $line = Slic3r::GUI::OptionsGroup::Line->new(
                label => 'Time (seconds)',
            );
            $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
                opt_id      => 'bottom_exposure_time',
                type        => 'f',
                label       => 'Bottom exposure',
                tooltip     => '',
                default     => $self->config2->{bottom_exposure_time},
            ));
            $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
                opt_id      => 'exposure_time',
                type        => 'f',
                label       => 'Exposure',
                tooltip     => '',
                default     => $self->config2->{exposure_time},
            ));
            $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
                opt_id      => 'settle_time',
                type        => 'f',
                label       => 'Settle',
                tooltip     => '',
                default     => $self->config2->{settle_time},
            ));
            $optgroup->append_line($line);
        }
        
        $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
            opt_id      => 'bottom_layers',
            type        => 'i',
            label       => 'Bottom layers',
            tooltip     => '',
            default     => $self->config2->{bottom_layers},
        ));
        
        {
            my $line = Slic3r::GUI::OptionsGroup::Line->new(
                label => 'Z Lift',
            );
            $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
                opt_id      => 'z_lift',
                type        => 'f',
                label       => 'Distance',
                sidetext    => 'mm',
                tooltip     => '',
                default     => $self->config2->{z_lift},
            ));
            $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
                opt_id      => 'z_lift_speed',
                type        => 'f',
                label       => 'Speed',
                sidetext    => 'mm/s',
                tooltip     => '',
                default     => $self->config2->{z_lift_speed},
            ));
            $optgroup->append_line($line);
        }
    }
    
    $self->_optgroups([@optgroups]);
    
    {
        my $sizer1 = Wx::BoxSizer->new(wxHORIZONTAL);
        $sizer->Add($sizer1, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
        
        {
            my $btn = $self->{btn_manual_control} = Wx::Button->new($self, -1, 'Manual Control', wxDefaultPosition, wxDefaultSize);
            if ($Slic3r::GUI::have_button_icons) {
                $btn->SetBitmap(Wx::Bitmap->new($Slic3r::var->("cog.png"), wxBITMAP_TYPE_PNG));
            }
            $sizer1->Add($btn, 0);
            EVT_BUTTON($self, $btn, sub {
                my $sender = Slic3r::GCode::Sender->new;
                my $res = $sender->connect(
                    $self->config->serial_port,
                    $self->config->serial_speed,
                );
                if (!$res || !$sender->wait_connected) {
                    Slic3r::GUI::show_error(undef, "Connection failed. Check serial port and speed.");
                    return;
                }
                my $dlg = Slic3r::GUI::Controller::ManualControlDialog->new
                    ($self, $self->config, $sender, $self->manual_control_config);
                $dlg->ShowModal;
                $sender->disconnect;
            });
            
            
        }
        {
            my $btn = $self->{btn_print} = Wx::Button->new($self, -1, 'Print', wxDefaultPosition, wxDefaultSize);
            if ($Slic3r::GUI::have_button_icons) {
                $btn->SetBitmap(Wx::Bitmap->new($Slic3r::var->("control_play.png"), wxBITMAP_TYPE_PNG));
            }
            $sizer1->Add($btn, 0);
            EVT_BUTTON($self, $btn, sub {
                $self->controller->start_print;
                $self->_update_buttons;
                $self->_set_status('');
            });
        }
        {
            my $btn = $self->{btn_stop} = Wx::Button->new($self, -1, 'Stop/Black', wxDefaultPosition, wxDefaultSize);
            if ($Slic3r::GUI::have_button_icons) {
                $btn->SetBitmap(Wx::Bitmap->new($Slic3r::var->("control_stop.png"), wxBITMAP_TYPE_PNG));
            }
            $sizer1->Add($btn, 0);
            EVT_BUTTON($self, $btn, sub {
                $self->controller->stop_print;
                $self->_update_buttons;
                $self->_set_status('');
            });
        }
        
        {
            {
                my $text = Wx::StaticText->new($self, -1, "Layer:", wxDefaultPosition, wxDefaultSize);
                $text->SetFont($Slic3r::GUI::small_font);
                $sizer1->Add($text, 0, wxEXPAND | wxLEFT, 10);
            }
            {
                my $spin = $self->{layers_spinctrl} = Wx::SpinCtrl->new($self, -1, 0, wxDefaultPosition, [60,-1],
                    0, 0, 300, 0);
                $sizer1->Add($spin, 0);
                EVT_SPINCTRL($self, $spin, sub {
                    my $value = $spin->GetValue;
                    $self->{layers_slider}->SetValue($value);
                    $self->controller->project_layer($value);
                    $self->_update_buttons;
                });
            }
            {
                my $slider = $self->{layers_slider} = Wx::Slider->new(
                    $self, -1,
                    0,           # default
                    0,           # min
                    300,         # max
                    wxDefaultPosition,
                    wxDefaultSize,
                );
                $sizer1->Add($slider, 1);
                EVT_SLIDER($self, $slider, sub {
                    my $value = $slider->GetValue;
                    $self->{layers_spinctrl}->SetValue($value);
                    $self->controller->project_layer($value);
                    $self->_update_buttons;
                });
            }
        }
        
        my $sizer2 = Wx::BoxSizer->new(wxHORIZONTAL);
        $sizer->Add($sizer2, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
        
        {
            $self->{status_text} = Wx::StaticText->new($self, -1, "", wxDefaultPosition, wxDefaultSize);
            $self->{status_text}->SetFont($Slic3r::GUI::small_font);
            $sizer2->Add($self->{status_text}, 1 | wxEXPAND);
        }
    }
    
    {
        # should be wxCLOSE but it crashes on Linux, maybe it's a Wx bug
        my $buttons = Wx::BoxSizer->new(wxHORIZONTAL);
        {
            my $btn = Wx::Button->new($self, -1, "Export SVG…");
            EVT_BUTTON($self, $btn, sub {
                $self->_export_svg;
            });
            $buttons->Add($btn, 0);
        }
        $buttons->AddStretchSpacer(1);
        {
            my $btn = Wx::Button->new($self, -1, "Close");
            $btn->SetDefault;
            EVT_BUTTON($self, $btn, sub {
                $self->_close;
            });
            $buttons->Add($btn, 0);
        }
        $sizer->Add($buttons, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
    }
    EVT_CLOSE($self, sub {
        $self->_close;
    });
    
    $self->SetSizer($sizer);
    $sizer->SetSizeHints($self);
    
    # reuse existing screen if any
    if ($Slic3r::GUI::DLP_projection_screen) {
        $self->screen($Slic3r::GUI::DLP_projection_screen);
        $self->screen->config($self->config);
        $self->screen->config2($self->config2);
    } else {
        $self->screen(Slic3r::GUI::Projector::Screen->new($parent, $self->config, $self->config2));
        $Slic3r::GUI::DLP_projection_screen = $self->screen;
    }
    $self->screen->reposition;
    $self->screen->Show;
    wxTheApp->{mainframe}->Hide;
    
    # initialize controller
    $self->controller(Slic3r::GUI::Projector::Controller->new(
        config  => $self->config,
        config2 => $self->config2,
        screen  => $self->screen,
        on_project_layer => sub {
            my ($layer_num) = @_;
            
            $self->{layers_spinctrl}->SetValue($layer_num);
            $self->{layers_slider}->SetValue($layer_num);
            
            my $duration = $self->controller->remaining_print_time;
            $self->_set_status(sprintf "Printing layer %d/%d (z = %.2f); %d minutes and %d seconds left",
                $layer_num, $self->controller->_print->layer_count,
                $self->controller->current_layer_height,
                int($duration/60), ($duration - int($duration/60)*60));  # % truncates to integer
        },
        on_print_completed => sub {
            $self->_update_buttons;
            $self->_set_status('');
            Wx::Bell();
        },
    ));
    {
        my $max = $self->controller->_print->layer_count-1;
        $self->{layers_spinctrl}->SetRange(0, $max); 
        $self->{layers_slider}->SetRange(0, $max);
    }
    
    $self->_update_buttons;
    $self->show_print_time;
    
    return $self;
}

sub _update_buttons {
    my ($self) = @_;
    
    my $is_printing = $self->controller->is_printing;
    my $is_projecting = $self->controller->is_projecting;
    $self->{btn_manual_control}->Show(!$is_printing);
    $self->{btn_print}->Show(!$is_printing && !$is_projecting);
    $self->{btn_stop}->Show($is_printing || $is_projecting);
    $self->{layers_spinctrl}->Enable(!$is_printing);
    $self->{layers_slider}->Enable(!$is_printing);
    if ($is_printing) {
        $_->disable for @{$self->_optgroups};
    } else {
        $_->enable for @{$self->_optgroups};
    }
    $self->Layout;
}

sub _export_svg {
    my ($self) = @_;
    
    my $output_file = 'print.svg';
    my $dlg = Wx::FileDialog->new(
        $self,
        'Save SVG file as:',
        wxTheApp->output_path(dirname($output_file)),
        basename($output_file),
        &Slic3r::GUI::FILE_WILDCARDS->{svg},
        wxFD_SAVE | wxFD_OVERWRITE_PROMPT,
    );
    if ($dlg->ShowModal != wxID_OK) {
        $dlg->Destroy;
        return;
    }
    $output_file = Slic3r::decode_path($dlg->GetPath);
    
    $self->controller->_print->write_svg($output_file);
}

sub _set_status {
    my ($self, $status) = @_;
    $self->{status_text}->SetLabel($status // '');
    $self->{status_text}->Wrap($self->{status_text}->GetSize->GetWidth);
    $self->{status_text}->Refresh;
    $self->Layout;
}

sub show_print_time {
    my ($self) = @_;
    
    
    my $duration = $self->controller->print_time;
    $self->_set_status(sprintf "Estimated print time: %d minutes and %d seconds - %.2f liters",
        int($duration/60), ($duration - int($duration/60)*60),  # % truncates to integer
        $self->controller->total_resin);
}

sub _close {
    my $self = shift;
    
    # if projection screen is not on the same display as our dialog,
    # ask the user whether they want to keep it open
    my $keep_screen = 0;
    my $display_area = Wx::Display->new($self->config2->{display})->GetGeometry;
    if (!$display_area->Contains($self->GetScreenPosition)) {
        my $res = Wx::MessageDialog->new($self, "Do you want to keep the black screen open?", 'Black screen', wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION)->ShowModal;
        $keep_screen = ($res == wxID_YES);
    }
    
    if ($keep_screen) {
        $self->screen->config(undef);
        $self->screen->config2(undef);
        $self->screen->Refresh;
    } else {
        $self->screen->Destroy;
        $self->screen(undef);
        $Slic3r::GUI::DLP_projection_screen = undef;
    }
    wxTheApp->{mainframe}->Show;
    
    $self->EndModal(wxID_OK);
}

package Slic3r::GUI::Projector::Controller;
use Moo;
use Wx qw(wxTheApp :id :timer);
use Wx::Event qw(EVT_TIMER);
use Slic3r::Geometry qw(unscale);
use Slic3r::Print::State ':steps';
use Time::HiRes qw(gettimeofday tv_interval);

has 'config'                => (is => 'ro', required => 1);
has 'config2'               => (is => 'ro', required => 1);
has 'screen'                => (is => 'ro', required => 1);
has 'on_project_layer'      => (is => 'rw');
has 'on_print_completed'    => (is => 'rw');
has 'sender'                => (is => 'rw');
has 'timer'                 => (is => 'rw');
has 'is_printing'           => (is => 'rw', default => sub { 0 });
has '_print'                => (is => 'rw');
has '_heights'              => (is => 'rw');
has '_layer_num'            => (is => 'rw');
has '_timer_cb'             => (is => 'rw');

sub BUILD {
    my ($self) = @_;
    
    Slic3r::GUI::disable_screensaver();
    
    # init print
    {
        my $print = Slic3r::SLAPrint->new(wxTheApp->{mainframe}->{plater}->{model});
        $print->apply_config(wxTheApp->{mainframe}->{plater}->config);
        $print->apply_config(wxTheApp->{mainframe}->{slaconfig});
        $self->_print($print);
        $self->screen->print($print);
    
        # make sure layers were sliced
        {
            my $progress_dialog = Wx::ProgressDialog->new('Slicing…', "Processing layers…", 100, undef, 0);
            $progress_dialog->Pulse;
            $print->slice;
            $progress_dialog->Destroy;
        }
        
        $self->_heights($print->heights);
    }
    
    # projection timer
    my $timer_id = &Wx::NewId();
    $self->timer(Wx::Timer->new($self->screen, $timer_id));
    EVT_TIMER($self->screen, $timer_id, sub {
        my $cb = $self->_timer_cb;
        $self->_timer_cb(undef);
        $cb->();
    });
}

sub delay {
    my ($self, $wait, $cb) = @_;
    
    $self->_timer_cb($cb);
    $self->timer->Start($wait * 1000, wxTIMER_ONE_SHOT);
}

sub current_layer_height {
    my ($self) = @_;
    
    return $self->_heights->[$self->_layer_num];
}

sub start_print {
    my ($self) = @_;
    
    {
        $self->sender(Slic3r::GCode::Sender->new);
        my $res = $self->sender->connect(
            $self->config->serial_port,
            $self->config->serial_speed,
        );
        if (!$res || !$self->sender->wait_connected) {
            Slic3r::GUI::show_error(undef, "Connection failed. Check serial port and speed.");
            return;
        }
        Slic3r::debugf "connected to " . $self->config->serial_port . "\n";
        
        # TODO: this wait should be handled by GCodeSender
        sleep 4;
        
        # send custom start G-code
        $self->sender->send($_, 1) for grep !/^;/, split /\n/, $self->config->start_gcode;
        $self->sender->("G90", 1); # set absolute positioning
    }
    
    $self->is_printing(1);
    
    # TODO: block until the G1 command has been performed
    # we could do this with M400 + M115 but maybe it's not portable
    $self->delay(5, sub {
        # start with black
        Slic3r::debugf "starting black projection\n";
        $self->_layer_num(-1);
        $self->screen->project_layer(undef);
        $self->delay($self->config2->{settle_time}, sub {
            $self->project_next_layer;
        });
    });
}

sub stop_print {
    my ($self) = @_;
    
    if ($self->sender) {
        $self->sender->disconnect;
    }
    
    $self->is_printing(0);
    $self->timer->Stop;
    $self->_timer_cb(undef);
    $self->screen->project_layer(undef);
}

sub print_completed {
    my ($self) = @_;
    
    # send custom end G-code
    if ($self->sender) {
        $self->sender->send($_, 1) for grep !/^;/, split /\n/, $self->config->end_gcode;
    }
    
    # call this before the on_print_completed callback otherwise buttons
    # won't be updated correctly
    $self->stop_print;
    
    $self->on_print_completed->()
        if $self->is_printing && $self->on_print_completed;
}

sub is_projecting {
    my ($self) = @_;
    
    return defined $self->screen->layer_num;
}

sub project_layer {
    my ($self, $layer_num) = @_;
    
    if (!defined $layer_num || $layer_num >= $self->_print->layer_count) {
        $self->screen->project_layer(undef);
        return;
    }
    
    $self->screen->project_layer($layer_num);
}

sub project_next_layer {
    my ($self) = @_;
    
    $self->_layer_num($self->_layer_num + 1);
    Slic3r::debugf "projecting layer %d\n", $self->_layer_num;
    if ($self->_layer_num >= $self->_print->layer_count) {
        $self->print_completed;
        return;
    }
    
    $self->on_project_layer->($self->_layer_num) if $self->on_project_layer;
    
    if ($self->sender) {
        my $z = $self->current_layer_height + $self->config->z_offset;
        my $F = $self->config2->{z_lift_speed} * 60;
        if ($self->config2->{z_lift} != 0) {
            $self->sender->send(sprintf("G1 Z%.5f F%d", $z + $self->config2->{z_lift}, $F), 1);
        }
        $self->sender->send(sprintf("G1 Z%.5f F%d", $z, $F), 1);
    }
    
    # TODO: we should block until G1 commands have been performed, see note below
    $self->delay($self->config2->{settle_time}, sub {
        $self->project_layer($self->_layer_num);
        
        # get exposure time
        my $time = $self->config2->{exposure_time};
        if ($self->_layer_num < $self->config2->{bottom_layers}) {
            $time = $self->config2->{bottom_exposure_time};
        }
        
        $self->delay($time, sub {
            $self->screen->project_layer(undef);
            $self->project_next_layer;
        });
    });
}

sub remaining_print_time {
    my ($self) = @_;
    
    my $remaining_layers = @{$self->_heights} - $self->_layer_num;
    my $remaining_bottom_layers = $self->_layer_num >= $self->config2->{bottom_layers}
        ? 0
        : $self->config2->{bottom_layers} - $self->_layer_num;
    
    return $remaining_bottom_layers * $self->config2->{bottom_exposure_time}
        + ($remaining_layers - $remaining_bottom_layers) * $self->config2->{exposure_time}
        + $remaining_layers * $self->config2->{settle_time};
}

sub print_time {
    my ($self) = @_;
    
    return $self->config2->{bottom_layers} * $self->config2->{bottom_exposure_time}
        + ($self->_print->layer_count - $self->config2->{bottom_layers}) * $self->config2->{exposure_time}
        + $self->_print->layer_count * $self->config2->{settle_time};
}

sub total_resin {
    my ($self) = @_;
    
    my $vol = 0;  # mm^3
    
    for my $i (0..($self->_print->layer_count-1)) {
        my $lh = $self->_heights->[$i] - ($i == 0 ? 0 : $self->_heights->[$i-1]);
        $vol += unscale(unscale($_->area)) * $lh for @{ $self->_print->layer_slices($i) };
    }
    
    return $vol/1000/1000;  # liters
}

sub DESTROY {
    my ($self) = @_;
    
    $self->timer->Stop if $self->timer;
    $self->sender->disconnect if $self->sender;
    Slic3r::GUI::enable_screensaver();
}

package Slic3r::GUI::Projector::Screen;
use Wx qw(:dialog :id :misc :sizer :colour :pen :brush :font wxBG_STYLE_CUSTOM);
use Wx::Event qw(EVT_PAINT EVT_SIZE);
use base qw(Wx::Dialog Class::Accessor);

use List::Util qw(min);
use Slic3r::Geometry qw(X Y unscale scale);
use Slic3r::Geometry::Clipper qw(intersection_pl union_ex);

__PACKAGE__->mk_accessors(qw(config config2 scaling_factor bed_origin print layer_num));

sub new {
    my ($class, $parent, $config, $config2) = @_;
    my $self = $class->SUPER::new($parent, -1, "Projector", wxDefaultPosition, wxDefaultSize, 0);
    
    $self->config($config);
    $self->config2($config2);
    $self->SetBackgroundStyle(wxBG_STYLE_CUSTOM);
    EVT_SIZE($self, \&_resize);
    EVT_PAINT($self, \&_repaint);
    $self->_resize;
    
    return $self;
}

sub reposition {
    my ($self) = @_;
    
    my $display = Wx::Display->new($self->config2->{display});
    my $area = $display->GetGeometry;
    $self->Move($area->GetPosition);
    # ShowFullScreen doesn't use the right screen
    #$self->ShowFullScreen($self->config2->{fullscreen});
    $self->SetSize($area->GetSize);
    $self->_resize;
    $self->Refresh;
}

sub _resize {
    my ($self) = @_;
    
    return if !$self->config;
    my ($cw, $ch) = $self->GetSizeWH;
    
    # get bed shape polygon
    my $bed_polygon = Slic3r::Polygon->new_scale(@{$self->config->bed_shape});
    my $bb = $bed_polygon->bounding_box;
    my $size = $bb->size;
    my $center = $bb->center;

    # calculate the scaling factor needed for constraining print bed area inside preview
    # scaling_factor is expressed in pixel / mm
    $self->scaling_factor(min($cw / unscale($size->x), $ch / unscale($size->y))); #)
    
    # apply zoom to scaling factor
    if ($self->config2->{zoom} != 0) {
        # TODO: make sure min and max in the option config are enforced
        $self->scaling_factor($self->scaling_factor * ($self->config2->{zoom}/100));
    }
    
    # calculate the displacement needed for centering bed on screen
    $self->bed_origin([
        $cw/2 - (unscale($center->x) - $self->config2->{offset}->[X]) * $self->scaling_factor,
        $ch/2 - (unscale($center->y) - $self->config2->{offset}->[Y]) * $self->scaling_factor,  #))
    ]);
    
    $self->Refresh;
}

sub project_layer {
    my ($self, $layer_num) = @_;
    
    $self->layer_num($layer_num);
    $self->Refresh;
}

sub _repaint {
    my ($self) = @_;
    
    my $dc = Wx::AutoBufferedPaintDC->new($self);
    my ($cw, $ch) = $self->GetSizeWH;
    return if $cw == 0;  # when canvas is not rendered yet, size is 0,0
    
    $dc->SetPen(Wx::Pen->new(wxBLACK, 1, wxSOLID));
    $dc->SetBrush(Wx::Brush->new(wxBLACK, wxSOLID));
    $dc->DrawRectangle(0, 0, $cw, $ch);
    
    return if !$self->config;
    
    # turn size into max visible coordinates
    # TODO: or should we use ClientArea?
    $cw--;
    $ch--;
    
    # draw bed
    if ($self->config2->{show_bed}) {
        $dc->SetPen(Wx::Pen->new(wxRED, 2, wxSOLID));
        $dc->SetBrush(Wx::Brush->new(wxWHITE, wxTRANSPARENT));
        
        # draw contour
        my $bed_polygon = Slic3r::Polygon->new_scale(@{$self->config->bed_shape});
        $dc->DrawPolygon($self->scaled_points_to_pixel($bed_polygon), 0, 0);
        
        # draw grid
        $dc->SetPen(Wx::Pen->new(wxRED, 1, wxSOLID));
        {
            my $bb = $bed_polygon->bounding_box;
            my $step = scale 10;  # 1cm grid
            my @polylines = ();
            for (my $x = $bb->x_min - ($bb->x_min % $step) + $step; $x < $bb->x_max; $x += $step) {
                push @polylines, Slic3r::Polyline->new([$x, $bb->y_min], [$x, $bb->y_max]);
            }
            for (my $y = $bb->y_min - ($bb->y_min % $step) + $step; $y < $bb->y_max; $y += $step) {
                push @polylines, Slic3r::Polyline->new([$bb->x_min, $y], [$bb->x_max, $y]);
            }
            $dc->DrawLine(map @$_, @$_)
                for map $self->scaled_points_to_pixel([ @$_[0,-1] ]),
                    @{intersection_pl(\@polylines, [$bed_polygon])};
        }
        
        # draw axes orientation
        $dc->SetPen(Wx::Pen->new(wxWHITE, 4, wxSOLID));
        {
            foreach my $endpoint ([10, 0], [0, 10]) {
                $dc->DrawLine(
                    map @{$self->unscaled_point_to_pixel($_)}, [0,0], $endpoint
                );
            }
            
            $dc->SetTextForeground(wxWHITE);
            $dc->SetFont(Wx::Font->new(20, wxDEFAULT, wxNORMAL, wxNORMAL));
            
            my $p = $self->unscaled_point_to_pixel([10, 0]);
            $dc->DrawText("X", $p->[X], $p->[Y]);
            $p = $self->unscaled_point_to_pixel([0, 10]);
            $dc->DrawText("Y", $p->[X]-20, $p->[Y]-10);
        }
    }
    
    # get layers at this height
    # draw layers
    $dc->SetPen(Wx::Pen->new(wxWHITE, 1, wxSOLID));
    
    return if !$self->print || !defined $self->layer_num;
    
    if ($self->print->layer_solid($self->layer_num)) {
        $self->_paint_expolygon($_, $dc) for @{$self->print->layer_slices($self->layer_num)};
    } else {
        # perimeters first, because their "hole" is painted black
        $self->_paint_expolygon($_, $dc) for
            @{$self->print->layer_perimeters($self->layer_num)},
            @{$self->print->layer_solid_infill($self->layer_num)};
        
        $self->_paint_expolygon($_, $dc)
            for @{union_ex($self->print->layer_infill($self->layer_num)->grow)};
    }
    
    # draw support material
    my $sm_radius = $self->print->config->get_abs_value_over('support_material_extrusion_width', $self->print->config->layer_height)/2;
    $dc->SetBrush(Wx::Brush->new(wxWHITE, wxSOLID));
    foreach my $pillar (@{$self->print->sm_pillars}) {
        next unless $pillar->{top_layer}    >= $self->layer_num
                 && $pillar->{bottom_layer} <= $self->layer_num;
        
        my $radius = min(
            $sm_radius,
            ($pillar->{top_layer} - $self->layer_num + 1) * $self->print->config->layer_height,
        );
        
        $dc->DrawCircle(
            @{$self->scaled_points_to_pixel([$pillar->{point}])->[0]},
            $radius * $self->scaling_factor,
        );
    }
}

sub _paint_expolygon {
    my ($self, $expolygon, $dc) = @_;
    
    my @polygons = sort { $a->contains_point($b->first_point) ? -1 : 1 } @$expolygon;
    $self->_paint_polygon($_, $dc) for @polygons;
}

sub _paint_polygon {
    my ($self, $polygon, $dc) = @_;
    
    if ($polygon->is_counter_clockwise) {
        $dc->SetBrush(Wx::Brush->new(wxWHITE, wxSOLID));
    } else {
        $dc->SetBrush(Wx::Brush->new(wxBLACK, wxSOLID));
    }
    $dc->DrawPolygon($self->scaled_points_to_pixel($polygon->pp), 0, 0);
}

# convert a model coordinate into a pixel coordinate
sub unscaled_point_to_pixel {
    my ($self, $point) = @_;
    
    my $zero = $self->bed_origin;
    my $p = [
        $point->[X] * $self->scaling_factor + $zero->[X],
        $point->[Y] * $self->scaling_factor + $zero->[Y],
    ];
    
    if (!$self->config2->{invert_y}) {
        my $ch = $self->GetSize->GetHeight;
        $p->[Y] = $ch - $p->[Y];
    }
    
    return $p;
}

sub scaled_points_to_pixel {
    my ($self, $points) = @_;
    
    return [
        map $self->unscaled_point_to_pixel($_),
            map Slic3r::Pointf->new_unscale(@$_),
            @$points
    ];
}

1;
