#!/usr/bin/env python

#  Python modules for drawing go positions based on ASCII data
#  Copyright (C) 2003  José Geraldo A. Brito Neto
#
#  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 2 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, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
import os, string, re, sha
import Image, ImageDraw, ImageFont

class Diagram:
    def __init__(self):
	self.ok = 0
	self.blackParity = 1
	self.coordinates = 0
	self.leftBorder = 0
	self.rightBorder = 0
	self.upperBorder = 0
	self.lowerBorder = 0
	self.size = 0
	self.rows = 0
	self.columns = 0
	self.title = ''
	self.diagram = []
	self.extendedSyntax = 0
        self.territory = 0
        
    def hash(self):
        return sha.new ('0').hexdigest()



class ASCIIDiagram (Diagram):
    
    def __init__ (self, f):
        Diagram.__init__(self)
	self.header = ''
	self.diagString = ''
        self.allowedCharsRegExp = None

        self.parseHeader(f)
        self.parseDiagram(f)
        self.ok = 1

    def parseHeader(self, f):
        self.header = string.strip(f.readline())
        if (self.header[0:2] != '$$'):
            raise ParseError, 'Line 1 does not begin with $$'

        # Interpretar o cabeçalho do diagrama
        i = 2
        while (i < len(self.header)):
            if (self.header[i] == 'B'):
                # A numeração começa com as pretas
                self.blackParity = 1
            elif (self.header[i] == 'W'):
                # A numeração começa com as brancas
                self.blackParity = 0
            elif (self.header[i] == 'c'):
                # Coordenadas devem ser impressas
                self.coordinates = 1
            elif (self.header[i] == 'x'):
                self.extendedSyntax = 1
            elif (self.header[i] == 't'):
                self.territory = 1
            elif (string.find (string.digits, self.header[i]) >= 0):
                # Tamanho do goban
                j = i+1
                while ((j < len(self.header)) and
                       (string.find (string.digits,self.header[j]) >= 0)):
                    j = j + 1
                self.size = int(self.header[i:j])
                i = j-1
	    elif (string.find(string.whitespace,self.header[i])):
                # O resto é título
                self.title = string.strip(self.header[i:])
                break
            i = i + 1

        # Compilar a expressão regular usada para checar a sintaxe do
        # diagrama
        expr = '[^-|XO.,1234567890abcdefghijklmnopqrstuvwxyzCBWS#@'
        if (self.extendedSyntax):
            expr = expr + 'EFGHIJKLMN'
        if (self.territory):
            expr = expr + '&*'
        self.allowedCharsRegExp = re.compile (expr + ']+')


        
    def parseDiagram(self,f):
        lc = 1
        # Ler o diagrama jogando fora os '$$ '
        line = string.strip(f.readline())
        while (len(line) > 0):
            lc = lc + 1

            if (line[0:3] != '$$ '):
                raise ParseError, 'Line ' + str(lc)+' does not begin with $$'

            # Eliminar os espaços da string, checar a sintaxe e
            # adicioná-la crua ao atributo diagString
            compact_line = string.join(string.split(line[3:]),'')
            match = self.allowedCharsRegExp.search(compact_line)
            if (match != None):
                raise ParseError, 'Invalid character at line ' + \
                   str (lc) + ', position ' + str(match.start() + 1) + \
                   ': \'' + match.group()[0] + '\''
            self.diagString = self.diagString + compact_line + '\n'

            # Tratar bordas horizontais
            if (string.find(compact_line, '-') != -1):
                if (len(self.diagram) != 0):
                    # Oops... Isso força o fim do diagrama
		    self.lowerBorder = 1
                    break
                else:
                    self.upperBorder = 1
                    line = string.strip(f.readline())
                    continue
            # Checar borda vertical esquerda
            if (compact_line[0] == '|'):
                self.leftBorder = 1
                compact_line = compact_line[1:]
            # Checar borda vertical direita
            if (compact_line[-1] == '|'):
                self.rightBorder = 1
                compact_line = compact_line[0:-1]
            self.diagram.append(compact_line)
            line = string.strip(f.readline())

        self.rows = len(self.diagram)
        # Determinar o número de colunas pela linha mais comprida
        self.columns = 0
        for i in range(len(self.diagram)):
            if (self.columns < len(self.diagram[i])):
                self.columns = len(self.diagram[i])
        # Completar as linhas mais curtas com pontos vazios
        for i in range(len(self.diagram)):
            if (len(self.diagram[i]) < self.columns):
                self.diagram[i] = self.diagram[i] + '.' * \
                                  (self.columns - len(self.diagram[i]))

        if (self.rows == 0):
            raise ParseError, 'Diagram has zero rows'
        if (self.columns == 0):
            raise ParseError, 'Diagram has zero columns'
        if (self.size and
            ((self.rows > self.size) or (self.columns > self.size))):
            raise ParseError, 'Diagram too big for its declared size'


    def hash(self):
        if (self.ok != 1):
            raise InvalidDiagram, 'Invalid diagram'
        hasher = sha.new(string.split(self.header,' ')[0])
        hasher.update(self.diagString)
        return hasher.hexdigest()



