#!/usr/bin/env python

# This file is part of the Printrun suite.
#
# Printrun 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.
#
# Printrun 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 Printrun.  If not, see <http://www.gnu.org/licenses/>.

import os

# Set up Internationalization using gettext
# searching for installed locales on /usr/share; uses relative folder if not found (windows)
from printrun.printrun_utils import install_locale
install_locale('pronterface')

import wx
import time
import threading
import math
import sys
import re
import traceback
import subprocess
from copy import copy

from printrun import stltool
from printrun.objectplater import Plater

glview = False
if "-nogl" not in sys.argv:
    try:
        from printrun import stlview
        glview = True
    except:
        print "Could not load 3D viewer for plater:"
        traceback.print_exc()


def evalme(s):
    return eval(s[s.find("(") + 1:s.find(")")])


class showstl(wx.Window):
    def __init__(self, parent, size, pos):
        wx.Window.__init__(self, parent, size = size, pos = pos)
        self.i = 0
        self.parent = parent
        self.previ = 0
        self.Bind(wx.EVT_MOUSEWHEEL, self.rot)
        self.Bind(wx.EVT_MOUSE_EVENTS, self.move)
        self.Bind(wx.EVT_PAINT, self.repaint)
        self.Bind(wx.EVT_KEY_DOWN, self.keypress)
        self.triggered = 0
        self.initpos = None
        self.prevsel = -1

    def drawmodel(self, m, scale):
        m.bitmap = wx.EmptyBitmap(800, 800, 32)
        dc = wx.MemoryDC()
        dc.SelectObject(m.bitmap)
        dc.SetBackground(wx.Brush((0, 0, 0, 0)))
        dc.SetBrush(wx.Brush((0, 0, 0, 255)))
        dc.SetBrush(wx.Brush(wx.Colour(128, 255, 128)))
        dc.SetPen(wx.Pen(wx.Colour(128, 128, 128)))
        for i in m.facets:
            dc.DrawPolygon([wx.Point(400 + scale * p[0], (400 - scale * p[1])) for p in i[1]])
        dc.SelectObject(wx.NullBitmap)
        m.bitmap.SetMask(wx.Mask(m.bitmap, wx.Colour(0, 0, 0, 255)))

    def move_shape(self, delta):
        """moves shape (selected in l, which is list ListBox of shapes)
        by an offset specified in tuple delta.
        Positive numbers move to (rigt, down)"""
        name = self.parent.l.GetSelection()
        if name == wx.NOT_FOUND:
            return False
        name = self.parent.l.GetString(name)
        model = self.parent.models[name]
        model.offsets = [model.offsets[0] + delta[0],
                         model.offsets[1] + delta[1],
                         model.offsets[2]
                         ]
        self.Refresh()
        return True

    def move(self, event):
        if event.ButtonUp(wx.MOUSE_BTN_LEFT):
            if(self.initpos is not None):
                currentpos = event.GetPositionTuple()
                delta = (0.5 * (currentpos[0] - self.initpos[0]),
                         -0.5 * (currentpos[1] - self.initpos[1])
                         )
                self.move_shape(delta)
                self.Refresh()
                self.initpos = None
        elif event.ButtonDown(wx.MOUSE_BTN_RIGHT):
            self.parent.right(event)
        elif event.Dragging():
            if self.initpos is None:
                self.initpos = event.GetPositionTuple()
            self.Refresh()
            dc = wx.ClientDC(self)
            p = event.GetPositionTuple()
            dc.DrawLine(self.initpos[0], self.initpos[1], p[0], p[1])
            del dc
        else:
            event.Skip()

    def rotate_shape(self, angle):
        """rotates acive shape
        positive angle is clockwise
        """
        self.i += angle
        if not self.triggered:
            self.triggered = 1
            threading.Thread(target = self.cr).start()

    def keypress(self, event):
        """gets keypress events and moves/rotates acive shape"""
        keycode = event.GetKeyCode()
        #print keycode
        step = 5
        angle = 18
        if event.ControlDown():
            step = 1
            angle = 1
        #h
        if keycode == 72:
            self.move_shape((-step, 0))
        #l
        if keycode == 76:
            self.move_shape((step, 0))
        #j
        if keycode == 75:
            self.move_shape((0, step))
        #k
        if keycode == 74:
            self.move_shape((0, -step))
        #[
        if keycode == 91:
            self.rotate_shape(-angle)
        #]
        if keycode == 93:
            self.rotate_shape(angle)
        event.Skip()

    def rotateafter(self):
        if self.i != self.previ:
            i = self.parent.l.GetSelection()
            if i != wx.NOT_FOUND:
                self.parent.models[self.parent.l.GetString(i)].rot -= 5 * (self.i - self.previ)
            self.previ = self.i
            self.Refresh()

    def cr(self):
        time.sleep(0.01)
        wx.CallAfter(self.rotateafter)
        self.triggered = 0

    def rot(self, event):
        z = event.GetWheelRotation()
        s = self.parent.l.GetSelection()
        if self.prevsel != s:
            self.i = 0
            self.prevsel = s
        if z < 0:
            self.rotate_shape(-1)
        else:
            self.rotate_shape(1)

    def repaint(self, event):
        dc = wx.PaintDC(self)
        self.paint(dc = dc)

    def paint(self, coord1 = "x", coord2 = "y", dc = None):
        if dc is None:
            dc = wx.ClientDC(self)
        scale = 2
        dc.SetPen(wx.Pen(wx.Colour(100, 100, 100)))
        for i in xrange(20):
            dc.DrawLine(0, i * scale * 10, 400, i * scale * 10)
            dc.DrawLine(i * scale * 10, 0, i * scale * 10, 400)
        dc.SetPen(wx.Pen(wx.Colour(0, 0, 0)))
        for i in xrange(4):
            dc.DrawLine(0, i * scale * 50, 400, i * scale * 50)
            dc.DrawLine(i * scale * 50, 0, i * scale * 50, 400)
        dc.SetBrush(wx.Brush(wx.Colour(128, 255, 128)))
        dc.SetPen(wx.Pen(wx.Colour(128, 128, 128)))
        dcs = wx.MemoryDC()
        for m in self.parent.models.values():
            b = m.bitmap
            im = b.ConvertToImage()
            imgc = wx.Point(im.GetWidth() / 2, im.GetHeight() / 2)
            im = im.Rotate(math.radians(m.rot), imgc, 0)
            bm = wx.BitmapFromImage(im)
            dcs.SelectObject(bm)
            bsz = bm.GetSize()
            dc.Blit(scale * m.offsets[0] - bsz[0] / 2, 400 - (scale * m.offsets[1] + bsz[1] / 2), bsz[0], bsz[1], dcs, 0, 0, useMask = 1)
        del dc

