#!/usr/bin/python

"""Wires

This is a simple game where a set of tiles must be rotated so as to
form a simply connected graph. The game is also known as Net.

Written by Ask Hjorth Larsen, asklarsen@gmail.com

Copyright (C) 2007.

This program 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.

This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
"""

def makesections(strings):
    """
    Concatenate strings with sections between each
    """
    return ''.join([string+'\n\n' for string in strings]).strip()        

# A quick hack so we don't have to rewrite all the docstring text
NAME, QUICK_DESCR, WRITTEN_BY, COPYRIGHT, GPL1, GPL2, GPL3 = \
      __doc__.split('\n\n')

VERSION = 'beta 0.2'
AUTHOR = 'Ask Hjorth Larsen'
EMAIL = 'asklarsen@gmail.com'

INSTRUCTIONS = """
The objective of Wires is to make sure that all tiles are connected by
rotating each tile perspicaciously.

Use the mouse scroll wheel to rotate a tile.

Use the left mouse button to lock a tile, thus labelling it so as to
remember that it is not supposed to be turned around anymore.

Right-click on a tile to flood-fill all tiles connected to it.

Clear all flood-filled tiles by pressing the Clean button in the Game
menu.

If all tiles can be flood-filled with one right-click, then the game
is completed.
"""


import pygtk
pygtk.require('2.0')
import gtk
import random
import sys

class Wires:

    def __init__(self, width, height, seed, shuffle, wrap):
        self.model = Model(width, height, seed, wrap)
        self.board = Board(self.model.tilemap, 55)

        vbox = gtk.VBox()
        vbox.show()

        if wrap:
            arrows = [gtk.Arrow(gtk.ARROW_UP, gtk.SHADOW_OUT),
                      gtk.Arrow(gtk.ARROW_DOWN, gtk.SHADOW_OUT),
                      gtk.Arrow(gtk.ARROW_LEFT, gtk.SHADOW_OUT),
                      gtk.Arrow(gtk.ARROW_RIGHT, gtk.SHADOW_OUT)]

            directions = [(0,1), (0,-1), (1,0), (-1,0)]
            buttons = [self.make_translator_button(dir, arrow)
                       for dir,arrow in zip(directions, arrows)]
            arrowbox = gtk.Toolbar()
            for button, arrow in zip(buttons,arrows):
                arrowbox.append_widget(button, None, None)
                arrow.show()
                button.show()   
                arrowbox.show()
            vbox.pack_start(arrowbox, False, False, 0)

        vbox.pack_end(self.board.table, True, True, 0)

        #self.frame.add(vbox)
        self.component = vbox

        if shuffle:
            self.model.generator.randomize(None)

    def make_translator_button(self, direction, arrow):
        button = gtk.Button()
        button.add(arrow)
        tilemap = self.model.tilemap
        translator = lambda a,b=None : tilemap.translate(*direction)
        button.connect('clicked', translator)
        return button

    def clearmarks(self, widget, data=None):
        for tile in self.model.tilemap.tiles:
            tile.haschanged()

