"""Copyright (c) 2010-2012 David Rio Vierra

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE."""

from toolbasics import *
from select import SelectionOperation
from pymclevel.pocket import PocketWorld

CloneSettings = config.Settings("Clone")
CloneSettings.copyAir = CloneSettings("Copy Air", True)
CloneSettings.copyWater = CloneSettings("Copy Water", True)
CloneSettings.placeImmediately = CloneSettings("Place Immediately", True)


class CoordsInput(Widget):
    is_gl_container = True

    def __init__(self):
        Widget.__init__(self)

        self.nudgeButton = NudgeButton()
        self.nudgeButton.nudge = self._nudge

        self.xField = IntField(value=0)
        self.yField = IntField(value=0)
        self.zField = IntField(value=0)

        for field in (self.xField, self.yField, self.zField):
            field.change_action = self._coordsChanged
            field.enter_passes = False

        offsetCol = Column((self.xField, self.yField, self.zField))

        nudgeOffsetRow = Row((offsetCol, self.nudgeButton))

        self.add(nudgeOffsetRow)
        self.shrink_wrap()

    def getCoords(self):
        return self.xField.value, self.yField.value, self.zField.value

    def setCoords(self, coords):
        x, y, z = coords
        self.xField.text = str(x)
        self.yField.text = str(y)
        self.zField.text = str(z)

    coords = property(getCoords, setCoords, None)

    def _coordsChanged(self):
        self.coordsChanged()

    def coordsChanged(self):
        # called when the inputs change.  override or replace
        pass

    def _nudge(self, nudge):
        self.nudge(nudge)

    def nudge(self, nudge):
        # nudge is a 3-tuple where one of the elements is -1 or 1, and the others are 0.
        pass


class BlockCopyOperation(Operation):
    def __init__(self, editor, sourceLevel, sourceBox, destLevel, destPoint, copyAir, copyWater):
        self.editor = editor
        self.sourceLevel = sourceLevel
        self.sourceBox = sourceBox
        self.destLevel = destLevel
        self.destPoint = destPoint
        self.copyAir = copyAir
        self.copyWater = copyWater
        self.sourceBox, self.destPoint = self.destLevel.adjustCopyParameters(self.sourceLevel, self.sourceBox, self.destPoint)

    def dirtyBox(self):
        return BoundingBox(self.destPoint, self.sourceBox.size)

    def name(self):
        return "Copy {0} blocks".format(self.sourceBox.volume)

    def recordUndo(self):
        self.undoSchematic = self.extractUndoSchematicFrom(self.destLevel, BoundingBox(self.destPoint, self.sourceBox.size))

    def perform(self, recordUndo=True):
        dirtyBox = self.dirtyBox()
        sourceBox = self.sourceBox
        destBox = BoundingBox(self.destPoint, sourceBox.size)

        if recordUndo:
            self.recordUndo()

        blocksToCopy = None
        if not (self.copyAir and self.copyWater):
            blocksToCopy = range(256)
            if not self.copyAir:
                blocksToCopy.remove(0)
            if not self.copyWater:
                blocksToCopy.remove(8)
            if not self.copyWater:
                blocksToCopy.remove(9)

        with setWindowCaption("Copying - "):
            i = self.destLevel.copyBlocksFromIter(self.sourceLevel, self.sourceBox, self.destPoint, blocksToCopy, create=True)
            showProgress("Copying {0:n} blocks...".format(self.sourceBox.volume), i)

    def undo(self):
        if self.undoSchematic:
            self.destLevel.removeEntitiesInBox(BoundingBox(self.destPoint, self.sourceBox.size))
            self.destLevel.removeTileEntitiesInBox(BoundingBox(self.destPoint, self.sourceBox.size))

            with setWindowCaption("Undoing - "):
                i = self.destLevel.copyBlocksFromIter(self.undoSchematic, BoundingBox((0, 0, 0), self.sourceBox.size), self.destPoint, create=True)
                showProgress("Copying {0:n} blocks...".format(self.sourceBox.volume), i)

    def bufferSize(self):
        return self.undoSchematic.compressedSize()


