#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
$Id$

Copyright 2010 Lars Kruse <devel@sumpfralle.de>
Copyright 2008-2009 Lode Leroy

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/>.
"""

from optparse import OptionParser
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
from pycam.Physics.ode_physics import override_ode_availability
import pycam.Gui.common as GuiCommon
import pycam.Gui.Settings
import pycam.Importers.STLImporter
import pycam.Importers.TestModel
import pycam.Exporters.SimpleGCodeExporter
import pycam.Toolpath.Generator
import pycam.Toolpath
from pycam import VERSION
import pycam.Utils.log

log = pycam.Utils.log.get_logger()

EXAMPLE_MODEL_LOCATIONS = (
        os.path.join(os.path.dirname(__file__), "samples"),
        os.path.join(sys.prefix, "share", "pycam", "samples"),
        os.path.join("usr", "share", "pycam", "samples"))
DEFAULT_MODEL_FILE = "pycam.stl"


def show_gui(inputfile=None, task_settings_file=None):
    deps_gtk = GuiCommon.requirements_details_gtk()
    report_gtk = GuiCommon.get_dependency_report(deps_gtk, prefix="\t")
    if GuiCommon.check_dependencies(deps_gtk):
        from pycam.Gui.Project import ProjectGui
        gui_class = ProjectGui
    else:
        full_report = []
        full_report.append("PyCAM dependency problem")
        full_report.append("Error: Failed to load the GTK interface.")
        full_report.append("Details:")
        full_report.append(report_gtk)
        full_report.append("")
        full_report.append("Detailed list of requirements: %s" % GuiCommon.REQUIREMENTS_LINK)
        log.critical(os.linesep.join(full_report))
        sys.exit(1)

    gui = gui_class()

    # load the given model or the default
    if inputfile is None:
        default_model = get_default_model()
        if isinstance(default_model, basestring):
            gui.load_model_file(filename=default_model)
        else:
            gui.load_model(default_model)
    else:
        gui.load_model_file(filename=inputfile)

    # load task settings file
    if not task_settings_file is None:
        gui.open_task_settings_file(task_settings_file)

    # open the GUI
    gui.mainloop()


def get_default_model():
    """ return a filename or a Model instance """
    # try to load the default model file ("pycam" logo)
    for inputdir in EXAMPLE_MODEL_LOCATIONS:
        inputfile = os.path.join(inputdir, DEFAULT_MODEL_FILE)
        if os.path.isfile(inputfile):
            return inputfile
    else:
        # fall back to the simple test model
        log.warn("Failed to find the default model (%s) in the " \
                "following locations: %s" % (DEFAULT_MODEL_FILE,
                        ", ".join(EXAMPLE_MODEL_LOCATIONS)))
        return pycam.Importers.TestModel.get_test_model()


# check if we were started as a separate program
if __name__ == "__main__":
    parser = OptionParser( prog="PyCAM", version="PyCAM v%s" % VERSION,
            usage="usage: %prog [options] [inputfile]\n\n" \
                    + "Use any of the '--export-???' parameters to disable " \
                    + "the graphical user interface (GUI).\n" \
                    + "When starting the GUI (default) all arguments except " \
                    + "'inputfile' are ignored.\n" \
                    + "'inputfile' may be an STL or DXF file.",
            epilog="Use any of the '--export-???' parameters to enable " \
                    + "non-interactive mode. Otherwise all options (except " \
                    + "'inputfile') are silently ignored and the GUI is " \
                    + "started.")
    group_general = parser.add_option_group("General options")
    group_export = parser.add_option_group("Export formats",
            "Export the resulting toolpath or meta-data in various formats. " \
            + "This option triggers the non-interactive mode. Thus the GUI " \
            + "is disabled.")
    group_tool = parser.add_option_group("Tool definition",
            "Specify the tool paramters. The default tool is spherical and " \
            + "has a diameter of 1mm. The default speeds are 1000 (feedrate) " \
            + "and 250 (drill spindle rotations per minute)")
    group_process = parser.add_option_group("Process definition",
            "Specify the process parameters: toolpath strategy, layer height," \
            + " and others. A typical roughing operation is configured by " \
            + "default.")
    group_bounds = parser.add_option_group("Boundary definition",
            "Specify the outer limits of the processing area (x/y/z). " \
            + "You may choose between 'relative_margin' (margin is given as " \
            + "percentage of the respective model dimension), 'fixed_margin' " \
            + "(margin for each face given in absolute units of length) " \
            + "and 'custom' (absolute coordinates of the bounding box - " \
            + "regardless of the model size and position). Negative values " \
            + "are allowed and can make sense (e.g. negative margin).")
    group_support_grid = parser.add_option_group("Support grid",
            "An optional support grid can be used to keep the object in " \
            + "place during the mill operation. The support grid can be " \
            + "removed manually afterwards. The support grid can have a " \
            + "rectangular profile. By default the support grid is disabled.")
    group_general.add_option("-c", "--config", dest="config_file",
            default=None, action="store", type="string",
            help="load a task settings file")
    group_general.add_option("", "--unit", dest="unit_size",
            default="mm", action="store", type="choice",
            choices=["mm", "inch"], help="choose 'mm' or 'inch' for all " \
            + "numbers. By default 'mm' is assumed.")
    group_general.add_option("", "--collision-engine", dest="collision_engine",
            default="triangles", action="store", type="choice",
            choices=["triangles", "ode", "help"],
            help="choose a specific collision detection engine. The default " \
                    + "is 'triangles'. Use 'help' to get a list of possible " \
                    + "engines.")
    group_general.add_option("", "--boundary-mode", dest="boundary_mode",
            default="along", action="store", type="choice",
            choices=["inside", "along", "outside"],
            help="specify if the mill tool (including its radius) should " \
                    + "move completely 'inside', 'along' or 'outside' the " \
                    + "defined processing boundary.")
    group_general.add_option("", "--disable-psyco", dest="disable_psyco",
            default=False, action="store_true", help="disable the Psyco " \
                    + "just-in-time-compiler even when it is available")
    group_export.add_option("", "--export-gcode", dest="export_gcode",
            default=None, action="store", type="string",
            help="export the generated toolpaths to a file")
    group_export.add_option("", "--export-task-config",
            dest="export_task_config", default=None, action="store",
            type="string",
            help="export the current task configuration (mainly for debugging)")
    group_tool.add_option("", "--tool-shape", dest="tool_shape",
            default="cylindrical", action="store", type="choice",
            choices=["cylindrical", "spherical", "toroidal"],
            help="tool shape for the operation (cylindrical, spherical, " \
            + "toroidal)")
    group_tool.add_option("", "--tool-size", dest="tool_diameter",
            default=1.0, action="store", type="float",
            help="diameter of the tool")
    group_tool.add_option("", "--tool-torus-size", dest="tool_torus_diameter",
            default=0.25, action="store", type="float",
            help="torus diameter of the tool (only for toroidal tool shape)")
    group_tool.add_option("", "--tool-feedrate", dest="tool_feedrate",
            default=1000, action="store", type="float",
            help="allowed movement velocity of the tool (units/minute)")
    group_tool.add_option("", "--tool-speed", dest="tool_speed",
            default=250, action="store", type="float",
            help="rotation speed of the tool (per minute)")
    group_tool.add_option("", "--tool-id", dest="tool_id",
            default=1, action="store", type="int",
            help="tool ID - to be used for tool selection via GCode " \
            + "(default: 1)")
    group_process.add_option("", "--process-path-direction",
            dest="process_path_direction", default="x", action="store",
            type="choice", choices=["x", "y", "xy"],
            help="primary direction of the generated toolpath (x/y/xy)")
    group_process.add_option("", "--process-path-generator",
            dest="process_path_generator", default="layer", action="store",
            type="choice", choices=["layer", "surface", "engrave"],
            help="one of the available toolpath strategies (layer, surface, " \
            + "engrave)")
    group_process.add_option("", "--process-path-postprocessor",
            dest="process_path_postprocessor", default="polygon",
            action="store", type="choice",
            choices=["polygon", "contour", "zigzag"], help="one of the " \
            + "available toolpath postprocessors (polygon, zigzag, contour)")
    group_process.add_option("", "--process-material-allowance",
            dest="process_material_allowance", default=0.0, action="store",
            type="float", help="minimum distance between the tool and the " \
            + "object (for rough processing)")
    group_process.add_option("", "--process-step-down",
            dest="process_step_down", default=3.0, action="store", type="float",
            help="the maximum thickness of each processed material layer " \
            + "(only for 'layer' strategy)")
    group_process.add_option("", "--process-overlap-percent",
            dest="process_overlap_percent", default=0, action="store",
            type="int", help="how much should two adjacent parallel " \
            + "toolpaths overlap each other (0..99)")
    group_process.add_option("", "--safety-height", dest="safety_height",
            default=0.0, action="store", type="float",
            help="height for safe re-positioning moves")
    group_process.add_option("", "--process-engrave-offset",
            dest="process_engrave_offset", default=0.0, action="store",
            type="float", help="engrave along the contour of a model with a " \
            + "given distance (only for 'engrave' strategy)")
    group_bounds.add_option("", "--bounds-type", dest="bounds_type",
            default="relative-margin", action="store", type="choice",
            choices=["relative-margin", "fixed-margin", "custom"],
            help="type of the boundary definition (relative-margin, " \
            + "fixed-margin, custom)")
    group_bounds.add_option("", "--bounds-lower", dest="bounds_lower",
            default="", action="store", type="string",
            help="comma-separated x/y/z combination of the lower boundary " \
            + "(e.g. '4,4,-0.5')")
    group_bounds.add_option("", "--bounds-upper", dest="bounds_upper",
            default="", action="store", type="string",
            help="comma-separated x/y/z combination of the upper boundary " \
            + "(e.g. '12,5.5,0')")
    group_support_grid.add_option("", "--enable-support-grid",
            dest="support_grid_enabled", default=False, action="store_true",
            help="enable the support grid")
    group_support_grid.add_option("", "--support-grid-distance",
            dest="support_grid_distance", default=10.0, action="store",
            type="float", help="horizontal and vertical distance between two " \
                    + "adjacent parallel lines of the support grid pattern")
    group_support_grid.add_option("", "--support-grid-height",
            dest="support_grid_height", default=2.0, action="store",
            type="float", help="height of the support grid profile")
    group_support_grid.add_option("", "--support-grid-thickness",
            dest="support_grid_thickness", default=0.5, action="store",
            type="float", help="width of the support grid profile")
    (opts, args) = parser.parse_args()

    if len(args) > 0:
        inputfile = args[0]
    else:
        inputfile = None

    if not opts.disable_psyco:
        try:
            import psyco
            psyco.full()
            log.info("Psyco enabled")
        except ImportError:
            log.info("Psyco is not available (performance will probably " \
                    + "suffer slightly)")
    else:
        log.info("Psyco was disabled via the commandline")

    if not opts.export_gcode and not opts.export_task_config:
        show_gui(inputfile, opts.config_file)
    else:
        # generate toolpath
        tps = pycam.Gui.Settings.ToolpathSettings()
        tool_shape = {"cylindrical": "CylindricalCutter",
                "spherical": "SphericalCutter",
                "toroidal": "ToroidalCutter",
            }[opts.tool_shape]
        tps.set_tool(opts.tool_id, tool_shape, 0.5 * opts.tool_diameter,
                0.5 * opts.tool_torus_diameter, opts.tool_speed,
                opts.tool_feedrate)
        if opts.support_grid_enabled:
            tps.set_support_grid(opts.support_grid_distance,
                    opts.support_grid_thickness, opts.support_grid_height)
        if opts.collision_engine == "ode":
            tps.set_calculation_backend("ODE")
        tps.set_unit_size(opts.unit_size)
        path_generator = {"layer": "PushCutter",
                "surface": "DropCutter",
                "engrave": "EngraveCutter",
            }[opts.process_path_generator]
        postprocessor = {"zigzag": "ZigZagCutter",
                "contour": "ContourCutter",
                "polygon": "PolygonCutter",
            }[opts.process_path_postprocessor]
        tps.set_process_settings(path_generator, postprocessor,
                opts.process_path_direction, opts.process_material_allowance,
                opts.safety_height, opts.process_overlap_percent / 100.0,
                opts.process_step_down, opts.process_engrave_offset)
        model = get_model(inputfile)
        # calculate the processing boundary
        bounds = pycam.Toolpath.Bounds()
        # set the bounds type and let the default bounding box match the model
        if opts.bounds_type == "relative-margin":
            bounds.set_type(pycam.Toolpath.Bounds.TYPE_RELATIVE_MARGIN)
            bounds_default_low = (10, 10, 0)
            bounds_default_high = (10, 10, 0)
        elif opts.bounds_type == "fixed-margin":
            bounds.set_type(pycam.Toolpath.Bounds.TYPE_FIXED_MARGIN)
            bounds_default_low = (10, 10, 0)
            bounds_default_high = (10, 10, 0)
        else:
            # custom boundary setting
            bounds.set_type(pycam.Toolpath.Bounds.TYPE_CUSTOM)
            bounds_default_low = (model.minx, model.miny, model.minz)
            bounds_default_high = (model.maxx, model.maxy, model.maxz)
        # TODO: use the optparse conversion callback instead
        def parse_triple_float(text):
            nums = text.split(",")
            if len(nums) != 3:
                return None
            result = []
            for num in nums:
                try:
                    result.append(float(num))
                except ValueError:
                    return None
            return result
        bounds_lower_nums = parse_triple_float(opts.bounds_lower)
        bounds_upper_nums = parse_triple_float(opts.bounds_upper)
        if bounds_lower_nums is None \
                and bounds_upper_nums is None:
            # no bound was given 
            bounds.set_bounds(bounds_default_low, bounds_default_high)
        elif bounds_lower_nums is None:
            # only the upper bound was specified
            bounds.set_bounds(bounds_default_low, bounds_upper_nums)
        elif bounds_upper_nums is None:
            # only the lower bound was specified
            bounds.set_bounds(bounds_lower_nums, bounds_default_high)
        else:
            # both lower and upper bounds were specified
            bounds.set_bounds(bounds_lower_nums, bounds_upper_nums)
        # adjust the bounding box according to the "boundary_mode"
        if opts.boundary_mode == "along":
            offset = (0, 0, 0)
        elif opts.boundary_mode == "inside":
            offset = (-0.5 * opts.tool_diameter, -0.5 * opts.tool_diameter, 0)
        else:
            # "outside"
            offset = (0.5 * opts.tool_diameter, 0.5 * opts.tool_diameter, 0)
        process_bounds = Bounds(Bounds.TYPE_FIXED_MARGIN, offset, offset)
        process_bounds.set_reference(bounds)
        tps.set_bounds(process_bounds)
        if opts.export_gcode:
            # generate the toolpath
            tp = pycam.Toolpath.Generator.generate_toolpath_from_settings(model, tps)
            # write result
            if isinstance(tp, basestring):
                # an error occoured
                log.error(tp)
            else:
                start_pos = tp.get_start_position()
                meta_data = os.linesep.join(tp.get_meta_data())
                if opts.export_gcode == "-":
                    destination = sys.stdout
                    close_destination = False
                else:
                    destination = open(opts.export_gcode, "w")
                    close_destination = True
                pycam.Exporters.SimpleGCodeExporter.ExportPathList(destination,
                        tp.get_path(), opts.unit_size, start_pos.x,
                        start_pos.y, start_pos.z, opts.tool_feedrate,
                        opts.tool_speed, safety_height=opts.safety_height,
                        max_skip_safety_distance=opts.tool_diameter,
                        comment=meta_data)
                if close_destination:
                    destination.close()
        if opts.export_task_config:
            raise NotImplementedError("Export of task config is not " \
                    + "implemented, yet!")