class GUI:
    def __init__(self, wires):
        w = gtk.Window(gtk.WINDOW_TOPLEVEL)
        w.set_title(NAME+' '+VERSION)
        w.connect('destroy', lambda a,b=None : gtk.main_quit())
        w.connect('delete_event', lambda a,b=None : False)
        self.frame = w

        self.wires = wires
        bar = self.make_menu_bar(w)

        vbox = gtk.VBox()
        vbox.show()
        vbox.pack_start(bar, False, False, 0)
        vbox.pack_end(wires.component, False, False, 0)
        w.add(vbox)

    def set_wires(self, wires):
        self.wires = wires
        

    def make_menu_bar(self, window):
        bar = gtk.MenuBar()
        bar.show()

        gameitem, gamemenu = self.makemenu('Game', bar)
        newitem = self.makemenuitem('New', gamemenu)
        cleanitem = self.makemenuitem('Clean', gamemenu, self.wires.clearmarks)
        quititem = self.makemenuitem('Quit', gamemenu,
                                     lambda a,b=None : gtk.main_quit())

        settingsitem, settingsmenu = self.makemenu('Settings', bar)
        prefitem = self.makemenuitem('Preferences', settingsmenu)

        helpitem, helpmenu = self.makemenu('Help', bar)
        instructionsitem = self.makemenuitem('Instructions', helpmenu,
                                             self.show_instructions_dialog)
        aboutitem = self.makemenuitem('About', helpmenu,
                                      self.show_about_dialog)
        licenseitem = self.makemenuitem('License', helpmenu,
                                        self.show_license_dialog)

        return bar

    def makemenuitem(self, name, menu=None, action=None):
        item = gtk.MenuItem(name)
        if action is not None:
            item.connect('activate', action, None)
        item.show()
        if menu is not None:
            menu.append(item)
        return item

    def makemenu(self, name, bar=None):
        menu = gtk.Menu()
        item = gtk.MenuItem(name)
        item.set_submenu(menu)
        item.show()
        if bar is not None:
            bar.append(item)
        return item,menu
        
    def showdialog(self, title, text):
        dialog = gtk.Dialog(title, self.frame)
        label = gtk.Label(text)
        label.show()

        button = gtk.Button('All right then')
        button.show()
        button.connect('clicked', lambda a,b: dialog.destroy(), None)

        dialog.vbox.pack_start(label, True, True, 0)
        dialog.action_area.pack_start(button, True, True, 0)
        
        dialog.show()

    def show_instructions_dialog(self, widget=None, data=None):
        title = 'Instructions'
        text = INSTRUCTIONS
        self.showdialog(title, text)
        
    def show_about_dialog(self, widget=None, data=None):

        title = 'About '+NAME

        text = NAME + ' ' + VERSION + ' by ' +AUTHOR + '.\n\n' + \
               EMAIL + '\n\n' + QUICK_DESCR
        
        self.showdialog(title, text)
        
    def show_license_dialog(self, widget=None, data=None):
        title = 'License'
        text = makesections([COPYRIGHT+' '+AUTHOR+'.', GPL1, GPL2, GPL3])
        self.showdialog(title, text)        
        
class Tile:

    def __init__(self, index=0, x=0, y=0):
        self.index = index
        self.group = index # Used to track connected tiles
        self.x = x
        self.y = y
        self.orientation = 0
        self.connections = [False]*DIRCOUNT
        self.connectioncount = 0
        self.neighbours = [None]*DIRCOUNT
        self.mark = None # Used to fetch connected tiles with flood fill
        self.locked = False
        self.listener = None

    def rotate(self, amount):
        self.orientation = (self.orientation + amount) % DIRCOUNT
        self.group = self.index
        self.haschanged()

    def isconnected(self, direction):
        return self.connections[(direction - self.orientation) % DIRCOUNT]

    def ismutuallyconnected(self, direction):
        return (self.isconnected(direction)
                and self.neighbours[direction].isconnected(direction+2))

    def descriptor(self):
        facing = ''.join([str(i) for i in map(int, self.connections)])
        return ''.join(['Dir:',str(self.orientation),'\n',facing])

    def connect(self, direction):
        if self.isconnected(direction):
            raise Exception('Already connected!')
        self.connectioncount += 1
        self.connections[direction] = True
        nb = self.neighbours[direction]

        nb.connectioncount += 1
        nb.connections[-DIRCOUNT/2+direction] = True
        self.haschanged()
        nb.haschanged()

        if self.group > nb.group:
            self.set_new_group(nb.group)
        elif nb.group > self.group:
            nb.set_new_group(self.group)
        else:
            print '--------------------------------------------'
            print 'WARNING: Tiles from same group connected!'
            print '         Tiles', (self.x,self.y),'and',(nb.x,nb.y)
            print '--------------------------------------------'
        
        return nb

    def markconnected(self, index):
        if self.mark == index:
            return #Already marked
        else:
            self.mark = index
            for i in range(DIRCOUNT):
                if self.ismutuallyconnected(i):
                    self.neighbours[i].markconnected(index)
        self.haschanged()
        self.mark = None

    def toggle_lock(self):
        self.locked = not self.locked
        self.haschanged()

    def set_new_group(self, group):
        if group > self.group:
            raise Exception('Requested group '+group+
                            ', but is group '+self.group)
        self.group = group
        for isconnected, neighbour in zip(self.connections, self.neighbours):
            if isconnected:
                if neighbour.group > group:
                    neighbour.set_new_group(group)

    def haschanged(self):
        if self.listener != None:
            self.listener.tilechanged(self)