class CloneOperation (Operation):
    def __init__(self, editor, sourceLevel, sourceBox, originSourceBox, destLevel, destPoint, copyAir, copyWater, repeatCount):
        self.blockCopyOps = []
        dirtyBoxes = []
        if repeatCount > 1:  # clone tool only
            delta = map(operator.sub, destPoint, editor.toolbar.tools[0].selectionBox().origin)
        else:
            delta = (0, 0, 0)

        for i in range(repeatCount):
            op = BlockCopyOperation(editor, sourceLevel, sourceBox, destLevel, destPoint, copyAir, copyWater)
            dirty = op.dirtyBox()

            # bounds check - xxx move to BoundingBox
            if dirty.miny >= destLevel.Height or dirty.maxy < 0:
                continue
            if destLevel.Width != 0:
                if dirty.minx >= destLevel.Width or dirty.maxx < 0:
                    continue
                if dirty.minz >= destLevel.Length or dirty.maxz < 0:
                    continue

            dirtyBoxes.append(dirty)
            self.blockCopyOps.append(op)

            destPoint = map(operator.add, delta, destPoint)

        if len(dirtyBoxes):
            def enclosingBox(dirtyBoxes):
                minx = min(map(lambda x: x.minx, dirtyBoxes))
                miny = min(map(lambda x: x.miny, dirtyBoxes))
                minz = min(map(lambda x: x.minz, dirtyBoxes))

                maxx = max(map(lambda x: x.maxx, dirtyBoxes))
                maxy = max(map(lambda x: x.maxy, dirtyBoxes))
                maxz = max(map(lambda x: x.maxz, dirtyBoxes))

                origin = (minx, miny, minz)
                maximum = (maxx, maxy, maxz)

                return BoundingBox(origin, map(operator.sub, maximum, origin))

            self._dirtyBox = enclosingBox(dirtyBoxes)

            if repeatCount > 1 and self.selectOriginalAfterRepeat:
                dirtyBoxes.append(originSourceBox)

            dirty = enclosingBox(dirtyBoxes)
            points = (dirty.origin, map(lambda x: x - 1, dirty.maximum))

            self.selectionOps = [SelectionOperation(editor.selectionTool, points)]

        else:
            self._dirtyBox = None
            self.selectionOps = []

    selectOriginalAfterRepeat = True

    def dirtyBox(self):
        return self._dirtyBox

    def perform(self, recordUndo=True):
        if recordUndo:
            [i.recordUndo() for i in self.blockCopyOps]
        [i.perform(False) for i in self.blockCopyOps]
        [i.perform(recordUndo) for i in self.selectionOps]

    def undo(self):
        [i.undo() for i in self.blockCopyOps]
        [i.undo() for i in self.selectionOps]


class CloneToolPanel(Panel):
    useOffsetInput = True

    def transformEnable(self):
        return not isinstance(self.tool.level, MCInfdevOldLevel)

    def __init__(self, tool):
        Panel.__init__(self)
        self.tool = tool

        rotateRow = Row((
            Label(config.config.get("Keys", "Rotate").upper()), Button("Rotate", width=80, action=tool.rotate, enable=self.transformEnable),
        ))

        rollRow = Row((
            Label(config.config.get("Keys", "Roll").upper()), Button("Roll", width=80, action=tool.roll, enable=self.transformEnable),
        ))

        flipRow = Row((
            Label(config.config.get("Keys", "Flip").upper()), Button("Flip", width=80, action=tool.flip, enable=self.transformEnable),
        ))

        mirrorRow = Row((
            Label(config.config.get("Keys", "Mirror").upper()), Button("Mirror", width=80, action=tool.mirror, enable=self.transformEnable),
        ))

        alignRow = Row((
            CheckBox(ref=AttrRef(self.tool, 'chunkAlign')), Label("Chunk Align")
        ))

        # headerLabel = Label("Clone Offset")
        if self.useOffsetInput:
            self.offsetInput = CoordsInput()
            self.offsetInput.coordsChanged = tool.offsetChanged
            self.offsetInput.nudgeButton.bg_color = tool.color
            self.offsetInput.nudge = tool.nudge
        else:
            self.nudgeButton = NudgeButton()
            self.nudgeButton.bg_color = tool.color
            self.nudgeButton.nudge = tool.nudge

        repeatField = IntField(ref=AttrRef(tool, 'repeatCount'))
        repeatField.min = 1
        repeatField.max = 50

        repeatRow = Row((
            Label("Repeat"), repeatField
        ))
        self.repeatField = repeatField

        scaleField = FloatField(ref=AttrRef(tool, 'scaleFactor'))
        scaleField.min = 0.125
        scaleField.max = 8
        dv = scaleField.decrease_value
        iv = scaleField.increase_value

        def scaleFieldDecrease():
            if scaleField.value > 1 / 8.0 and scaleField.value <= 1.0:
                scaleField.value *= 0.5
            else:
                dv()

        def scaleFieldIncrease():
            if scaleField.value < 1.0:
                scaleField.value *= 2.0
            else:
                iv()

        scaleField.decrease_value = scaleFieldDecrease
        scaleField.increase_value = scaleFieldIncrease

        scaleRow = Row((
            Label("Scale Factor"), scaleField
        ))

        self.scaleField = scaleField

        self.copyAirCheckBox = CheckBox(ref=AttrRef(self.tool, "copyAir"))
        self.copyAirLabel = Label("Copy Air")
        self.copyAirLabel.mouse_down = self.copyAirCheckBox.mouse_down
        self.copyAirLabel.tooltipText = "Shortcut: ALT-1"
        self.copyAirCheckBox.tooltipText = self.copyAirLabel.tooltipText

        copyAirRow = Row((self.copyAirCheckBox, self.copyAirLabel))

        self.copyWaterCheckBox = CheckBox(ref=AttrRef(self.tool, "copyWater"))
        self.copyWaterLabel = Label("Copy Water")
        self.copyWaterLabel.mouse_down = self.copyWaterCheckBox.mouse_down
        self.copyWaterLabel.tooltipText = "Shortcut: ALT-2"
        self.copyWaterCheckBox.tooltipText = self.copyWaterLabel.tooltipText

        copyWaterRow = Row((self.copyWaterCheckBox, self.copyWaterLabel))

        self.performButton = Button("Clone", width=100, align="c")
        self.performButton.tooltipText = "Shortcut: ENTER"
        self.performButton.action = tool.confirm
        self.performButton.enable = lambda: (tool.destPoint is not None)
        if self.useOffsetInput:
            col = Column((rotateRow, rollRow, flipRow, mirrorRow, alignRow, self.offsetInput, repeatRow, scaleRow, copyAirRow, copyWaterRow, self.performButton))
        else:
            col = Column((rotateRow, rollRow, flipRow, mirrorRow, alignRow, self.nudgeButton, copyAirRow, copyWaterRow, self.performButton))

        self.add(col)
        self.anchor = "lwh"

        self.shrink_wrap()