#class SGFDiagram (Diagram):
#    from sgflib import SGFParser
    
#    def __init__ (self, sgf):
#        Diagram.__init__(self)
#        if (not isinstance (sgf, SGFParser)):
#            raise InvalidDiagram, 'Not a SGFParser instance'
        
#        self.ok = 1
        
#    def hash (self):
#        pass


class PNGRenderer:

    cellHeight = 24
    cellWidth = 24
    fontFile = "lub12.pil"
    horizontalCoords = 'abcdefghjklmnopqrst'
    
    #
    # Métodos
    #
    def __init__ (self, dia):
        if ((not isinstance (dia, Diagram)) or (dia.ok != 1)):
            raise InvalidDiagramException, 'Invalid diagram'
        self.dia = dia
    
    def render(self,f):
        # Criar a imagem e desenhar as coordenadas
        dia = self.dia
        im = Image.new ("RGB", (self.cellWidth*dia.columns+2,
                                self.cellHeight*dia.rows+2),
                        (255,255,255))
        gc = ImageDraw.Draw(im)
        font = ImageFont.load (self.fontFile)
        hcw = self.cellWidth/2
        hch = self.cellHeight/2
        gobanX = hcw + 1
        gobanY = hch + 1
        for i in range(dia.rows):
            y = gobanY + i*self.cellHeight
            for j in range(dia.columns):
                x = gobanX + j*self.cellWidth
                if (dia.diagram[i][j] == 'O'):
                    self.drawWhiteStone(gc,x,y);
                elif (dia.diagram[i][j] == 'X'):
                    self.drawBlackStone(gc,x,y);
                elif (dia.diagram[i][j] == '.'):
                    self.drawEmptyPoint(gc, i, j, x, y)
                elif (dia.diagram[i][j] == ','):
                    self.drawEmptyPoint(gc, i, j, x, y)
                    hoshiXRadius = int(round(self.cellWidth*0.1))
                    hoshiYRadius = int(round(self.cellHeight*0.1))
                    gc.ellipse ((x-hoshiXRadius, y-hoshiYRadius,
                                 x+hoshiXRadius, y+hoshiYRadius),
                                outline = (0,0,0), fill=(0,0,0))
                elif (string.find (string.digits, dia.diagram[i][j]) >= 0):
                    s = dia.diagram[i][j]
                    n = int(s)
                    if (n == 0): n = 10
                    self.drawNumberedPlay (gc, font, x, y, n) 
                elif (dia.diagram[i][j] == 'C'):
                    self.drawEmptyPoint(gc, i, j, x, y)
                    xRadius = int(round(self.cellWidth*0.2))
                    yRadius = int(round(self.cellHeight*0.2))
                    gc.ellipse ((x-xRadius, y-yRadius,
                                 x+xRadius, y+yRadius),
                                outline = (255,0,0))
                elif (dia.diagram[i][j] == 'B'):
                    self.drawBlackStone (gc,x,y);
                    xRadius = int(round(self.cellWidth*0.2))
                    yRadius = int(round(self.cellHeight*0.2))
                    gc.ellipse ((x-xRadius, y-yRadius,
                                 x+xRadius, y+yRadius),
                                outline = (255,0,0))
                elif (dia.diagram[i][j] == 'W'):
                    self.drawWhiteStone (gc,x,y);
                    xRadius = int(round(self.cellWidth*0.2))
                    yRadius = int(round(self.cellHeight*0.2))
                    gc.ellipse ((x-xRadius, y-yRadius,
                                 x+xRadius, y+yRadius),
                                outline = (255,0,0))
                elif (dia.diagram[i][j] == 'S'):
                    self.drawEmptyPoint(gc, i, j, x, y)
                    dx = int(round(self.cellWidth*0.2))
                    dy = int(round(self.cellHeight*0.2))
                    gc.rectangle ((x-dx,y-dy,x+dx,y+dy),
                                  outline = (255,0,0))
                elif (dia.diagram[i][j] == '#'):
                    self.drawBlackStone (gc,x,y);
                    dx = int(round(self.cellWidth*0.2))
                    dy = int(round(self.cellHeight*0.2))
                    gc.rectangle ((x-dx,y-dy,x+dx,y+dy),
                                  outline = (255,0,0))
                elif (dia.diagram[i][j] == '@'):
                    self.drawWhiteStone (gc,x,y);
                    dx = int(round(self.cellWidth*0.2))
                    dy = int(round(self.cellHeight*0.2))
                    gc.rectangle ((x-dx,y-dy,x+dx,y+dy),
                                  outline = (255,0,0))
                elif (string.find(string.lowercase, dia.diagram[i][j]) != -1):
                    self.drawEmptyPoint(gc, i, j, x, y)
                    s = dia.diagram[i][j]
                    (txt_width, txt_height) = gc.textsize (s, font=font)
                    (htw,hth) = (txt_width/2, txt_height/2)
                    (hrw,hrh) = (int(round(txt_width*0.6)),
                                 int(round(txt_height*0.6)))
                    gc.rectangle ((x-hrw-1,y-hrw-1,x+hrw+1,y+hrw+1),
                                  fill=(255,255,255), outline=(255,255,255))
                    gc.text ((x-htw, y-hth), s, font=font, fill=(0,0,0))
		else:
		    if (dia.extendedSyntax):
			n = string.find ('EFGHIJKLMN', dia.diagram[i][j])
			if (n > -1):
			    self.drawNumberedPlay (gc, font, x, y, n+11)
		    if (dia.territory):
			if (dia.diagram[i][j] == '&'):
			    self.drawEmptyPoint (gc, i, j, x, y)
			    hw = int(round(0.15*self.cellWidth))
			    hh = int(round(0.15*self.cellHeight))
			    hbw = int(round(0.2*self.cellWidth))
			    hbh = int(round(0.2*self.cellHeight))
			    gc.rectangle ((x-hbw,y-hbh,x+hbw,y+hbh),
			                  fill=(255,255,255), 
					  outline=(255,255,255))
			    gc.rectangle ((x-hw,y-hh,x+hw,y+hh),
			                  fill=(255,255,255), 
					  outline=(0,0,0))
			elif (dia.diagram[i][j] == '*'):
			    self.drawEmptyPoint (gc, i, j, x, y)
			    hw = int(round(0.15*self.cellWidth))
			    hh = int(round(0.15*self.cellHeight))
			    hbw = int(round(0.2*self.cellWidth))
			    hbh = int(round(0.2*self.cellHeight))
			    gc.rectangle ((x-hbw,y-hbh,x+hbw,y+hbh),
			                  fill=(255,255,255), 
					  outline=(255,255,255))
			    gc.rectangle ((x-hw,y-hh,x+hw,y+hh),
					  fill=(0,0,0), 
					  outline=(0,0,0))

        # Desenhar as coordenadas
        im = self.drawCoordinates (im, font)
        # Salvar a imagem no arquivo
        im.save (f, "PNG")
    
    def drawEmptyPoint (self, gc, i, j, x, y):
        hcw = self.cellWidth/2
        hch = self.cellHeight/2
        dia = self.dia
        x0 = x - hcw
        x1 = x + hcw
        y0 = y - hch
        y1 = y + hch
        if ((i != 0) or (dia.upperBorder == 0)):
            gc.line ((x,y0,x,y), fill=(0,0,0))
        if ((i < dia.rows-1) or (dia.lowerBorder == 0)):
            gc.line ((x,y,x,y1), fill=(0,0,0))
        if ((j != 0) or (dia.leftBorder == 0)):
            gc.line ((x0,y,x,y), fill=(0,0,0))
        if ((j < dia.columns-1) or (dia.rightBorder == 0)):
            gc.line ((x,y,x1,y), fill=(0,0,0))

    def drawWhiteStone (self,gc,x,y):
        (hcw,hch) = (self.cellWidth/2, self.cellHeight/2)
        gc.ellipse((x-hcw,y-hch,x+hcw,y+hch),
                   fill=(255,255,255), outline=(0,0,0))

    def drawBlackStone (self,gc,x,y):
        (hcw,hch) = (self.cellWidth/2, self.cellHeight/2)
        gc.ellipse((x-hcw,y-hch,x+hcw,y+hch),
                   fill=(0,0,0), outline=(0,0,0))

    def drawNumberedPlay(self, gc, font, x, y, n):
        if (n % 2 == self.dia.blackParity):
            self.drawBlackStone(gc,x,y)
            txt_color = (255,255,255)
        else:
            self.drawWhiteStone(gc,x,y)
            txt_color = (0,0,0)
        s = str(n)
        (txt_width, txt_height) = gc.textsize (s, font=font)
        gc.text ((x-txt_width/2, y-txt_height/2), s,
                 font=font, fill=txt_color)

    def drawCoordinates (self, im, font):
        (imageWidth, imageHeight) = im.size
        gobanX = 0
        gobanY = 0
        dia = self.dia
        # Criar as imagens das coordenadas 
        if (dia.coordinates and (dia.leftBorder or dia.rightBorder) and
	    (dia.lowerBorder or dia.upperBorder)):
	    # Coordenadas verticais
	    verticalCoords = self.drawVerticalCoords(font)
            if (dia.leftBorder and dia.rightBorder):
                imageWidth = imageWidth + 2 * verticalCoords.size[0]
            else:
                imageWidth = imageWidth + verticalCoords.size[0]
            if (dia.leftBorder): gobanX = verticalCoords.size[0]
	    # Coordenadas horizontais
            horizontalCoords = self.drawHorizontalCoords(font)
            if (dia.lowerBorder and dia.upperBorder):
                imageHeight = imageHeight + 2*horizontalCoords.size[1]
            else:
                imageHeight = imageHeight + horizontalCoords.size[1]
            if (dia.upperBorder): gobanY = horizontalCoords.size[1]
        # Criar a nova imagem
        newIm = Image.new ("RGB",
                           (int(round(1.05*imageWidth)),
                            int(round(1.05*imageHeight))),
                           (255,255,255))
        gobanX = gobanX + (newIm.size[0] - imageWidth)/2
        gobanY = gobanY + (newIm.size[1] - imageHeight)/2
        # Desenhar o goban
        newIm.paste (im, (gobanX, gobanY))
        # Desenhar as coordenadas
        if (dia.coordinates):
            if (dia.upperBorder):
                newIm.paste (horizontalCoords,
                             (gobanX, gobanY-horizontalCoords.size[1] - 1))
            if (dia.lowerBorder):
                newIm.paste (horizontalCoords,
                             (gobanX, gobanY+self.cellHeight*dia.rows + 2))
            if (dia.leftBorder):
                newIm.paste (verticalCoords,
                             (gobanX-verticalCoords.size[0] - 1, gobanY))
            if (dia.rightBorder):
                newIm.paste (verticalCoords,
                             (gobanX+self.cellWidth*dia.columns + 2, gobanY))
        return (newIm)

    def drawHorizontalCoords (self,font):
        dia = self.dia
        if (not (dia.leftBorder or dia.rightBorder)): return None
        # Determinar a primeira e a última coordenada
        first = -1
        last = -1
        if (dia.leftBorder and dia.rightBorder):
            first = 0
            last = dia.columns-1
        elif (dia.leftBorder):
            first = 0
            last = dia.columns-1
        else:
            if (dia.size and (dia.columns <= dia.size)): last=dia.size-1
            else: last = dia.columns-1
            first = last - dia.columns + 1
        # Montar as strings e determinar a altura do bitmap
        im = Image.new ("RGB", (10,10))
        gc = ImageDraw.Draw(im)
        h_min = 0
        sl = []
        size = []
        n = len(self.horizontalCoords)
        for i in range (first,last+1):
            j = i / n
            s = self.horizontalCoords[i%n]
            while (j > 0):
                s = self.horizontalCoords[j % n] + s
                j = j / n
            sl.append(s)
            (w,h) = gc.textsize (s, font=font)
            size.append((w,h))
            if (h > h_min): h_min = h
        im = Image.new ("RGB", (self.cellWidth*dia.columns, h_min),
                        (255,255,255))
        gc = ImageDraw.Draw(im)
        for i in range (last-first+1):
            x = (i)*self.cellWidth + self.cellWidth/2 - size[i][0]/2
            y = (h_min - size[i][1])/2
            gc.text ((x,y), sl[i], font=font, fill=(0,0,0))
        return im
        
    def drawVerticalCoords (self,font):
        dia = self.dia
        if (not (dia.upperBorder or dia.lowerBorder)): return None
        # Determinar a primeira e a última coordenada
        first = -1
        last = -1
        if (dia.upperBorder and dia.lowerBorder):
            first = 0
            last = dia.rows-1
        elif (dia.lowerBorder):
            first = 0
            last = dia.rows-1
        else:
            if (dia.size and (dia.rows <= dia.size)): last=dia.size-1
            else: last = dia.rows-1
            first = last - dia.rows + 1
        # Montar as strings e determinar a largura do bitmap
        im = Image.new ("RGB", (10,10))
        gc = ImageDraw.Draw(im)
        w_min = 0
        sl = []
        size = []
        for i in range (first,last+1):
            s = str(i+1)
            sl.append(s)
            (w,h) = gc.textsize (s, font=font)
            size.append((w,h))
            if (w > w_min): w_min = w
        sl.reverse()
        size.reverse()
        im = Image.new ("RGB", (w_min, self.cellHeight*dia.rows),
                        (255,255,255))
        gc = ImageDraw.Draw(im)
        for i in range (last-first+1):
            x = (w_min - size[i][0])/2
            y = (i)*self.cellHeight+self.cellHeight/2-size[i][1]/2
            gc.text ((x,y), sl[i], font=font, fill=(0,0,0))
        return im



