'''
|BuildFEN| 
The FEN-Builder is an extra tool which can be started from the main window of the chess GUI.
It allows to populate all items of the position by using the Forsyth-Edwards Notation (`FEN`_).
It consists of 7 parts:
    * *Board* displaying the current position
    * *Pieces* displaying the pieces to be placed and the X piece for deletion
    * *Next to Move* indicating the next player to move
    * *Castling* indicating the rights to castle
    * *En Passant* displaying the target square of an en passant move
    * *Move* indicating the number of full-moves of the current game
    * *Clock* indicating the number of half-moves since the last capture or pawn advance, see fifty-move rule (`FMR`_)
    
.. |BuildFEN| image:: buildFEN.png
  :width: 800
  :alt: Build FEN Window
.. _FEN: https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation
.. _FMR: https://en.wikipedia.org/wiki/Fifty-move_rule
'''
from typing import Optional,  Callable,  List
import sys, os, os.path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from MzChess import Position
try:
 from PyQt6 import QtWidgets, QtGui, QtCore
 from PyQt6 import uic
 import PyQt6.QtSvgWidgets
 import PyQt6.QtCharts
except:
 try:
  from PyQt5 import QtWidgets, QtGui, QtCore
  from PyQt5 import uic
  import PyQt5.QtSvg
  import PyQt5.QtChart
 except:
  raise ModuleNotFoundError('Neither the required PyQt6 nor PyQt5 modules installed')
import chess, chess.pgn
import MzChess
import AboutDialog
from installLeipFont import installLeipFont
[docs]class BuildFenClass(QtWidgets.QMainWindow):
 '''The *chessboard* is based on Qt's QGraphicsView.
 '''
