# -*- coding: utf-8 -*-
"""
"""

__author__ = 'Robert Ancell <bob27@users.sourceforge.net>'
__license__ = 'GNU General Public License Version 2'
__copyright__ = 'Copyright 2005-2006  Robert Ancell'

__all__ = ['SANConverter']

# Examples of SAN moves:
#
# f4      (pawn move to f4)
# fxg3    (pawn on file f takes opponent on g3)
# Qh5     (queen moves to h5)
# Qh5+    (queen moves to h5 and puts opponent into check)
# Ned4    (knight on file e moves to d4)
# gxh8=Q# (pawn on g7 takes opponent in h8 promotes to queen and puts oponent into checkmate (smooth!))

# Notation for takes
SAN_TAKE   = 'x'

# Castling moves
SAN_CASTLE_SHORT = 'O-O'
SAN_CASTLE_LONG  = 'O-O-O'

RANKS = 'abcdefgh'
FILES = '12345678'

# Suffixes
SAN_PROMOTE   = '='

class Error(Exception):
    """
    """
    pass

class SANConverter:
    """
    
    Define file and rank
    """
    
    # Piece colours
    WHITE = 'White'
    BLACK = 'Black'
    
    # SAN piece types
    PAWN   = 'P'
    KNIGHT = 'N'
    BISHOP = 'B'
    ROOK   = 'R'
    QUEEN  = 'Q'
    KING   = 'K'
    __pieceTypes = PAWN + KNIGHT + BISHOP + ROOK + QUEEN + KING
    
    # Valid promotion types
    __promotionTypes = PAWN + KNIGHT + BISHOP + ROOK + QUEEN
    
    # Move results
    CHECK     = '+'
    CHECKMATE = '#'
    
    def __init__(self):
        """Constructor"""
        pass
    
    # Methods to extend
    
    def getPiece(self, location):
        """Get a piece from the chess board.
        
        'location' is the location to get the piece from (string, e.g. 'a1', h8').
        
        Return a tuple containing (colour, type) or None if no piece at this location.
        """
        return None
    
    def testMove(self, colour, start, end, promotionType, allowSuicide = False):
        """Test if a move is valid.
        
        'colour' is the colour of the player making the move (self.WHITE or self.BLACK).
        'start' is the board location to move from (string, e.g. 'a1', 'h8').
        'end' is the board location to move to (string, e.g. 'a1', 'h8').
        'promotionType' is the piece type to promote to (self.[PAWN|KNIGHT|BISHOP|ROOK|QUEEN]).
        'allowSuicide' is a flag to show if the move should be disallowed (False) or
                       allowed (True) if it would put the moving player into check.

        Return False if the move is dissallowed or
               self.CHECK if the move puts the opponent into check or
               self.CHECKMATE if the move puts the opponent into checkmate or
               True if the move is allowed and does not put the opponent into check.
        """
        pass
    
    def decode(self, colour, san):
        """Decode a SAN move.
        
        'colour' is the colour of the player making the move (self.WHITE or self.BLACK).
        'san' is the SAN description of the move (string).
        
        Returns the move this SAN describes in the form (start, end, promotionType).
        'start' is the square to move from (string, e.g. 'a1', 'h8').
        'end' is the square to move to (string, e.g. 'a1', 'h8').
        'promotionType' is the piece to promote to (self.[KNIGHT|BISHOP|ROOK|QUEEN]).
        If the move is invalid then an Error expection is raised.
        """
        copy = san[:]
        
        # Look for check hints
        expectedResult = True
        if copy[-1] == self.CHECK or copy[-1] == self.CHECKMATE:
            expectedResult = copy[-1]
            copy = copy[:-1]

        # Extract promotions
        promotionType = self.QUEEN
        if copy[-2] == SAN_PROMOTE:
            promotionType = copy[-1]
            copy = copy[:-2]
            if self.__promotionTypes.find(promotionType) < 0:
                raise Error("Error decoding '%s', Invalid promotion type %s" % (repr(san), repr(promotionType)))
            
        # Some people miss out the '='
        elif self.__promotionTypes.find(copy[-1]) >= 0:
            promotionType = copy[-1]
            copy = copy[:-1]

        # Check for castling moves
        if colour is self.WHITE:
            baseFile = '1'
        else:
            baseFile = '8'
        # FIXME: Update moveResult and compare against expectedResult
        if copy == SAN_CASTLE_SHORT:
            return ('e' + baseFile, 'g' + baseFile, expectedResult, promotionType)
        elif copy == SAN_CASTLE_LONG:
            return ('e' + baseFile, 'c' + baseFile, expectedResult, promotionType)

        # Get the destination (the last two characters before the suffix)
        end = copy[-2:]
        copy = copy[:-2]
        if RANKS.find(end[0]) < 0 or FILES.find(end[1]) < 0:
            raise Error("Error decoding '%s', Invalid destination type %s" % (repr(san), repr(end)))

        # Check if is a take move (use try in case there are no more characters)
        isTake = False
        try:
            if copy[-1] == SAN_TAKE:
                isTake = True
                copy = copy[:-1]
        except:
            pass

        # The first character is the piece type (or pawn if not specified)
        pieceType = self.PAWN
        if len(copy) > 0:
            if self.__pieceTypes.find(copy[0]) >= 0:
                pieceType = copy[0]
                copy = copy[1:]

        # Get the rank of the source piece (if supplied)
        rank = None
        if len(copy) > 0:
            if RANKS.find(copy[0]) >= 0:
                rank = copy[0]
                copy = copy[1:]

        # Get the file of the source piece (if supplied)
        file = None
        if len(copy) > 0:
            if FILES.find(copy[0]) >= 0:
                file = copy[0]
                copy = copy[1:]

        # There should be no more characters
        if len(copy) != 0:
            raise Error('Error decoding %s, Unexpected extra characters %s' % (repr(san), repr(end)))

        # If have both rank and file for source then we have the move completely defined
        moveResult = None
        move = None
        if rank is not None and file is not None:
            start = rank + file
            moveResult = self.testMove(colour, start, end, promotionType = promotionType)
            move = (start, end)
        else:
            # Try and find a piece that matches the source one
            if file is None:
                fileRange = FILES
            else:
                fileRange = file
            if rank is None:
                rankRange = RANKS
            else:
                rankRange = rank
            
            for file in fileRange:
                for rank in rankRange:
                    start = rank + file
                
                    # Only test our pieces
                    piece = self.getPiece(start)
                    if piece is None:
                        continue
                    if piece[0] != colour or piece[1] != pieceType:
                        continue

                    # If move is valid this is a possible move
                    # FIXME: Check the crafty case in reverse (i.e. suicidal moves)
                    result = self.testMove(colour, start, end, promotionType = promotionType)
                    if result is False:
                        continue
                
                    # Multiple matches
                    if moveResult is not None:
                        raise Error('Error decoding %s, Move is ambiguous, at least %s and %s are possible' % (repr(san), repr(move), repr([start, end])))
                    moveResult = result
                    move = [start, end]

        # Failed to find a match
        if moveResult is None:
            raise Error('Error decoding %s, Not a valid move' % repr(san))

        return (move[0], move[1], expectedResult, promotionType)

    def encode(self, start, end, isTake = False, promotionType = QUEEN):
        """Convert glChess co-ordinate move to SAN notation.

        'start' is the square to move from (string, e.g. 'a1', 'h8').
        'end' is the square to move to (string, e.g. 'a1', 'h8').
        'promotionType' is the piece used for pawn promotion (if necessary).

        Return the move in SAN notation or None if unable to convert.
        """
        piece = self.getPiece(start)
        if piece is None:
            raise Error('Encode error, no piece to move at %s' % repr(start))
        (pieceColour, pieceType) = piece

        # Test the move is valid
        if self.testMove(pieceColour, start, end, promotionType) is False:
            raise Error('Encode error, move %s%s is invalid' % (start, end))

        # Check for castling
        if pieceType is self.KING:
            # Castling
            if pieceColour is self.WHITE:
                baseFile = '1'
            else:
                baseFile = '8'
            shortCastle = ('e' + baseFile, 'g' + baseFile)
            longCastle = ('e' + baseFile, 'c' + baseFile)

            san = None
            if (start, end) == shortCastle:
                san = SAN_CASTLE_SHORT
            elif (start, end) == longCastle:
                san = SAN_CASTLE_LONG
            if san is not None:
                result = self.testMove(pieceColour, start, end, promotionType = promotionType, allowSuicide = True)
                if result is self.CHECK:
                    san += self.CHECK
                elif result is self.CHECKMATE:
                    san += self.CHECKMATE
                return san

        # Try and describe this piece with the minimum of information
        file = '?'
        rank = '?'
    
        # Pawns always explicitly provide rank when taking
        if pieceType is self.PAWN and isTake:
            rank = start[0]

        # First try no rank or file, then just file, then just rank, then both
        result = self.__isUnique(pieceColour, pieceType, rank + file, end, promotionType)
        if result is None:
            # Try with rank
            rank = start[0]
            file = '?'
            result = self.__isUnique(pieceColour, pieceType, rank + '?', end, promotionType)

            if result is None:
                # Try with file
                rank = '?'
                file = start[1]
                result = self.__isUnique(pieceColour, pieceType, '?' + file, end, promotionType)

                if result is None:
                    # Try with rank and file
                    result = self.__isUnique(pieceColour, pieceType, rank + file, end, promotionType)
            
                    # This move is illegal
                    if result is None:
                        raise Error('Encode error, unable to find unique move for %s%s' % (start, end))

        # Store the piece that is being moved, note pawns are not marked
        san = ''
        if pieceType is not self.PAWN:
            san += pieceType

        # Disambiguations
        if rank != '?':
            san += rank
        if file != '?':
            san += file

        # Mark if taking a piece
        if isTake:
            san += SAN_TAKE

        # Write target co-ordinate
        san += end

        # If a pawn promotion record the type
        if pieceColour is self.WHITE:
            promotionFile = '8'
        else:
            promotionFile = '1'
        if pieceType == self.PAWN and end[1] == promotionFile:
            san += SAN_PROMOTE + promotionType

        # Record if this is a check/checkmate move
        if result is self.CHECK:
            san += self.CHECK
        elif result is self.CHECKMATE:
            san += self.CHECKMATE

        return san
    
    def __isUnique(self, colour, pieceType, start, end, promotionType = QUEEN):
        """Test if a move is unique.
    
        'colour' is the piece colour being moved. (self.WHITE or self.BLACK).
        'pieceType' is the type of the piece being moved (self.[PAWN|KNIGHT|BISHOP|ROOK|QUEEN|KING]).
        'start' is the start location of the move (tuple (file, rank). rank and file can be None).
        'end' is the end point of the move (tuple (file,rank)).
        'promotionType' is the piece type to promote pawns to (self.[PAWN|KNIGHT|BISHOP|ROOK|QUEEN]).
    
        Return the result of self.testMove() if a unique move is found otherwise None.
        """
        lastResult = None
    
        # Work out what ranges to iterate over
        if start[0] == '?':
            rankRange = RANKS
        else:
            rankRange = start[0]
        if start[1] == '?':
            fileRange = FILES
        else:
            fileRange = start[1]
    
        for file in fileRange:
            for rank in rankRange:
                # Check if there is a piece of this type and colour at this location
                p = self.getPiece(rank + file)
                if p is None:
                    continue
                if p[1] != pieceType or p[0] != colour:
                    continue

                # If move is valid this is a possible move
                # NOTE: We check moves that would be suicide for us otherwise crafty claims they
                # are ambiguous, the PGN specification says we don't need to disambiguate if only
                # one non-suicidal move is available.
                # (8.2.3.4: Disambiguation) */
                result = self.testMove(colour, rank + file, end, promotionType = promotionType, allowSuicide = True)
                if result is not False:
                    # If multiple matches then not unique (duh!)
                    if lastResult != None:
                        return None
                    lastResult = result

        # Return the result of the move
        return lastResult