class StlPlater(Plater):

    load_wildcard = _("STL files (*.stl;*.STL)|*.stl;*.STL|OpenSCAD files (*.scad)|*.scad")
    save_wildcard = _("STL files (*.stl;*.STL)|*.stl;*.STL")

    def __init__(self, filenames = [], size = (800, 580), callback = None,
                 parent = None, build_dimensions = None, simarrange_path = None):
        super(StlPlater, self).__init__(filenames, size, callback, parent, build_dimensions)
        if glview:
            viewer = stlview.StlViewPanel(self, (580, 580), build_dimensions = self.build_dimensions)
        else:
            viewer = showstl(self, (580, 580), (0, 0))
        self.simarrange_path = simarrange_path if simarrange_path else "./simarrange/sa"
        self.set_viewer(viewer)

    def done(self, event, cb):
        try:
            os.mkdir("tempstl")
        except:
            pass
        name = "tempstl/" + str(int(time.time()) % 10000) + ".stl"
        self.export_to(name)
        if cb is not None:
            cb(name)
        self.Destroy()

    def load_file(self, filename):
        if filename.lower().endswith(".stl"):
            try:
                self.load_stl(filename)
            except:
                dlg = wx.MessageDialog(self, _("Loading STL file failed"), _("Error"),wx.OK)
                dlg.ShowModal()
        elif filename.lower().endswith(".scad"):
            try:
                self.load_scad(filename)
            except:
                dlg = wx.MessageDialog(self, _("Loading OpenSCAD file failed"), _("Error"),wx.OK)
                dlg.ShowModal()

    def load_scad(self, name):
        lf = open(name)
        s = [i.replace("\n", "").replace("\r", "").replace(";", "") for i in lf if "stl" in i]
        lf.close()

        for i in s:
            parts = i.split()
            for part in parts:
                if 'translate' in part:
                    translate_list = evalme(part)
            for part in parts:
                if 'rotate' in part:
                    rotate_list = evalme(part)
            for part in parts:
                if 'import' in part:
                    stl_file = evalme(part)

            newname = os.path.split(stl_file.lower())[1]
            c = 1
            while newname in self.models:
                newname = os.path.split(stl_file.lower())[1]
                newname = newname + "(%d)" % c
                c += 1
            stl_path = os.path.join(os.path.split(name)[0:len(os.path.split(stl_file)) - 1])
            stl_full_path = os.path.join(stl_path[0], str(stl_file))
            self.load_stl_into_model(stl_full_path, stl_file, translate_list, rotate_list[2])

    def load_stl(self, name):
        if not os.path.exists(name):
            print _("Couldn't load non-existing file %s") % name
            return
        path = os.path.split(name)[0]
        self.basedir = path
        if name.lower().endswith(".stl"):
            for model in self.models.values():
                if model.filename == name:
                    newmodel = copy(model)
                    newmodel.offsets = list(model.offsets)
                    newmodel.rot = model.rot
                    newmodel.scale = list(model.scale)
                    self.add_model(name, newmodel)
                    self.s.drawmodel(newmodel, 2)
                    break
            else:
                #Filter out the path, just show the STL filename.
                self.load_stl_into_model(name, name)
        self.Refresh()

    def load_stl_into_model(self, path, name, offset = [0, 0, 0], rotation = 0, scale = [1.0, 1.0, 1.0]):
        model = stltool.stl(path)
        model.offsets = list(offset)
        model.rot = rotation
        model.scale = list(scale)
        model.filename = name
        minx = float("inf")
        miny = float("inf")
        minz = float("inf")
        maxx = float("-inf")
        maxy = float("-inf")
        maxz = float("-inf")
        for i in model.facets:
            for j in i[1]:
                if j[0] < minx:
                    minx = j[0]
                if j[1] < miny:
                    miny = j[1]
                if j[2] < minz:
                    minz = j[2]
                if j[0] > maxx:
                    maxx = j[0]
                if j[1] > maxy:
                    maxy = j[1]
                if j[2] > maxz:
                    maxz = j[2]
        model.dims = [minx, maxx, miny, maxy, minz, maxz]
        self.add_model(name, model)
        model.centeroffset = [-(model.dims[1] + model.dims[0]) / 2,
                              -(model.dims[3] + model.dims[2]) / 2,
                              0]
        self.s.drawmodel(model, 2)

    def export_to(self, name):
        sf = open(name.replace(".", "_") + ".scad", "w")
        facets = []
        for model in self.models.values():
            r = model.rot
            rot = [0, 0, r] if r else None
            o = model.offsets
            co = model.centeroffset
            sf.write("translate([%s, %s, %s])"
                     "rotate([0, 0, %s])"
                     "translate([%s, %s, %s])"
                     "import(\"%s\");\n" % (o[0], o[1], o[2],
                                            r,
                                            co[0], co[1], co[2],
                                            model.filename))
            if any(co):
                model = model.translate(co)
            if rot:
                model = model.rotate(rot)
            if any(o):
                model = model.translate(o)
            facets += model.facets
        sf.close()
        stltool.emitstl(name, facets, "plater_export")
        print _("Wrote plate to %s") % name

    def autoplate(self, event = None):
        try:
            self.autoplate_simarrange()
        except:
            traceback.print_exc(file = sys.stdout)
            print _("Failed to use simarrange for plating, "
                    "falling back to the standard method")
            super(StlPlater, self).autoplate()

    def autoplate_simarrange(self):
        print _("Autoplating using simarrange")
        models = dict(self.models)
        files = [model.filename for model in models.values()]
        command = [self.simarrange_path, "--dryrun",
                   "-m",  # Pack around center
                   "-x", str(int(self.build_dimensions[0])),
                   "-y", str(int(self.build_dimensions[1]))] + files
        p = subprocess.Popen(command, stdout = subprocess.PIPE)

        pos_regexp = re.compile("File: (.*) minx: ([0-9]+), miny: ([0-9]+), minrot: ([0-9]+)")
        for line in p.stdout:
            line = line.rstrip()
            if "Generating plate" in line:
                plateid = int(line.split()[-1])
                if plateid > 0:
                    print _("Plate full, please remove some objects")
                    break
            if "File:" in line:
                bits = pos_regexp.match(line).groups()
                filename = bits[0]
                x = float(bits[1])
                y = float(bits[2])
                rot = -float(bits[3])
                for name, model in models.items():
                    # FIXME: not sure this is going to work superwell with utf8
                    if model.filename == filename:
                        model.offsets[0] = x
                        model.offsets[1] = y
                        model.rot = rot
                        del models[name]
                        break
        if p.wait() != 0:
            raise RuntimeError, _("simarrange failed")