[docs] def __init__(self, parent = None) -> None:
  super(BuildFenClass, self).__init__(parent)
  installLeipFont()
  fileDirectory = os.path.dirname(os.path.abspath(__file__))
  uic.loadUi(os.path.join(fileDirectory, 'buildFen.ui'), self)
  self.pgm = 'FEN-Builder'
  self.version = MzChess.__version__
  self.dateString = MzChess.__date__
  self.helpIndex = QtCore.QUrl('https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation')
 
  sbText = self.statusBar().font()
  sbText.setPointSize(12)
  self.position = MzChess.Position()
  self.msgBox = QtWidgets.QMessageBox()
  self.msgBox.setIcon(QtWidgets.QMessageBox.Icon.Critical)
  self.msgBox.setWindowTitle("Error ...")
  self.infoLabel = QtWidgets.QLabel()
  self.infoLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft)
  self.infoLabel.setFont(sbText)
  self.statusBar().addPermanentWidget(self.infoLabel, 150)
 
  sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Preferred)
  sizePolicy.setHorizontalStretch(0)
  sizePolicy.setVerticalStretch(0)
  sizePolicy.setHeightForWidth(self.castlingGroupBox.sizePolicy().hasHeightForWidth())
  self.castlingGroupBox.setSizePolicy(sizePolicy)
  if True:
   self.wkCheckBox.toggled.connect(self.on_castlingCheckBox_toggled)
   self.wqCheckBox.toggled.connect(self.on_castlingCheckBox_toggled)
   self.bkCheckBox.toggled.connect(self.on_castlingCheckBox_toggled)
   self.bqCheckBox.toggled.connect(self.on_castlingCheckBox_toggled)
  else:
   self.wkCheckBox = self. _addCastlingBox(chess.WHITE, True)
   self.wqCheckBox = self. _addCastlingBox(chess.WHITE, False)
   self.bkCheckBox = self. _addCastlingBox(chess.BLACK, True)
   self.bqCheckBox = self. _addCastlingBox(chess.BLACK, False)
  
  self.aboutDialog = AboutDialog.AboutDialog()
  self.aboutDialog.setup(
   pgm = self.pgm, 
   version = self.version, 
   dateString = self.dateString) 
 def setup(self) -> None:
  self.selectionBox.setup()
  self.placementBoard.setup(self)
  self._resetFen()
  
 def notifyError(self, str : str) -> None:
  self.msgBox.setText(str)
  self.msgBox.exec()
 @QtCore.pyqtSlot(str)
 def notify(self, str : str) -> None:
  self.infoLabel.setText(str)
  self.infoLabel.update()
  QtWidgets.QApplication.processEvents()
  
 def _addCastlingBox(self, color : chess.Color, kingside : bool ) -> None:
  box = QtWidgets.QCheckBox(self.castlingGroupBox)
  box.toggled.connect(self.on_castlingCheckBox_toggled)
  if color == chess.BLACK:
   box.setStyleSheet('background-color: rgb(0, 0, 0);\ncolor: rgb(255, 255, 255);')
  if kingside:
   box.setText('O-O')
  else:
   box.setText('O-O-O')
  box.setChecked(False)
  self.castlingGroupBox.layout().addWidget(box)
  return box
 def _resetFen(self) -> None:
  self.placementBoard.resetPosition()
  if self.position.turn:
   self.wRadioButton.click()
  else:
   self.bRadioButton.click()
  validWK = self.position.kings & chess.BB_E1 > 0
  self.wkCheckBox.setChecked(validWK and self.position.rooks & chess.BB_H1 > 0)
  self.wqCheckBox.setChecked(validWK and self.position.rooks & chess.BB_A1 > 0)
  validBK = self.position.kings & chess.BB_E8 > 0
  self.bkCheckBox.setChecked(validBK and self.position.rooks & chess.BB_H8 > 0)
  self.bqCheckBox.setChecked(validBK and self.position.rooks & chess.BB_A8 > 0)
  self.moveSpinBox.setValue(self.position.halfmove_clock)
  self.clockSpinBox.setValue(self.position.fullmove_number)
  self.notify(self.position.fen())
  epSquares = list(self.position.potentialEnPassantSquares(self.position.turn))
  self.enPassantListWidget.clear()
  self.enPassantListWidget.addItem('-')
  selIndex = 0
  for index,  square in enumerate(epSquares):
   self.enPassantListWidget.addItem(chess.square_name(square))
   if square == self.position.ep_square:
    selIndex = index + 1
  self.enPassantListWidget.setCurrentRow(selIndex)
  self.notify(self.position.fen())
 @QtCore.pyqtSlot()
 def on_actionCopy_triggered(self):
  try:
   MzChess.checkFEN(self.position, allowIncompleteBoard = True)
   fen = self.position.fen(en_passant = 'fen')
   QtWidgets.QApplication.clipboard().setText(fen)
  except ValueError as err:
   self.notifyError('Improper FEN {}:\n{}'.format(fen, str(err)))
   return
   
 @QtCore.pyqtSlot()
 def on_actionPaste_triggered(self):
  fen = QtWidgets.QApplication.clipboard().text()
  try:
   MzChess.checkFEN(fen, allowIncompleteBoard = True)
   self.position.set_fen(fen)
   self._resetFen()
  except ValueError as err:
   self.notifyError('Improper FEN {}:\n{}'.format(fen, str(err)))
   return
 @QtCore.pyqtSlot()
 def on_actionResetBoard_triggered(self):
  self.position.reset()
  self._resetFen()
  
 @QtCore.pyqtSlot()
 def on_actionClearBoard_triggered(self):
  self.position.clear()
  self._resetFen()
  
 @QtCore.pyqtSlot()
 def on_wRadioButton_clicked(self):
  if self.position.turn != chess.WHITE:
   self.position.turn = chess.WHITE
   self._resetFen()
 @QtCore.pyqtSlot()
 def on_bRadioButton_clicked(self):
  if self.position.turn != chess.BLACK:
   self.position.turn = chess.BLACK
   self._resetFen()
  
 @QtCore.pyqtSlot(bool)
 def on_castlingCheckBox_toggled(self, checked):
  if    'wkCheckBox' not in vars(self) \
    
or 'wqCheckBox' not in vars(self) \
    
or 'bkCheckBox' not in vars(self) \
    
or 'bqCheckBox' not in vars(self):
    return
  castlingString = ''
  if self.wkCheckBox.isChecked():
   castlingString += 'K'
  if self.wqCheckBox.isChecked():
   castlingString += 'Q'
  if self.bkCheckBox.isChecked():
   castlingString += 'k'
  if self.bqCheckBox.isChecked():
   castlingString += 'q'
  if len(castlingString) == 0:
   castlingString = '-'
  self.position.set_castling_fen(castlingString)
  self.notify(self.position.fen(en_passant = 'fen'))
  
 @QtCore.pyqtSlot(QtWidgets.QListWidgetItem)
 def on_enPassantListWidget_itemClicked(self, enPassantItem):
  fenList = self.position.fen().split(' ')
  fenList[3] = enPassantItem.text()
  self.position.set_fen(' '.join(fenList))
  self.notify(self.position.fen(en_passant = 'fen'))
 @QtCore.pyqtSlot(int)
 def on_moveSpinBox_valueChanged(self, moveNumber):
  self.position.fullmove_number = moveNumber
  self.notify(self.position.fen(en_passant = 'fen'))
  
 @QtCore.pyqtSlot(int)
 def on_clockSpinBox_valueChanged(self, halfmoveClock):
  self.position.halfmove_clock = halfmoveClock
  self.notify(self.position.fen(en_passant = 'fen'))
 @QtCore.pyqtSlot()
 def on_actionAbout_triggered(self):
  self.notify('')
  self.aboutDialog.exec()
 
 @QtCore.pyqtSlot()
 def on_actionHelp_triggered(self):
  self.notify('')
  QtGui.QDesktopServices.openUrl(self.helpIndex) 
