# -*- coding: utf-8 -*-
"""
$Id$

Copyright 2011 Lars Kruse <devel@sumpfralle.de>

This file is part of PyCAM.

PyCAM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

PyCAM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with PyCAM.  If not, see <http://www.gnu.org/licenses/>.
"""

import math
import datetime
import gobject

import pycam.Plugins
import pycam.Gui.common
# this requires ODE - we import it later, if necessary
#import pycam.Simulation.ODEBlocks


class ToolpathSimulation(pycam.Plugins.PluginBase):

    UI_FILE = "toolpath_simulation.ui"
    DEPENDS = ["Toolpaths", "OpenGLViewToolpath"]
    CATEGORIES = ["Toolpath"]

    def setup(self):
        self._running = None
        if self.gui:
            self._gtk_handlers = []
            self._frame = self.gui.get_object("SimulationBox")
            self.core.register_ui("toolpath_handling", "Simulation",
                    self._frame, 25)
            self._speed_factor_widget = self.gui.get_object(
                    "SimulationSpeedFactorValue")
            self._speed_factor_widget.set_value(1.0)
            self._progress = self.gui.get_object(
                    "SimulationProgressTimelineValue")
            self._timer_widget = self.gui.get_object(
                    "SimulationProgressTimeDisplay")
            self.core.set("show_simulation", False)
            self._toolpath_moves = None
            self._start_button = self.gui.get_object("SimulationStartButton")
            self._pause_button = self.gui.get_object("SimulationPauseButton")
            self._stop_button = self.gui.get_object("SimulationStopButton")
            for obj, handler in ((self._start_button, self._start_simulation),
                    (self._pause_button, self._pause_simulation),
                    (self._stop_button, self._stop_simulation)):
                self._gtk_handlers.append((obj, "clicked", handler))
            self._gtk_handlers.append((self._progress, "value-changed",
                    self._update_toolpath))
            self._event_handlers = (
                    ("toolpath-selection-changed", self._update_visibility), )
            self.register_event_handlers(self._event_handlers)
            self.register_gtk_handlers(self._gtk_handlers)
            self._update_visibility()
            self.core.register_event("visualize-items", self.show_simulation)
        return True

    def teardown(self):
        if self.gui:
            del self.core["show_simulation"]
            self.core.unregister_ui("toolpath_handling", self._frame)
            self.core.unregister_event("visualize-items", self.show_simulation)
            self.unregister_event_handlers(self._event_handlers)
            self.unregister_gtk_handlers(self._gtk_handlers)

    def _update_visibility(self):
        toolpaths = self.core.get("toolpaths").get_selected()
        if toolpaths and (len(toolpaths) == 1):
            self._frame.show()
        else:
            self._frame.hide()

    def _start_simulation(self, widget=None):
        if self._running is None:
            # initial start of simulation (not just continuing)
            toolpaths = self.core.get("toolpaths")
            if not toolpaths:
                # this should not happen
                return
            # we use only one toolpath
            self._toolpath = toolpaths[0]
            # calculate steps
            self._safety_height = self.core.get("gcode_safety_height")
            self._progress.set_upper(self._toolpath.get_machine_time(
                    safety_height=self._safety_height))
            self._progress.set_value(0)
            self._distance = self._toolpath.get_machine_movement_distance(
                        safety_height=self._safety_height)
            self._feedrate = self._toolpath.get_params().get("tool_feedrate",
                    300)
            self._toolpath_moves = None
            self.core.set("show_simulation", True)
            self._running = True
            interval_ms = int(1000 / self.core.get("drill_progress_max_fps"))
            pycam.Gui.common.set_parent_controls_sensitivity(self._frame, False)
            gobject.timeout_add(interval_ms, self._next_timestep)
        else:
            self._running = True
        self._start_button.set_sensitive(False)
        self._pause_button.set_sensitive(True)
        self._stop_button.set_sensitive(True)

    def _pause_simulation(self, widget=None):
        self._start_button.set_sensitive(True)
        self._pause_button.set_sensitive(False)
        self._running = False

    def _stop_simulation(self, widget=None):
        self._running = None
        self.core.set("show_simulation", False)
        self._toolpath_moves = None
        self._timer_widget.set_label("")
        self._progress.set_value(0)
        self._start_button.set_sensitive(True)
        self._pause_button.set_sensitive(False)
        self._stop_button.set_sensitive(False)
        pycam.Gui.common.set_parent_controls_sensitivity(self._frame, True)
        self.core.emit_event("visual-item-updated")

    def _next_timestep(self):
        if self._running is None:
            # stop operation
            return False
        if not self._running:
            # pause -> no change
            return True
        if self._progress.get_value() < self._progress.get_upper():
            time_step = self._speed_factor_widget.get_value() / \
                    self.core.get("drill_progress_max_fps")
            new_time = self._progress.get_value() + time_step
            new_time = min(new_time, self._progress.get_upper())
            if new_time != self._progress.get_value():
                # update the visualization
                self._progress.set_value(new_time)
        return True

    def _update_toolpath(self, widget=None):
        if (not self._running is None) and (self._progress.get_upper() > 0):
            fraction = self._progress.get_value() / self._progress.get_upper()
            current = datetime.timedelta(
                    seconds=int(self._progress.get_value()))
            complete = datetime.timedelta(
                    seconds=int(self._progress.get_upper()))
            self._timer_widget.set_label("%s / %s" % (current, complete))
            self._toolpath_moves = self._toolpath.get_moves(
                    safety_height=self._safety_height,
                    max_movement=self._distance * fraction)
            self.core.emit_event("visual-item-updated")

    def show_simulation(self):
        if self._toolpath_moves and self.core.get("show_simulation"):
            self.core.get("draw_toolpath_moves_func")(self._toolpath_moves)

    def update_toolpath_simulation_ode(self, widget=None, toolpath=None):
        import pycam.Simulation.ODEBlocks as ODEBlocks
        # get the currently selected toolpath, if none is give
        if toolpath is None:
            toolpath_index = self._treeview_get_active_index(self.toolpath_table, self.toolpath)
            if toolpath_index is None:
                return
            else:
                toolpath = self.toolpath[toolpath_index]
        paths = toolpath.paths
        # set the current cutter
        self.cutter = pycam.Cutters.get_tool_from_settings(
                toolpath.get_tool_settings())
        # calculate steps
        detail_level = self.gui.get_object("SimulationDetailsValue").get_value()
        grid_size = 100 * pow(2, detail_level - 1)
        bounding_box = toolpath.get_toolpath_settings().get_bounds()
        (minx, miny, minz), (maxx, maxy, maxz) = bounding_box.get_bounds()
        # proportion = dimension_x / dimension_y
        proportion = (maxx - minx) / (maxy - miny)
        x_steps = int(sqrt(grid_size) * proportion)
        y_steps = int(sqrt(grid_size) / proportion)
        simulation_backend = ODEBlocks.ODEBlocks(toolpath.get_tool_settings(),
                toolpath.get_bounding_box(), x_steps=x_steps, y_steps=y_steps)
        self.core.set("simulation_object", simulation_backend)
        # disable the simulation widget (avoids confusion regarding "cancel")
        if not widget is None:
            self.gui.get_object("SimulationTab").set_sensitive(False)
        # update the view
        self.update_view()
        # calculate the simulation and show it simultaneously
        progress = self.core.get("progress")
        for path_index, path in enumerate(paths):
            progress_text = "Simulating path %d/%d" % (path_index, len(paths))
            progress_value_percent = 100.0 * path_index / len(paths)
            if progress.update(text=progress_text, percent=progress_value_percent):
                # break if the user pressed the "cancel" button
                break
            for index in range(len(path.points)):
                self.cutter.moveto(path.points[index])
                if index != 0:
                    start = path.points[index - 1]
                    end = path.points[index]
                    if start != end:
                        simulation_backend.process_cutter_movement(start, end)
                self.update_view()
                # break the loop if someone clicked the "cancel" button
                if progress.update():
                    break
            progress.finish()
        # enable the simulation widget again (if we were started from the GUI)
        if not widget is None:
            self.gui.get_object("SimulationTab").set_sensitive(True)