class CloneToolOptions(ToolOptions):
    def __init__(self, tool):
        Panel.__init__(self)
        self.tool = tool
        self.autoPlaceCheckBox = CheckBox(ref=AttrRef(tool, "placeImmediately"))
        self.autoPlaceLabel = Label("Place Immediately")
        self.autoPlaceLabel.mouse_down = self.autoPlaceCheckBox.mouse_down

        tooltipText = "When the clone tool is chosen, place the clone at the selection right away."
        self.autoPlaceLabel.tooltipText = self.autoPlaceCheckBox.tooltipText = tooltipText

        row = Row((self.autoPlaceCheckBox, self.autoPlaceLabel))
        col = Column((Label("Clone Options"), row, Button("OK", action=self.dismiss)))

        self.add(col)
        self.shrink_wrap()


class CloneTool(EditorTool):
    surfaceBuild = True
    toolIconName = "clone"
    tooltipText = "Clone\nRight-click for options"
    level = None
    repeatCount = 1
    _scaleFactor = 1.0
    _chunkAlign = False

    @property
    def scaleFactor(self):
        return self._scaleFactor

    @scaleFactor.setter
    def scaleFactor(self, val):
        self.rescaleLevel(val)
        self._scaleFactor = val

    @property
    def chunkAlign(self):
        return self._chunkAlign

    @chunkAlign.setter
    def chunkAlign(self, value):
        self._chunkAlign = value
        self.alignDestPoint()

    def alignDestPoint(self):
        if self.destPoint is not None:
            x, y, z = self.destPoint
            self.destPoint = (x >> 4) << 4, y, (z >> 4) << 4

    placeImmediately = CloneSettings.placeImmediately.configProperty()

    panelClass = CloneToolPanel
    # color = (0.89, 0.65, 0.35, 0.33)
    color = (0.3, 1.0, 0.3, 0.19)

    def __init__(self, *args):
        self.rotation = 0

        EditorTool.__init__(self, *args)
        self.previewRenderer = None
        self.panel = None

        self.optionsPanel = CloneToolOptions(self)

        self.destPoint = None

    @property
    def statusText(self):
        if self.destPoint == None:
            return "Click to set this item down."
        if self.draggingFace is not None:
            return "Mousewheel to move along the third axis. Hold SHIFT to only move along one axis."

        return "Click and drag to reposition the item. Double-click to pick it up. Click Clone or press ENTER to confirm."

    def quickNudge(self, nudge):
        return map(int.__mul__, nudge, self.selectionBox().size)

    copyAir = CloneSettings.copyAir.configProperty()
    copyWater = CloneSettings.copyWater.configProperty()

    def nudge(self, nudge):
        if self.destPoint is None:
            if self.selectionBox() is None:
                return
            self.destPoint = self.selectionBox().origin

        if self.chunkAlign:
            x, y, z = nudge
            nudge = x << 4, y, z << 4

        if key.get_mods() & KMOD_SHIFT:
            nudge = self.quickNudge(nudge)

        # self.panel.performButton.enabled = True
        self.destPoint = map(lambda a, b: a + b, self.destPoint, nudge)
        self.updateOffsets()

    def selectionChanged(self):
        if self.selectionBox() is not None:
            self.updateSchematic()
            self.updateOffsets()

    def updateOffsets(self):
        if self.panel and self.panel.useOffsetInput and self.destPoint is not None:
            self.panel.offsetInput.setCoords(map(operator.sub, self.destPoint, self.selectionBox().origin))

    def offsetChanged(self):

        if self.panel:
            if not self.panel.useOffsetInput:
                return
            box = self.selectionBox()
            if box is None:
                return

            delta = self.panel.offsetInput.coords
            self.destPoint = map(operator.add, box.origin, delta)

    def toolEnabled(self):
        return not (self.selectionBox() is None)

    def cancel(self):

        self.discardPreviewer()
        if self.panel:
            self.panel.parent.remove(self.panel)
            self.panel = None

        self.destPoint = None
        self.level = None
        self.originalLevel = None

    def toolReselected(self):
        self.pickUp()

    def safeToolDistance(self):
        return sqrt(sum([self.level.Width ** 2, self.level.Height ** 2, self.level.Length ** 2]))

    def toolSelected(self):
        box = self.selectionBox()
        if box is None:
            self.editor.toolbar.selectTool(-1)
            return

        if box.volume > self.maxBlocks:
            self.editor.mouseLookOff()
            alert("Selection exceeds {0:n} blocks. Increase the block buffer setting and try again.".format(self.maxBlocks))
            self.editor.toolbar.selectTool(-1)
            return

        self.rotation = 0
        self.repeatCount = 1
        self._scaleFactor = 1.0

        if self.placeImmediately:
            self.destPoint = list(box.origin)
        else:
            self.destPoint = None

        self.updateSchematic()
        self.cloneCameraDistance = max(self.cloneCameraDistance, self.safeToolDistance())
        self.showPanel()

    cloneCameraDistance = 0

    @property
    def cameraDistance(self):
        return self.cloneCameraDistance

    @alertException
    def rescaleLevel(self, factor):
        # if self.level.cloneToolScaleFactor == newFactor:
        #    return
        # oldfactor = self.level.cloneToolScaleFactor
        # factor = newFactor / oldfactor
        if factor == 1:
            self.level = self.originalLevel
            self.setupPreview()
            return

        oldshape = self.originalLevel.Blocks.shape
        blocks = self.originalLevel.Blocks
        data = self.originalLevel.Data

        if factor < 1.0:
            roundedShape = map(lambda x: int(int(x * factor) / factor), oldshape)
            roundedSlices = map(lambda x: slice(0, x), roundedShape)
            blocks = blocks[roundedSlices]
            data = data[roundedSlices]
        else:
            roundedShape = oldshape

        newshape = map(lambda x: int(x * factor), oldshape)
        xyzshape = newshape[0], newshape[2], newshape[1]
        newlevel = MCSchematic(xyzshape, mats=self.editor.level.materials)

        srcgrid = mgrid[0:roundedShape[0]:1.0 / factor, 0:roundedShape[1]:1.0 / factor, 0:roundedShape[2]:1.0 / factor].astype('uint')
        dstgrid = mgrid[0:newshape[0], 0:newshape[1], 0:newshape[2]].astype('uint')
        srcgrid = srcgrid[map(slice, dstgrid.shape)]
        dstgrid = dstgrid[map(slice, srcgrid.shape)]

        def copyArray(dest, src):
            dest[dstgrid[0], dstgrid[1], dstgrid[2]] = src[srcgrid[0], srcgrid[1], srcgrid[2]]

        copyArray(newlevel.Blocks, blocks)
        copyArray(newlevel.Data, data)

        self.level = newlevel
        self.setupPreview()