class TileMap:

    def __init__(self, width, height, wrap):
        self.width = width
        self.height = height
        self.offsetx = 0
        self.offsety = 0
        self.tiles = [Tile(i, i%width, i/width) for i in range(width*height)]

        if wrap:
            self.get = self.get_wrap
        else:
            self.get = self.get_nowrap

        # Fake tile used to distinguish board edge
        # This is an unbelievably ugly hack which, perhaps, should be replaced
        # before it causes trouble
        self.edge_of_the_world = Tile(-1, -1, -1)
        self.edge_of_the_world.ismutuallyconnected = lambda direction : False

        for tile in self.tiles:
            tile.neighbours[0] = self.get(tile.x + 1, tile.y)
            tile.neighbours[1] = self.get(tile.x, tile.y - 1)
            tile.neighbours[2] = self.get(tile.x - 1, tile.y)
            tile.neighbours[3] = self.get(tile.x, tile.y + 1)

    def get_wrap(self, x, y):
        x, y = self.transform(x,y)
        return self.tiles[ (x % self.width)
                          +(y % self.height) * self.width]

    def get_nowrap(self, x, y):
        if x < 0 or x >= self.width or y < 0 or y >= self.height:
            return self.edge_of_the_world
        return self.tiles[x + self.width*y]

    def transform(self, x, y):
        return (x + self.offsetx, y + self.offsety)

    def translate(self, offsetx, offsety):
        self.offsetx = (self.offsetx + offsetx) % self.width
        self.offsety = (self.offsety + offsety) % self.height

        for tile in self.tiles:
            tile.haschanged()

    def set(self, x, y, tile):
        x, y = self.transform(x,y)
        self.tiles[(x % self.width) + self.width*(y % self.height)] = tile

class Generator:
    
    def __init__(self, seed=None, wrap=False):
        self.seed = seed
        if seed is None:
            seed = random.randint(0,1<<64)
            print 'Seed randomly selected:',seed
        self.wrap = wrap
        self.rand = random.Random(seed)
        self.generate = self.generate_contiguous
        self.edgedata = None
        self.tilemap = None

    def isvalidconnection(self, edge):
        return not edge.vertex1.group == edge.vertex2.group

    def randomize(self, widget, data=None):
        for tile in self.tilemap.tiles:
            tile.orientation = self.rand.randrange(0,4)

    def generate_contiguous(self, width, height):
        tilemap = TileMap(width, height, self.wrap)
        self.tilemap = tilemap
        rand = self.rand
        vertices = list(tilemap.tiles)

        candidatecount = 2*width*height
        maxedges = width*height-1
        edgecandidates = []

        # Add all possible edges to candidate list
        for i in range(width):
            for j in range(height):
                tile1 = tilemap.get(i,j)
                edge1 = Edge(tile1, 0) # connection right
                edge2 = Edge(tile1, 3) # connection down
                edgecandidates.append(edge1)
                edgecandidates.append(edge2)

        if not self.wrap:
            edgecandidates = [edge for edge in edgecandidates
                              if not edge.wraps()]

        rand.shuffle(edgecandidates)
        for edge in edgecandidates:
            if self.isvalidconnection(edge):
                edge.vertex1.connect(edge.direction)
                
        return tilemap


class Edge:
    def __init__(self, vertex1, direction):
        self.direction = direction
        self.vertex1 = vertex1
        self.vertex2 = vertex1.neighbours[direction]

    def wraps(self):
        v1 = self.vertex1
        v2 = self.vertex2
        return abs(v2.x - v1.x) > 1 or abs(v2.y - v1.y) > 1