class SGFRenderer:

    coords = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
    
    def __init__ (self, dia):
        if ((not isinstance (dia, Diagram)) or (dia.ok != 1)):
            raise InvalidDiagramException, 'Invalid diagram'
        self.dia = dia
    
    def render(self):
        sgf = '('
        dia = self.dia

        if (dia.size): size = dia.size
        else:
            dh = dia.rows + 2 - dia.lowerBorder - dia.upperBorder
            dw = dia.columns + 2 - dia.leftBorder - dia.rightBorder
            size = max (dw,dh)
	    if (size <= 19): size = 19
	    
        sgf = sgf + ';SZ[%d];' % size

        if (dia.leftBorder): x_origin = 0
        elif (dia.rightBorder): x_origin = size - dia.columns
        else: x_origin = 1

        if (dia.upperBorder): y_origin = 0
        elif (dia.lowerBorder): y_origin = size - dia.rows
        else: y_origin = 1

        moves = {}
        max_move = 0

        for i in range(dia.rows):
            y = self.coords[y_origin + i]
            for j in range(dia.columns):
                x = self.coords[x_origin + j]
                
                if (dia.diagram[i][j] == 'O'):
                    sgf = sgf + 'AW[' + x + y + ']'
                elif (dia.diagram[i][j] == 'X'):
                    sgf = sgf + 'AB[' + x + y + ']'
                elif (string.find (string.digits, dia.diagram[i][j]) >= 0):
                    n = int (dia.diagram[i][j])
                    if (n == 0): n = 10
                    moves['%d' % n] = x + y
                    if (n > max_move): max_move = n
                elif (dia.diagram[i][j] == 'C'):
                    sgf = sgf + 'CR[' + x + y + ']'
                elif (dia.diagram[i][j] == 'B'):
                    sgf = sgf + 'AB[' + x + y + ']'
                    sgf = sgf + 'CR[' + x + y + ']'
                elif (dia.diagram[i][j] == 'W'):
                    sgf = sgf + 'AW[' + x + y + ']'
                    sgf = sgf + 'CR[' + x + y + ']'
                elif (dia.diagram[i][j] == 'S'):
                    sgf = sgf + 'SQ[' + x + y + ']'
                elif (dia.diagram[i][j] == '#'):
                    sgf = sgf + 'AB[' + x + y + ']'
                    sgf = sgf + 'SQ[' + x + y + ']'
                elif (dia.diagram[i][j] == '@'):
                    sgf = sgf + 'AW[' + x + y + ']'
                    sgf = sgf + 'SQ[' + x + y + ']'
                elif (string.find(string.lowercase, dia.diagram[i][j]) != -1):
                    sgf = sgf + 'LB[' + x + y + ':' + dia.diagram[i][j] + ']'
                elif (dia.extendedSyntax):
                    n = string.find ('EFGHIJKLMN', dia.diagram[i][j])
                    if (n > -1):
                        n = n + 11
                        moves['%d' % n] = x + y
                        if (n > max_move): max_move = n
                elif (dia.territory):
                    if (dia.diagram[i][j] == '&'):
                        pass
                    elif (dia.diagram[i][j] == '*'):
                        pass

        if (max_move > 0):
            for i in range(1,max_move+1):
                try:
                    xy = moves['%d' % i]
                    sgf = sgf + ';MN[%d]' % i
                    if (i % 2 == dia.blackParity):
                        sgf = sgf + 'B[' + xy + ']'
                    else:
                        sgf = sgf + 'W[' + xy + ']'
                except (KeyError):
                    pass
                
        sgf = sgf + ')'
        return sgf