#
#        """
#        use array broadcasting to fill in the extra dimensions with copies of the
#        existing ones, then later change the shape to "fold" the extras back
#        into the original three
#        """
#        # if factor > 1.0:
#        sourceSlice = slice(0, 1)
#        destSlice = slice(None)
#
#        # if factor < 1.0:
#
#        destfactor = factor
#        srcfactor = 1
#        if factor < 1.0:
#            destfactor = 1.0
#            srcfactor = 1.0 / factor
#
#        intershape = newshape[0]/destfactor, destfactor, newshape[1]/destfactor, destfactor, newshape[2]/destfactor, destfactor
#        srcshape = roundedShape[0]/srcfactor, srcfactor, roundedShape[1]/srcfactor, srcfactor, roundedShape[2]/srcfactor, srcfactor
#
#        newlevel = MCSchematic(xyzshape)
#
#        def copyArray(dest, src):
#            dest.shape = intershape
#            src.shape = srcshape
#
#            dest[:, destSlice, :, destSlice, :, destSlice] = src[:, sourceSlice, :, sourceSlice, :, sourceSlice]
#            dest.shape = newshape
#            src.shape = roundedShape
#
#        copyArray(newlevel.Blocks, blocks)
#        copyArray(newlevel.Data, data)
#
#        newlevel.cloneToolScaleFactor = newFactor
#

    @alertException
    def updateSchematic(self):
        # extract blocks
        with setWindowCaption("COPYING - "):
            self.editor.freezeStatus("Copying to clone buffer...")
            box = self.selectionBox()
            self.level = self.editor.level.extractSchematic(box)
            self.originalLevel = self.level
            # self.level.cloneToolScaleFactor = 1.0
            self.rescaleLevel(self.scaleFactor)
            self.setupPreview()

    def showPanel(self):
        if self.panel:
            self.panel.set_parent(None)

        self.panel = self.panelClass(self)
        # self.panel.performButton.enabled = False

        self.panel.centery = self.editor.centery
        self.panel.left = self.editor.left
        self.editor.add(self.panel)

    def setupPreview(self, alpha=1.0):
        self.discardPreviewer()
        if self.level:
            self.previewRenderer = PreviewRenderer(self.level, alpha)
            self.previewRenderer.position = self.editor.renderer.position
            self.editor.addWorker(self.previewRenderer)
        else:
            self.editor.toolbar.selectTool(-1)

    @property
    def canRotateLevel(self):
        return not isinstance(self.level, (MCInfdevOldLevel, PocketWorld))

    def rotatedSelectionSize(self):
        if self.canRotateLevel:
            sizes = self.level.Blocks.shape
            return sizes[0], sizes[2], sizes[1]
        else:
            return self.level.size

    # ===========================================================================
    # def getSelectionRanges(self):
    #    return self.editor.selectionTool.selectionBox()
    #
    # ===========================================================================
    def getBlockAt(self):
        return None  # use level's blockAt

    def getReticleOrigin(self):
        # returns a new origin for the current selection, where the old origin is at the new selection's center.
        pos, direction = self.editor.blockFaceUnderCursor

        lev = self.editor.level
        size = self.rotatedSelectionSize()
        if not size:
            return
        if size[1] >= self.editor.level.Height:
            direction = (0, 1, 0)  # always use the upward face whenever we're splicing full-height pieces, to avoid "jitter"

        # print size; raise SystemExit
        if any(direction) and pos[1] >= 0:
            x, y, z = map(lambda p, s, d: p - s / 2 + s * d / 2 + (d > 0), pos, size, direction)
        else:
            x, y, z = map(lambda p, s: p - s / 2, pos, size)

        if self.chunkAlign:
            x = x & ~0xf
            z = z & ~0xf

        sy = size[1]
        if sy > lev.Height:  # don't snap really tall stuff to the height
            return x, y, z

        if y + sy > lev.Height:
            y = lev.Height - sy
        if y < 0:
            y = 0

        if not isinstance(lev, MCInfdevOldLevel):
            sx = size[0]
            if x + sx > lev.Width:
                x = lev.Width - sx
            if x < 0:
                x = 0

            sz = size[2]
            if z + sz > lev.Length:
                z = lev.Length - sz
            if z < 0:
                z = 0

        return x, y, z

    def getReticleBox(self):

        pos = self.getReticleOrigin()
        sizes = self.rotatedSelectionSize()

        if None is sizes:
            return

        return BoundingBox(pos, sizes)

    def getDestBox(self):
        selectionSize = self.rotatedSelectionSize()
        return BoundingBox(self.destPoint, selectionSize)

    def drawTerrainReticle(self):
        if self.level is None:
            return

        if self.destPoint != None:
            destPoint = self.destPoint
            if self.draggingFace is not None:
                # debugDrawPoint()
                destPoint = self.draggingOrigin()

            self.drawTerrainPreview(destPoint)
        else:
            self.drawTerrainPreview(self.getReticleBox().origin)

    draggingColor = (0.77, 1.0, 0.55, 0.05)

    def drawToolReticle(self):

        if self.level is None:
            return

        glPolygonOffset(DepthOffset.CloneMarkers, DepthOffset.CloneMarkers)

        color = self.color
        if self.destPoint is not None:
            alpha = sin((self.editor.frameStartTime.second + self.editor.frameStartTime.microsecond / 1000000) * 3.14) * 0.124 + 0.25
            color = (self.color[0], self.color[1], self.color[2], 0.06)
            box = self.getDestBox()
            if self.draggingFace is not None:
                box.origin = self.draggingOrigin()
                guideBox = BoundingBox(box)
                for i in range(3):
                    if i == self.draggingFace >> 1:
                        continue
                    guideBox._origin[i] -= 1000
                    guideBox._size[i] += 2000

                color = self.draggingColor
                glColor(1.0, 1.0, 1.0, 0.33)
                with gl.glEnable(GL_BLEND, GL_TEXTURE_2D, GL_DEPTH_TEST):
                    self.editor.sixteenBlockTex.bind()
                    drawFace(guideBox, self.draggingFace ^ 1)
        else:
            box = self.getReticleBox()
            if box is None:
                return
        self.drawRepeatedCube(box, color)

        glPolygonOffset(DepthOffset.CloneReticle, DepthOffset.CloneReticle)
        if self.destPoint:
            box = self.getDestBox()
            if self.draggingFace is not None:
                face = self.draggingFace
                box.origin = self.draggingOrigin()
            face, point = self.boxFaceUnderCursor(box)
            if face is not None:
                glEnable(GL_BLEND)
                glDisable(GL_DEPTH_TEST)

                glColor(*self.color)
                drawFace(box, face)
                glDisable(GL_BLEND)
                glEnable(GL_DEPTH_TEST)

    def drawRepeatedCube(self, box, color):
        # draw several cubes according to the repeat count
        # it's not really sensible to repeat a crane because the origin point is literally out of this world.
        delta = map(operator.sub, box.origin, self.selectionBox().origin)

        for i in range(self.repeatCount):
            self.editor.drawConstructionCube(box, color)
            box.origin = map(operator.add, box.origin, delta)

    def sourceLevel(self):
        return self.level

    @alertException
    def rotate(self, amount=1):
        if self.canRotateLevel:
            self.rotation += amount
            self.rotation &= 0x3
            for i in range(amount & 0x3):
                self.level.rotateLeft()

            self.previewRenderer.level = self.level

    @alertException
    def roll(self, amount=1):
        if self.canRotateLevel:
            for i in range(amount & 0x3):
                self.level.roll()

            self.previewRenderer.level = self.level

    @alertException
    def flip(self, amount=1):
        if self.canRotateLevel:
            for i in range(amount & 0x1):
                self.level.flipVertical()

            self.previewRenderer.level = self.level

    @alertException
    def mirror(self):
        if self.canRotateLevel:
            yaw = int(self.editor.mainViewport.yaw) % 360
            if (yaw >= 45 and yaw < 135) or (yaw > 225 and yaw <= 315):
                self.level.flipEastWest()
            else:
                self.level.flipNorthSouth()

            self.previewRenderer.level = self.level

    def option1(self):
        self.copyAir = not self.copyAir

    def option2(self):
        self.copyWater = not self.copyWater

    draggingFace = None
    draggingStartPoint = None

    def draggingOrigin(self):
        p = self._draggingOrigin()
        return p

    def _draggingOrigin(self):
        dragPos = map(int, map(floor, self.positionOnDraggingPlane()))
        delta = map(lambda s, e: e - int(floor(s)), self.draggingStartPoint, dragPos)

        if key.get_mods() & KMOD_SHIFT:
            ad = map(abs, delta)
            midx = ad.index(max(ad))
            d = [0, 0, 0]
            d[midx] = delta[midx]
            dragY = self.draggingFace >> 1
            d[dragY] = delta[dragY]
            delta = d

        p = map(lambda a, b: a + b, delta, self.destPoint)
        if self.chunkAlign:
            p = [i // 16 * 16 for i in p]
        return p

    def positionOnDraggingPlane(self):
        pos = self.editor.mainViewport.cameraPosition
        dim = self.draggingFace >> 1
#        if key.get_mods() & KMOD_SHIFT:
#            dim = self.findBestTrackingPlane(self.draggingFace)
#
        distance = self.draggingStartPoint[dim] - pos[dim]
        distance += self.draggingY

        mouseVector = self.editor.mainViewport.mouseVector
        scale = distance / (mouseVector[dim] or 1)
        point = map(lambda a, b: a * scale + b, mouseVector, pos)
        return point

    draggingY = 0

    @alertException
    def mouseDown(self, evt, pos, direction):
        box = self.selectionBox()
        if not box:
            return
        self.draggingY = 0

        if self.destPoint is not None:
            if evt.num_clicks == 2:
                self.pickUp()
                return

            face, point = self.boxFaceUnderCursor(self.getDestBox())
            if face is not None:
                self.draggingFace = face
                self.draggingStartPoint = point

        else:
            self.destPoint = self.getReticleOrigin()

            if self.panel and self.panel.useOffsetInput:
                self.panel.offsetInput.setCoords(map(operator.sub, self.destPoint, box.origin))
            print "Destination: ", self.destPoint

    @alertException
    def mouseUp(self, evt, pos, direction):
        if self.draggingFace is not None:
            self.destPoint = self.draggingOrigin()

        self.draggingFace = None
        self.draggingStartPoint = None

    def increaseToolReach(self):
        if self.draggingFace is not None:
            d = (1, -1)[self.draggingFace & 1]
            if self.draggingFace >> 1 != 1:  # xxxxx y
                d = -d
            self.draggingY += d
            x, y, z = self.editor.mainViewport.cameraPosition
            pos = [x, y, z]
            pos[self.draggingFace >> 1] += d
            self.editor.mainViewport.cameraPosition = tuple(pos)

        else:
            self.cloneCameraDistance = self.editor._incrementReach(self.cloneCameraDistance)
        return True

    def decreaseToolReach(self):
        if self.draggingFace is not None:
            d = (1, -1)[self.draggingFace & 1]
            if self.draggingFace >> 1 != 1:  # xxxxx y
                d = -d

            self.draggingY -= d
            x, y, z = self.editor.mainViewport.cameraPosition
            pos = [x, y, z]
            pos[self.draggingFace >> 1] -= d
            self.editor.mainViewport.cameraPosition = tuple(pos)

        else:
            self.cloneCameraDistance = self.editor._decrementReach(self.cloneCameraDistance)
        return True

    def resetToolReach(self):
        if self.draggingFace is not None:
            x, y, z = self.editor.mainViewport.cameraPosition
            pos = [x, y, z]
            pos[self.draggingFace >> 1] += (1, -1)[self.draggingFace & 1] * -self.draggingY
            self.editor.mainViewport.cameraPosition = tuple(pos)
            self.draggingY = 0

        else:
            self.cloneCameraDistance = max(self.editor.defaultCameraToolDistance, self.safeToolDistance())

        return True

    def pickUp(self):
        if self.destPoint == None:
            return

        box = self.selectionBox()

        # pick up the object. reset the tool distance to the object's distance from the camera
        d = map(lambda a, b, c: abs(a - b - c / 2), self.editor.mainViewport.cameraPosition, self.destPoint, box.size)
        self.cloneCameraDistance = sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2])
        self.destPoint = None
        # self.panel.performButton.enabled = False
        print "Picked up"

    @alertException
    def confirm(self):
        destPoint = self.destPoint
        if destPoint is None:
            return

        sourceLevel = self.sourceLevel()
        sourceBox = sourceLevel.bounds

        destLevel = self.editor.level
        destVolume = BoundingBox(destPoint, sourceBox.size).volume

        op = CloneOperation(editor=self.editor,
                            sourceLevel=sourceLevel,
                            sourceBox=sourceBox,
                            originSourceBox=self.selectionBox(),
                            destLevel=destLevel,
                            destPoint=self.destPoint,
                            copyAir=self.copyAir,
                            copyWater=self.copyWater,
                            repeatCount=self.repeatCount)

        self.editor.toolbar.selectTool(-1)  # deselect tool so that the clone tool's selection change doesn't update its schematic

        with setWindowCaption("COPYING - "):
            self.editor.freezeStatus("Copying %0.1f million blocks" % (float(destVolume) / 1048576.,))
            self.performWithRetry(op)

        self.editor.addUnsavedEdit()

        self.editor.addOperation(op)

        dirtyBox = op.dirtyBox()
        if dirtyBox:
            self.editor.invalidateBox(dirtyBox)
        self.editor.renderer.invalidateChunkMarkers()

        self.editor.currentOperation = None

        self.destPoint = None
        self.level = None

    def discardPreviewer(self):
        if self.previewRenderer is None:
            return
        self.previewRenderer.stopWork()
        self.previewRenderer.discardAllChunks()
        self.editor.removeWorker(self.previewRenderer)
        self.previewRenderer = None