if __name__ == '__main__':
    
    import chess_board
    
    class TestConverter(SANConverter):
        """
        """
        
        __colourToSAN = {chess_board.WHITE: SANConverter.WHITE, chess_board.BLACK: SANConverter.BLACK}
        __sanToColour = {}
        for (a, b) in __colourToSAN.iteritems():
            __sanToColour[b] = a
        
        __typeToSAN = {chess_board.PAWN: SANConverter.PAWN,
                       chess_board.KNIGHT: SANConverter.KNIGHT,
                       chess_board.BISHOP: SANConverter.BISHOP,
                       chess_board.ROOK: SANConverter.ROOK,
                       chess_board.QUEEN: SANConverter.QUEEN,
                       chess_board.KING: SANConverter.KING}
        __sanToType = {}
        for (a, b) in __typeToSAN.iteritems():
            __sanToType[b] = a
        
        __board = None
        
        def __init__(self, board):
            self.__board = board
            
        def testEncode(self, start, end):
            print str((start, end)) + ' => ' + str(self.encode(start, end))
            
        def testDecode(self, colour, san):
            try:
                result = self.decode(colour, san)
                print san.ljust(7) + ' => ' + str(result)
            except Error, e:
                print san.ljust(7) + ' !! ' + str(e)
        
        def getPiece(self, file, rank):
            """Called by SANConverter"""
            piece = self.__board.getPiece((file, rank))
            if piece is None:
                return None
            return (self.__colourToSAN[piece.getColour()], self.__typeToSAN[piece.getType()])

        def testMove(self, colour, start, end, promotionType, allowSuicide = False):
            """Called by SANConverter"""
            moveResult = self.__board.testMove(self.__sanToColour[colour], ((start, end)), self.__sanToType[promotionType], allowSuicide)
            
            return {chess_board.MOVE_RESULT_ILLEGAL: False,
                    chess_board.MOVE_RESULT_ALLOWED: True,
                    chess_board.MOVE_RESULT_OPPONENT_CHECK: self.CHECK,
                    chess_board.MOVE_RESULT_OPPONENT_CHECKMATE: self.CHECKMATE}[moveResult]

    b = chess_board.ChessBoard()
    c = TestConverter(b)
    
    print b
    c.testEncode((1,1), (1,2))
    c.testEncode((1,0), (2,2))
    
    c.testDecode(c.WHITE, 'c3')    # Simple pawn move
    c.testDecode(c.WHITE, 'Pc3')   # Explicit pawn move
    c.testDecode(c.WHITE, 'c4')    # Pawn march
    c.testDecode(c.WHITE, 'Nc3')   # Non-pawn move
    c.testDecode(c.WHITE, 'Qd3')   # Invalid move
    c.testDecode(c.WHITE, 'Qd3=X') # Invalid promotion type
    c.testDecode(c.WHITE, 'x3')    # Invalid destination
    c.testDecode(c.WHITE, 'ic3')   # Extra characters
    # TODO: Ambiguous move
    print b
    