class HTMLRenderer:
    def __init__ (self, dia, repoDir, repoURL=''):
	if ((not isinstance (dia, Diagram)) or (dia.ok != 1)):
	    raise InvalidDiagramException, 'Invalid diagram'
        self.dia = dia
        self.repoDir = repoDir
        if (repoURL == ''): self.repoURL = repoDir
        else: self.repoURL = repoURL
        
    def render (self):
        h = self.dia.hash()
        pngname = self.repoDir + '/' + h + ".png"
        sgfname = self.repoDir + '/' + h + ".sgf"
        try: os.stat(pngname)
        except (os.error):
            PNGRenderer(self.dia).render(pngname)
        try: os.stat(sgfname)
        except (os.error):
            f = open(sgfname,'w')
            f.write(SGFRenderer(self.dia).render() + '\n')
            f.close

        pngname = self.repoURL + '/' + h + ".png"
        sgfname = self.repoURL + '/' + h + ".sgf"
	return \
    "<table border=1 align=LEFT width=\"5%\" style=\"margin-right:2em\">" + \
    "<tr><td align=center>" + \
    "<b>Diagrama: </b>" + self.dia.title + \
    "</td></tr>" + \
    "<tr><td align=center>" + \
    "<a href=\"" + sgfname + "\">" + \
    "<img src=\"" + pngname + "\"" + "alt=\"Diagrama\" border=0" + ">" + \
    "</a>" + \
    "</td></tr>" + \
    "</table>"
	   
class InvalidDiagramException (Exception):
    def __init__ (self, value):
        self.value = value
    def __str__ (self):
        return `self.value`

class ParseError (Exception):
    def __init__ (self, value):
        self.value = value
    def __str__ (self):
        return `self.value`

