# Allow user to move a connected group of live cells.
# Author: Andrew Trevorrow (andrew@trevorrow.com), Jan 2011.

import golly as g
from glife import rect, getminbox

ncells = []    # list of neighboring live cells

# set edges of bounded grid for later use
if g.getwidth() > 0:
   gridl = -int(g.getwidth()/2)
   gridr = gridl + g.getwidth() - 1
if g.getheight() > 0:
   gridt = -int(g.getheight()/2)
   gridb = gridt + g.getheight() - 1

helpmsg = " (hit 'h' for help)"

# --------------------------------------------------------------------

def showhelp1():
   g.note(
"""Hit the escape key to abort the script.

Note that alt-clicking on an object allows you to COPY it
to another location (the original object is not deleted).""")

# --------------------------------------------------------------------

def showhelp2():
   g.note(
"""While moving the object the following keys can be used:

x -- flip object left-right
y -- flip object top-bottom
> -- rotate object clockwise
< -- rotate object anticlockwise
h -- show this help
escape -- abort and restore the object""")

# ------------------------------------------------------------------------------

def getstate(x, y):
   # first check if x,y is outside bounded grid
   if g.getwidth() > 0 and (x < gridl or x > gridr): return 0
   if g.getheight() > 0 and (y < gridt or y > gridb): return 0
   return g.getcell(x, y)

# ------------------------------------------------------------------------------

def findlivecell(x, y):
   if g.getcell(x, y) > 0: return [x, y]
   
   # spiral outwards from x,y looking for a nearby live cell;
   # the smaller the scale the smaller the area searched
   maxd = 10
   mag = g.getmag()
   if mag > 0:
      # mag can be 1..4 (ie. scales 1:2 to 1:16)
      maxd = 2 * (5 - mag)    # 8, 6, 4, 2
   d = 1
   while d <= maxd:
      x -= 1
      y -= 1
      for i in xrange(2*d):
         x += 1   # move east
         if getstate(x, y) > 0: return [x, y]
      for i in xrange(2*d):
         y += 1   # move south
         if getstate(x, y) > 0: return [x, y]
      for i in xrange(2*d):
         x -= 1   # move west
         if getstate(x, y) > 0: return [x, y]
      for i in xrange(2*d):
         y -= 1   # move north
         if getstate(x, y) > 0: return [x, y]
      d += 1
   
   return []   # failed to find a live cell

# ------------------------------------------------------------------------------

def checkneighbor(x, y):
   # first check if x,y is outside bounded grid
   if g.getwidth() > 0 and (x < gridl or x > gridr): return
   if g.getheight() > 0 and (y < gridt or y > gridb): return
   if g.getcell(x, y) == 0: return
   # no need for next test because we kill cell after adding it to ncells
   # if (x, y) in ncells: return False
   ncells.append( (x, y, g.getcell(x,y)) )
   g.setcell(x, y, 0)

# ------------------------------------------------------------------------------

def getobject(x, y):
   object = []
   ncells.append( (x, y, g.getcell(x,y)) )
   g.setcell(x, y, 0)
   while len(ncells) > 0:
      # remove cell from end of ncells and append to object
      x, y, s = ncells.pop()
      object.append(x)
      object.append(y)
      object.append(s)
      # add any live neighbors to ncells
      checkneighbor(x  , y+1)
      checkneighbor(x  , y-1)
      checkneighbor(x+1, y  )
      checkneighbor(x-1, y  )
      checkneighbor(x+1, y+1)
      checkneighbor(x+1, y-1)
      checkneighbor(x-1, y+1)
      checkneighbor(x-1, y-1)
   # append padding int if necessary
   if len(object) > 0 and (len(object) & 1) == 0: object.append(0)
   g.putcells(object)
   return object

# ------------------------------------------------------------------------------

def underneath(object):
   # return list of live cells underneath given object (a multi-state list)
   cells = []
   objlen = len(object)
   if objlen % 3 == 1: objlen -= 1    # ignore padding int
   i = 0
   while i < objlen:
      x = object[i]
      y = object[i+1]
      s = g.getcell(x, y)
      if s > 0:
         cells.append(x)
         cells.append(y)
         cells.append(s)
      i += 3
   # append padding int if necessary
   if len(cells) > 0 and (len(cells) & 1) == 0: cells.append(0)
   return cells

# ------------------------------------------------------------------------------

def rectingrid(r):
   # return True if all of given rectangle is inside grid
   if g.getwidth() > 0 and (r[0] < gridl or r[0] + r[2] - 1 > gridr): return False
   if g.getheight() > 0 and (r[1] < gridt or r[1] + r[3] - 1 > gridb): return False
   return True

# ------------------------------------------------------------------------------