class ConstructionToolPanel (CloneToolPanel):
    useOffsetInput = False


class ConstructionTool(CloneTool):
    surfaceBuild = True
    toolIconName = "crane"
    tooltipText = "Import"

    panelClass = ConstructionToolPanel

    def toolEnabled(self):
        return True

    def selectionChanged(self):
        pass

    def updateSchematic(self):
        pass

    def quickNudge(self, nudge):
        return map(lambda x: x * 8, nudge)

    def __init__(self, *args):
        CloneTool.__init__(self, *args)
        self.level = None
        self.optionsPanel = None

    @property
    def statusText(self):
        if self.destPoint == None:
            return "Click to set this item down."

        return "Click and drag to reposition the item. Double-click to pick it up. Click Import or press ENTER to confirm."

    def showPanel(self):
        CloneTool.showPanel(self)
        self.panel.performButton.text = "Import"

    def toolReselected(self):
        self.toolSelected()

    #    def cancel(self):
#        print "Cancelled Clone"
#        self.level = None
#        super(ConstructionTool, self).cancel(self)
#

    def createTestBoard(self, anyBlock=True):
        if anyBlock:
            allBlocks = [self.editor.level.materials[a, b] for a in range(256) for b in range(16)]
            blockWidth = 64
        else:
            allBlocks = self.editor.level.materials.allBlocks
            blockWidth = 16
        blockCount = len(allBlocks)

        width = blockWidth * 3 + 1
        rows = blockCount // blockWidth + 1
        length = rows * 3 + 1
        height = 3

        schematic = MCSchematic((width, height, length), mats=self.editor.level.materials)
        schematic.Blocks[:, :, 0] = 1

        for i, block in enumerate(allBlocks):
            col = (i % blockWidth) * 3 + 1
            row = (i // blockWidth) * 3
            schematic.Blocks[col:col + 2, row:row + 2, 2] = block.ID
            schematic.Data[col:col + 2, row:row + 2, 2] = block.blockData

        return schematic

    def toolSelected(self):
        self.editor.mouseLookOff()

        mods = key.get_mods()
        if mods & KMOD_ALT and mods & KMOD_SHIFT:
            self.loadLevel(self.createTestBoard())
            return

        self.editor.mouseLookOff()

        clipFilename = mcplatform.askOpenFile(title='Import a schematic or level...', schematics=True)
        # xxx mouthful
        if clipFilename:

            self.loadSchematic(clipFilename)

        print "Canceled"
        if self.level is None:
            print "No level selected."

            self.editor.toolbar.selectTool(-1)

        # CloneTool.toolSelected(self)

    originalLevelSize = (0, 0, 0)

    def loadSchematic(self, filename):
        """ actually loads a schematic or a level """
        try:
            level = fromFile(filename)
            self.loadLevel(level)
        except Exception, e:
            print u"Unable to import file {0} : {1}".format(filename, e)

            traceback.print_exc()
            if filename:
                # self.editor.toolbar.selectTool(-1)
                alert(u"I don't know how to import this file: {0}.\n\nError: {1!r}".format(os.path.basename(filename), e))

            return

    @alertException
    def loadLevel(self, level):
        if level:
            self.level = level
            self.repeatCount = 1
            self.destPoint = None

            self.editor.currentTool = self  # because save window triggers loseFocus, which triggers tool.cancel... hmmmmmm

            self.cloneCameraDistance = self.safeToolDistance()

            self.chunkAlign = isinstance(self.level, MCInfdevOldLevel) and all(b % 16 == 0 for b in self.level.bounds.size)

            self.setupPreview()
            self.originalLevelSize = (self.level.Width, self.level.Height, self.level.Length)
            self.showPanel()
            return

    def selectionSize(self):
        if not self.level:
            return None
        return  self.originalLevelSize

    def selectionBox(self):
        if not self.level:
            return None
        return BoundingBox((0, 0, 0), self.selectionSize())

    def sourceLevel(self):
        return self.level

    def mouseDown(self, evt, pos, direction):
        # x,y,z = pos
        box = self.selectionBox()
        if not box:
            return

        CloneTool.mouseDown(self, evt, pos, direction)