class EdgeGenerationData:
    """
    A wrapper class used to hold different data during
    map generation.
    """
    def __init__(self, tilemap, vertices, maxedges, edges):
        self.edges = edges
        self.maxedges = maxedges
        self.tilemap = tilemap
        self.vertices = vertices

class Model:
    """
    A wrapper object around a Generator and a TileMap
    """
    
    def __init__(self, width, height, seed=0, wrap=False):
        self.generator = Generator(seed, wrap)
        self.tilemap = self.generator.generate(width,height)

class Board:
    def __init__(self, tilemap, tileSize):
        """
        Creates and returns a gtk.Table, the elements of which are
        appropriately arranged TilePanels, thus representing the
        entire board.
        """

        self.tilemap = tilemap
        self.panels = [None]*tilemap.width*tilemap.height
        table = gtk.Table(rows=tilemap.height, columns=tilemap.width,
                          homogeneous=True)
        self.table = table
        self.mark = -1
        
        for i in range(tilemap.width):
            for j in range(tilemap.height):
                tile = tilemap.get(i,j)
                panel = TilePanel(self, i, j, tileSize)
                self.panels[i + tilemap.width*j] = panel
                panel.show()
                table.attach(panel, i, i+1, j, j+1)
                buttonlistener = ButtonListener(self, i, j, panel)
                tile.listener = self
                panel.set_events(gtk.gdk.SCROLL_MASK)
                panel.connect('button_press_event',
                              buttonlistener.buttonPressed)
                panel.connect('scroll_event', buttonlistener.rotate)

                table.show()

        self.table = table

    def tilechanged(self, tile):
        i = (tile.x - self.tilemap.offsetx) % self.tilemap.width
        j = (tile.y - self.tilemap.offsety) % self.tilemap.width
        self.get_panel(i,j).repaint()

    def get_panel(self, x, y):
        return self.panels[x + self.tilemap.width * y]
        

DIRCOUNT = 4

class TilePanel(gtk.DrawingArea):
    """
    A panel which represents a single tile graphically
    """
    def __init__(self, board, x, y, tileSize):
        gtk.DrawingArea.__init__(self)
        self.board = board
        self.tilemap = board.tilemap
        self.x = x
        self.y = y
        self.set_size_request(tileSize, tileSize)
        self.width = 1
        self.height = 1
        self.pixmap = None
        self.connect('configure_event', self.resize)
        self.connect('expose_event', self.render)
        #self.markedGroup = None

    def resize(self, widget, event):
        """
        Make new backbuffer and repaint it
        """
        x, y, width, height = widget.get_allocation()
        self.width = width
        self.height = height
        self.pixmap = gtk.gdk.Pixmap(widget.window, width, height)
        self.repaint()

    def repaint(self):
        """
        Redraw widget backbuffer according to tile rotation or similar
        """
        (widget, width, height) = (self, self.width, self.height)

        x = width/5
        y = height/5

        style = widget.get_style()
        selectedBG = style.bg_gc[gtk.STATE_SELECTED]
        selectedFG = style.fg_gc[gtk.STATE_SELECTED]
        BG = style.bg_gc[gtk.STATE_NORMAL]
        FG = style.fg_gc[gtk.STATE_NORMAL]
        insensitive = style.bg_gc[gtk.STATE_INSENSITIVE]
        black = style.black_gc
        white = style.white_gc
        
        tile = self.tilemap.get(self.x, self.y)
        locked = tile.locked

        if locked:
            wire = black
            background = insensitive
            component = selectedBG
            marked = selectedBG
        else:
            wire = black
            background = white
            component = selectedBG
            marked = selectedBG

        self.pixmap.draw_rectangle(background, True, 0,0, width, height)

        #If this is a dead end, draw a neat-looking blob
        if tile.connectioncount == 1:
            self.pixmap.draw_rectangle(component, True, x, y, 3*x, 3*y)

        rec = self.pixmap.draw_rectangle

        #Draw wires
        if tile.isconnected(0):
            rec(wire, True, 2*x, 2*y, width-2*x, y)
        if tile.isconnected(1):
            rec(wire, True, 2*x, 0, x, 3*y)
        if tile.isconnected(2):
            rec(wire, True, 0, 2*y, 3*x, y)
        if tile.isconnected(3):
            rec(wire, True, 2*x, 2*y, x, height-2*y)

        #If the tile group is marked, draw smaller, nice-looking lines
        draw_group_connection = True

        if draw_group_connection and self.board.mark == tile.mark:
        #if drawGroupConnection and tile.group == 0:
            dx = x/3
            dy = y/3
            
            if tile.isconnected(0):
                rec(marked, True, 2*x+dx, 2*y+dy, width-2*x, y-2*dy)
            if tile.isconnected(1):
                rec(marked, True, 2*x+dx, 0, x-2*dx, 3*y-dy)
            if tile.isconnected(2):
                rec(marked, True, 0, 2*y+dy, 3*x-dx, y-2*dy)
            if tile.isconnected(3):
                rec(marked, True, 2*x+dx, 2*y+dy, x-2*dx, 3*y-dy)

        widget.queue_draw_area(0,0,width, height)

    def render(self, widget, event):
        """
        Render widget backbuffer to screen
        """
        x, y, width, height = event.area
        widget.window.draw_drawable(widget.get_style().fg_gc[gtk.STATE_NORMAL],
                                    self.pixmap, x, y, x, y, width, height)
        return False    