[docs]class ChessGroupBox(QtWidgets.QGroupBox):
 leipzigEncodeDict = {
   'P' : 'p', 'N' : 'n', 'B' : 'b', 'R' : 'r', 'Q' : 'q', 'K' : 'k',
   'p' : 'o', 'n' : 'm', 'b' : 'v', 'r' : 't', 'q' : 'w', 'k' : 'l',
 }
 colorName = ['black', 'white']
[docs] def __init__(self, parent = None) -> None:
  super(ChessGroupBox, self).__init__(parent)
  self.font = QtGui.QFont()
  self.font.setFamily("Chess Leipzig")
  self.font.setPointSize(24)
  self.gridLayout = QtWidgets.QGridLayout(self)
  self.gridLayout.setContentsMargins(5, 5, 5, 5)
  self.squareSize = 40  
  
[docs]class SelectionBox(ChessGroupBox):
[docs] def __init__(self, parent = None) -> None:
  super(SelectionBox, self).__init__(parent)
  self.button2PieceDict = dict()
  self.selectedPiece = None
  self.selectedButton = None 
 def setup(self):
  self.pushButtonList = list()
  for s in 'KkQqRrBbNnPp':
   self._addButton(chess.Piece.from_symbol(s))
  self._addButton(None)
  
 def getButton(self, piece = Optional[chess.Piece]):
  for button, actPiece in self.button2PieceDict.items():
   if actPiece == piece:
    return button
   
 def _addButton(self, piece : Optional[chess.Piece] ) -> None:
  pushButton = QtWidgets.QPushButton(self)
  pushButton.setMinimumSize(QtCore.QSize(self.squareSize, self.squareSize))
  pushButton.setMaximumSize(QtCore.QSize(self.squareSize, self.squareSize))
  pushButton.setCheckable(True)
  pushButton.setFont(self.font)
  nPieces = len(self.button2PieceDict)
  self.button2PieceDict[pushButton] = piece
  if piece is None:
   pushButton.setChecked(True)
   pushButton.setText('x')
   pushButton.setShortcut('x')   
   pushButton.setToolTip('remove piece')
   self.selectedButton = pushButton
   self.gridLayout.addWidget(pushButton, nPieces // 2, nPieces % 2, 1, -1, QtCore.Qt.AlignmentFlag.AlignCenter)
  else:
   pushButton.setChecked(False)
   pushButton.setText(self.leipzigEncodeDict[piece.symbol()])
   if piece.color:
    sc = 'Shift+'
   else:
    sc = ''
   sc += piece.symbol().upper()
   pushButton.setShortcut(sc)   
   pushButton.setToolTip('{} {}'.format(self.colorName[piece.color], chess.piece_name(piece.piece_type)))
   self.gridLayout.addWidget(pushButton, nPieces // 2, nPieces % 2)
  pushButton.clicked.connect(self.on_clicked)
  pushButton.clicked.connect(self.on_clicked)
  self.pushButtonList.append(pushButton)
  
 @QtCore.pyqtSlot()
 def on_clicked(self):
  self.selectedButton.setChecked(False)
  self.selectedButton = self.sender()
  self.selectedPiece = self.button2PieceDict[self.selectedButton]
  self.sender().setChecked(True) 
[docs]class PlacementBoard(ChessGroupBox):
[docs] def __init__(self, parent = None) -> None:
  super(PlacementBoard, self).__init__(parent)
  self.gridLayout.setSpacing(0)
  self.button2SquareList = 64 * [None]
  self.flipped = False 
 
 def setup(self, buildFenClass : BuildFenClass):
  self.buildFenClass = buildFenClass
  self.pushButtonList = list()
  for square in range(64):
   self._addButton(square)
  
 def resetPosition(self):
  pieceDict = self.buildFenClass.position.piece_map()
  for square, pushButton in enumerate(self.button2SquareList):
   if square in pieceDict:
    pushButton.setText(self.leipzigEncodeDict[pieceDict[square].symbol()])
   else:
    pushButton.setText('') 
 def setFlipped(self, flipped : bool):
  if self.flipped != flipped:
   for square in range(32):
    flippedSquare = square ^ 0x38
    tmp = self.button2SquareList[flippedSquare]
    self.button2SquareList[flippedSquare] = self.button2SquareList[square]
    self.button2SquareList[square] = tmp
    self._setBrushAndToolTip(self.button2SquareList[square], square)
    self._setBrushAndToolTip(self.button2SquareList[flippedSquare], flippedSquare)
    
 def _setBrushAndToolTip(self, pushButton : QtWidgets.QPushButton, square : chess.square) -> None:
  if (chess.square_rank(square) + self.flipped) % 2 == chess.square_file(square) % 2:
   pushButton.setStyleSheet("background-color: lightgray; \n;border: none;")  
  else:
   pushButton.setStyleSheet("background-color: white; \n;border: none;")  
  pushButton.setToolTip(chess.square_name(square))
  
 def _addButton(self, square : chess.square ) -> None:
  pushButton = QtWidgets.QPushButton(self)
  pushButton.setMinimumSize(QtCore.QSize(self.squareSize, self.squareSize))
  pushButton.setMaximumSize(QtCore.QSize(self.squareSize, self.squareSize))
  pushButton.setFont(self.font)
  pushButton.setText('')
  self._setBrushAndToolTip(pushButton, square)
  pushButton.clicked.connect(self.on_clicked)
  self.gridLayout.addWidget(pushButton, 7 - chess.square_rank(square), chess.square_file(square))
  self.button2SquareList[square] = pushButton
  self.pushButtonList.append(pushButton)
 @QtCore.pyqtSlot()
 def on_clicked(self):
  sendingButton = self.sender()
  square = self.button2SquareList.index(sendingButton)
  pieceDict = self.buildFenClass.position.piece_map()
  piece = self.buildFenClass.selectionBox.selectedPiece
  oldPiece = pieceDict.get(square, None)
  txt = ''
  if piece != oldPiece:
   if piece is None:
    pieceDict.pop(square, None)
   else:
    pieceDict[square] = piece
    txt = self.leipzigEncodeDict[piece.symbol()]
  else:
   return
  try:
   newBoard = MzChess.Position(self.buildFenClass.position.fen(en_passant = 'fen'))
   newBoard.set_piece_map(pieceDict)
   MzChess.checkFEN(newBoard, allowIncompleteBoard = True)
   sendingButton.setText(txt)
   self.buildFenClass.position.set_piece_map(pieceDict)
   self.buildFenClass._resetFen()
  except ValueError as err:
   self.buildFenClass.notifyError('Improper placement @ {}:\n{}'.format(chess.square_name(square), str(err)))
   return 
   
def showStatus(position):
 print('fen = {}'.format(position.fen(en_passant = 'fen')))
 for row in range(8):
  for col in range(8):
   chessSquare = chess.square(col, row)
   piece = position.piece_at(chessSquare)
   if piece is not None:
    print(' square = {}, name = {}, symbol = {}'.format(
     chessSquare, chess.square_name(chessSquare), piece.symbol()))
 for n, move in enumerate(position.move_stack):
  print('{}. {}'.format(n, move.uci()))
[docs]class MzClassApplication(QtWidgets.QApplication):
[docs] def __init__(self, argv : List[str], notifyFct : Callable[[str], None] = print) -> None: 
  super(MzClassApplication, self).__init__(argv)
  self.notifyFct = notifyFct 
[docs] def notify(self, rec, ev):
  rc = super(MzClassApplication, self).notify(rec, ev)
  #self.notifyFct('{} -> Type(Event)= {}, handled = {}'.format(rec, ev.type(),  rc))
  return rc  
def runFenBuilder(notifyFct : Optional[Callable[[str], None]] = None):
 os.chdir(os.path.expanduser('~'))
 if notifyFct is not None:
  qApp = MzClassApplication(sys.argv)
 else:
  qApp = QtWidgets.QApplication(sys.argv)
 chessMainWindow = BuildFenClass()
 chessMainWindow.show()
 chessMainWindow.setup()
 qApp.exec()
def _runFenBuilder():
 print('Hello, world')
if __name__ == "__main__":
 runFenBuilder(print)