def lookforkeys(event):
   global oldcells, object
   
   # look for keys used to flip/rotate object
   if event == "key x none" or event == "key y none":
      # flip floating object left-right or top-bottom
      g.putcells(object, 0, 0, 1, 0, 0, 1, "xor")  # erase object
      if len(oldcells) > 0: g.putcells(oldcells)
      obox = getminbox(object)
      if event == "key x none":
         # translate object so that bounding box doesn't change
         xshift = 2 * (obox.left + int(obox.wd/2))
         if obox.wd % 2 == 0: xshift -= 1
         object = g.transform(object, xshift, 0, -1, 0, 0, 1)
      else:
         # translate object so that bounding box doesn't change
         yshift = 2 * (obox.top + int(obox.ht/2))
         if obox.ht % 2 == 0: yshift -= 1
         object = g.transform(object, 0, yshift, 1, 0, 0, -1)
      oldcells = underneath(object)
      g.putcells(object)
      g.update()
      return
   
   if event == "key > none" or event == "key < none":
      # rotate floating object clockwise or anticlockwise
      # about the center of the object's bounding box
      obox = getminbox(object)
      midx = obox.left + int(obox.wd/2)
      midy = obox.top + int(obox.ht/2)
      newleft = midx + obox.top - midy
      newtop = midy + obox.left - midx
      rotrect = [ newleft, newtop, obox.ht, obox.wd ]
      if not rectingrid(rotrect):
         g.warn("Rotation is not allowed if object would be outside grid.")
         return
      g.putcells(object, 0, 0, 1, 0, 0, 1, "xor")  # erase object
      if len(oldcells) > 0: g.putcells(oldcells)
      if event == "key > none":
         # rotate clockwise
         object = g.transform(object, 0, 0, 0, -1, 1, 0)
      else:
         # rotate anticlockwise
         object = g.transform(object, 0, 0, 0, 1, -1, 0)
      # shift rotated object to same position as rotrect
      obox = getminbox(object)
      object = g.transform(object, newleft - obox.left, newtop - obox.top)
      oldcells = underneath(object)
      g.putcells(object)
      g.update()
      return

   if event == "key h none":
      showhelp2()
      return
   
   g.doevent(event)

# ------------------------------------------------------------------------------

def moveobject():
   global oldcells, object, object1
   
   # wait for 1st click in live cell
   while True:
      event = g.getevent()
      if event.startswith("click"):
         # event is a string like "click 10 20 left none"
         evt, xstr, ystr, butt, mods = event.split()
         result = findlivecell(int(xstr), int(ystr))
         if len(result) > 0:
            prevx = int(xstr)
            prevy = int(ystr)
            oldmouse = xstr + ' ' + ystr
            g.show("Extracting object...")
            x, y = result
            object = getobject(x, y)
            object1 = list(object)     # save in case user aborts script
            if mods == "alt":
               # don't delete object
               oldcells = list(object)
            break
         else:
            g.warn("Click on or near a live cell belonging to the desired object.")
      elif event == "key h none":
         showhelp1()
      else:
         g.doevent(event)
   
   # wait for 2nd click while moving object
   g.show("Move mouse and click again..." + helpmsg)
   gotclick = False
   while not gotclick:
      event = g.getevent()
      if event.startswith("click"):
         evt, x, y, butt, mods = event.split()
         mousepos = x+' '+y
         gotclick = True
      else:
         if len(event) > 0: lookforkeys(event)
         mousepos = g.getxy()
      if len(mousepos) > 0 and mousepos != oldmouse:
         # mouse has moved, so move object
         g.putcells(object, 0, 0, 1, 0, 0, 1, "xor")  # erase object
         if len(oldcells) > 0: g.putcells(oldcells)
         xstr, ystr = mousepos.split()
         x = int(xstr)
         y = int(ystr)

         if g.getwidth() > 0:
            # ensure object doesn't move beyond left/right edge of grid
            obox = getminbox( g.transform(object, x - prevx, y - prevy) )
            if obox.left < gridl:
               x += gridl - obox.left
            elif obox.right > gridr:
               x -= obox.right - gridr
         if g.getheight() > 0:
            # ensure object doesn't move beyond top/bottom edge of grid
            obox = getminbox( g.transform(object, x - prevx, y - prevy) )
            if obox.top < gridt:
               y += gridt - obox.top
            elif obox.bottom > gridb:
               y -= obox.bottom - gridb

         object = g.transform(object, x - prevx, y - prevy)
         oldcells = underneath(object)
         g.putcells(object)
         prevx = x
         prevy = y
         oldmouse = mousepos
         g.update()

# ------------------------------------------------------------------------------

if len(g.getrect()) == 0: g.exit("There are no objects.")

g.show("Click on or near live cell in object, move mouse and click again..." + helpmsg)
oldcursor = g.getcursor()
g.setcursor("Move")
oldcells = []        # cells under moved object
object = []          # cells in moving object
object1 = []         # cells in initial object

try:
   aborted = True
   moveobject()
   aborted = False
finally:
   g.setcursor(oldcursor)
   if aborted:
      # erase object if it moved
      if len(object) > 0: g.putcells(object, 0, 0, 1, 0, 0, 1, "xor")
      if len(oldcells) > 0: g.putcells(oldcells)
      if len(object1) > 0: g.putcells(object1)
   else:
      g.show(" ")