class ButtonListener:
    """
    Represents listeners which handle clicks/scroll events on some tile
    """
    
    def __init__(self, board, x, y, panel):
        self.panel = panel
        self.board = board
        self.tilemap = board.tilemap
        self.x = x
        self.y = y

    def buttonPressed(self, widget, event, data=None):
        if event.button == 1:
            self.toggleLock(widget, event, data=None)
        elif event.button == 3:
            tile = self.get_tile()
            self.board.mark = tile.index
            tile.markconnected(tile.index)

    def toggleLock(self, widget, event, data=None):
        self.get_tile().toggle_lock()

    def get_tile(self):
        return self.tilemap.get(self.x, self.y)

    def rotate(self, widget, event, data=None):
        tile = self.get_tile()
        if tile.locked:
            return
        
        if event.direction == gtk.gdk.SCROLL_DOWN:
            amount = -1
        else:
            amount = +1

        tile.rotate(amount)
        self.panel.repaint()

    def printInfo(self, widget, event, data=None):
        tile = self.get_tile()
        print tile.x, tile.y, tile.orientation, tile.connections

    def tilechanged(self, tile):
        self.panel.repaint()
    
def main():
    """
    Runs wires after parsing command line arguments
    """
    argc = len(sys.argv)-1    

    if argc >= 1 and sys.argv[1] == '-h':
        print 'Wires',version+'.'
        print
        print 'USAGE: wires.py [<width>] [<height>] [noshuffle] [<seed>]'
        print 
        print 'Starts wires on a width*height board.'
        print 
        print 'With the noshuffle argument, the graph will be generated but'
        print 'left continuous.'
        print 
        print 'If the seed parameter is specified, this will be the random'
        print 'seed used to generate the graph.'
        sys.exit(0)

    width = 6
    height = 6
    seed = None
    shuffle = True
    wrap = False

    if argc > 0:
        width = int(sys.argv[1])
        height = width
    if argc > 1:
        height = int(sys.argv[2])
    if argc > 2:
        args = sys.argv[3:]
        if args.count('noshuffle') > 0:
            shuffle = False
        if args.count('seed') > 0:
            seed = int(args[args.index('seed') + 1])
        if args.count('wrap') > 0:
            wrap = True

    wires = Wires(width, height, seed, shuffle, wrap)
    gui = GUI(wires)
    gui.frame.show()

    gtk.main()

if __name__ == '__main__':
    main()

