#!/usr/bin/env python3

import tkinter, random, re
from tkinter.messagebox import askyesno, showinfo;
import urllib.request
import packaging.version

VERSION="0.1.0";

"""
    Get Branch Chess here:
    https://vertex.alwaysdata.net/downloads/branchchess.py
    https://pastebin.com/tCY8VxSH
    
    Get Manual Chess here (if you want to be able to play regular Chess):
    https://vertex.alwaysdata.net/downloads/manualchess.py
    https://pastebin.com/5K16Ru3b

    Code mostly generated by ChatGPT (guided, edited, and extended by me), initially in Lua Löve and translated to and expanded in Tkinter.
    This is a manual Chess set where you can position the pieces however you want. If you move a piece onto another piece, the other piece will disappear. If you double-click on a piece, it will disappear. If you right-click on a tile, it will change colors, cycling between some colors before turning back. If you want more or fewer colors, you can add them in ChessApp.click_colors (i.e. self.click_colors below).
    You can drag and drop new pieces onto the board from the panels on the side.
    Press F11 to toggle fullscreen mode (or Esc to end fullscreen mode).
    To flip the board, press f.
    To shuffle the pieces, press s (when the first two rows of each player are still occupied).
    To reset the app, press r.
    To check for an update, press p.
    To alter the score (you do it manually) press l to give lowercase a point, and u to give uppcase a point. Press L to remove a point from lowercase and U to remove a point from uppercase.
    To toggle between a regular Chess board and an A-Z board, press A.
    To change the board size manually, see the variable comments in the constructor.
    To change the board setup, alter self.pieces (or comment out the current self.pieces and uncomment the other one.

Purpose:
    The purpose of this program is to allow you to manually play Chess variants with rules you make up yourself.

License [MIT license]:

Copyright 2024 by Mark

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

END OF LICENSE

Changes since version 0.6.1:
• Improved logic for when shuffling is and isn't available.
• Made it easier to manually change the board width for the standard Chess board.
"""

class ChessApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Chess App")
        self.root.attributes('-zoomed', True)

        self.game_active=False #This is used to make it so when you reset the game, you can shuffle it, and so when the game is active, shuffling doesn't work. The game is active when a piece has been moved or deleted, except when done by dragging a piece from the panel directly.
        self.PANEL_PAD=20 #The padding between the top of each panel and the first piece. Was originally 50 to account for the player names (which aren't in the version of the app).
        self.CWIDTH=10 #Modify this (and optionally self.pieces) to change the board width; don't change anything else
        #If there are errors, replaces where self.CWIDTH appears all used to be 8; examine that.
        self.WIDTH=self.CWIDTH
        self.HEIGHT=8 #Modify this (and optionally self.pieces) to change the board height; don't change anything else
        self.click_colors = ["#808080", "#CCCCCC"]  # Add any colors you want to cycle through when right-clicking a tile
        self.click_colors.append(None); #You need an extra one on the end.
        self.current_click_color_indices = {}  # Dictionary to hold current right-click color index for each tile
        self.original_tile_colors = {}  # Dictionary to hold original colors
        
        
        self.lowercase_score = 0
        self.uppercase_score = 0
        # Create labels for displaying scores
        self.score_frame = tkinter.Frame(root)
        self.score_frame.pack(pady=10)  # Add some vertical padding
        self.lowercase_label = tkinter.Label(self.score_frame, text=f"Lowercase: {self.lowercase_score}", font=("Arial", 20))
        #self.lowercase_label.pack(pady=10)
        self.lowercase_label.pack(side=tkinter.LEFT, padx=20)  # Pack to the left
        self.uppercase_label = tkinter.Label(self.score_frame, text=f"Uppercase: {self.uppercase_score}", font=("Arial", 20))
        #self.uppercase_label.pack(pady=10)
        self.uppercase_label.pack(side=tkinter.LEFT, padx=20)  # Pack to the left
        
        self.double_click_active = False
        self.dragging_from_panel = False  # New attribute to track dragging state
        self.selected_piece = None
        self.selected_x = None
        self.selected_y = None
        self.panel_width = 100
        self.panel_height = 640
        self.piece_spacing = 24 #24 is the default for 8 rows; ~29 for 10 rows
        self.tile_colors = {}
        self.default_color = "#66CC66"
        self.default_color_dark = "#FF9999"
        #self.tile_color1 = "#808080"  # This is the first color that the tile changes when you right-click on it.
        #self.tile_color2 = "#CCCCCC"  # This is the second color that the tile changes when you right-click on it.

        self.tile_size = 73 #was 80; altered for lower screen resolution
        self.board_width = self.WIDTH * self.tile_size
        self.board_height = self.HEIGHT * self.tile_size

        self.canvas = tkinter.Canvas(root, width=self.board_width + 2 * self.panel_width, height=self.board_height)
        self.canvas.pack()
        
        self.state = False
        self.root.bind("<F11>", self.toggle_fullscreen)
        self.root.bind("<Escape>", self.end_fullscreen)
        
        self.root.bind("<Key>", self.keybindings)

        self.init_board()
        self.init_pieces()
        self.draw_board()
        self.draw_panels()
        
        self.flip_direction = False  # Track current flip state
        self.flip_board();

        self.canvas.bind("<Button-1>", self.mouse_pressed)
        self.canvas.bind("<B1-Motion>", self.mouse_drag)
        self.canvas.bind("<ButtonRelease-1>", self.mouse_released)
        self.canvas.bind("<Button-3>", self.right_click)
        self.canvas.bind("<Double-1>", self.double_click)
        self.root.bind("<f>", lambda event: self.flip_board())
        self.shuffle_pieces();

    def init_board(self):
        for y in range(self.HEIGHT):
            for x in range(self.WIDTH):
                # Set the original color based on the checkered pattern
                color = self.default_color if (x + y) % 2 == 0 else self.default_color_dark
                self.tile_colors[(x, y)] = color  # Store in the main color map
                self.original_tile_colors[(x, y)] = color  # Store in the original color map
                self.current_click_color_indices[(x,y)]=0 #Initialize right-click color index for each tile.

    def init_pieces(self):
        #"""
        self.pieces = [
            ["F", "T", "R", "N", "B", "K", "Q", "B", "N", "R"],  # White pieces
            ["F", "D", "P", "P", "P", "P", "P", "P", "P", "P"],  # White pawns
            ["A", "A", "X", "D", "H", "M", "W", "H", "C", "X"],  # Empty row
            ["", "", "", "", "", "", "", "", "", ""],  # Empty row
            ["", "", "", "", "", "", "", "", "", ""],  # Empty row
            ["a", "a", "x", "c", "h", "w", "m", "h", "d", "x"],  # Empty row
            ["f", "d", "p", "p", "p", "p", "p", "p", "p", "p"],  # Black pawns
            ["f", "t", "r", "n", "b", "k", "q", "b", "n", "r"]   # Black pieces
        ]
        #"""
        
        """
        self.pieces = [
            ["Z", "Y", "X", "W", "V", "U", "T", "S", "R", "Q", "P", "O", "N"],
            ["M", "L", "K", "J", "I", "H", "G", "F", "E", "D", "C", "B", "A"],
            ["", "", "", "", "", "", "", "", "", "", "", "", ""],  # Empty row
            ["", "", "", "", "", "", "", "", "", "", "", "", ""],  # Empty row
            ["", "", "", "", "", "", "", "", "", "", "", "", ""],  # Empty row
            ["", "", "", "", "", "", "", "", "", "", "", "", ""],  # Empty row
            ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m"],
            ["n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"] 
        ]
        #"""
        
        self.initial_pieces=self.pieces.copy();
        
        #This ensures that self.pieces matches the board dimensions if self.pieces is too small.
        if len(self.pieces)>self.HEIGHT:
            raise ValueError("You made self.pieces have more rows than the height (self.HEIGHT).");
        while len(self.pieces)<self.HEIGHT:
            newlist=[]
            self.pieces.insert(2, newlist);
        for x in self.pieces:
            if len(x)>self.WIDTH:
                raise ValueError("You made more columns in self.pieces than the board width (self.WIDTH).");
            while len(x)<self.WIDTH:
                x.append("")
        
        self.panel_pieces_white = [chr(i) for i in range(65, 91)]  # A-Z
        self.panel_pieces_black = [chr(i).lower() for i in range(65, 91)]  # a-z

    def shuffle_pieces(self):
        player1pieces=self.pieces[0]+self.pieces[1]+self.pieces[2]
        random.shuffle(player1pieces)
        self.pieces[0]=player1pieces[:self.WIDTH]
        self.pieces[1]=player1pieces[self.WIDTH:self.WIDTH*2]
        self.pieces[2]=player1pieces[self.WIDTH*2:self.WIDTH*3]
        
        player2pieces=self.pieces[5]+self.pieces[6]+self.pieces[7]
        random.shuffle(player2pieces)
        self.pieces[5]=player2pieces[:self.WIDTH]
        self.pieces[6]=player2pieces[self.WIDTH:self.WIDTH*2]
        self.pieces[7]=player2pieces[self.WIDTH*2:self.WIDTH*3]
        self.draw_board();

    def draw_board(self):
        self.canvas.delete("all")
        for y in range(self.HEIGHT):
            for x in range(self.WIDTH):
                color = self.tile_colors[(x, y)]
                self.canvas.create_rectangle(x * self.tile_size + self.panel_width, 
                                             y * self.tile_size, 
                                             (x + 1) * self.tile_size + self.panel_width, 
                                             (y + 1) * self.tile_size, 
                                             fill=color)

                piece = self.pieces[y][x]
                if piece:
                    self.canvas.create_text((x + 0.5) * self.tile_size + self.panel_width, 
                                             (y + 0.5) * self.tile_size, 
                                             text=piece, font=("Arial", 24), fill="black" if piece.islower() else "white")

        self.draw_panels()

    def flip_board(self):
        # Create new lists for the flipped pieces and colors
        new_pieces = [["" for _ in range(self.WIDTH)] for _ in range(self.HEIGHT)]
        new_tile_colors = {}
    
        for y in range(self.HEIGHT):
            for x in range(self.WIDTH):
                # Calculate new positions after flipping
                new_x = self.WIDTH - 1 - x
                new_y = self.HEIGHT - 1 - y
                
                # Move the pieces
                new_pieces[new_y][new_x] = self.pieces[y][x]
    
                # Move the tile colors
                new_tile_colors[(new_x, new_y)] = self.tile_colors[(x, y)]
    
        # Update the pieces and tile colors
        self.pieces = new_pieces
        self.tile_colors = new_tile_colors
    
        # Redraw the board with the new arrangement
        self.draw_board()

    def draw_panels(self):
        left_panel_offset = self.PANEL_PAD+27  # Adjust this value to move pieces further right
        for i in range(26):
            y_pos_white = self.PANEL_PAD + i * self.piece_spacing
            y_pos_black = self.PANEL_PAD + i * self.piece_spacing
            self.canvas.create_text(left_panel_offset, y_pos_white, text=self.panel_pieces_white[i], font=("Arial", 16))  # Left panel unchanged
            self.canvas.create_text(self.board_width + 150, y_pos_black, text=self.panel_pieces_black[i], font=("Arial", 16))  # Right panel moved to the right
    
    def get_grid_coordinates(self, event):
        """Get the grid coordinates based on mouse event."""
        grid_x = (event.x - self.panel_width) // self.tile_size
        grid_y = event.y // self.tile_size
        return grid_x, grid_y
        
    def mouse_pressed(self, event):
        grid_x, grid_y=self.get_grid_coordinates(event);

        if 0 <= grid_x < self.WIDTH and 0 <= grid_y < self.HEIGHT:
            self.selected_piece = self.pieces[grid_y][grid_x]
            if self.selected_piece:
                self.selected_x, self.selected_y = grid_x, grid_y

        else:
            adjustment = int(0.02 * self.canvas.winfo_height())  # Adjust by 5% of canvas height
            if event.x < self.panel_width:  # Left panel
                index = (event.y - self.PANEL_PAD + adjustment) // self.piece_spacing
                if 0 <= index < len(self.panel_pieces_white):
                    self.selected_piece = self.panel_pieces_white[index]
                    self.selected_x, self.selected_y = None, None
                    self.dragging_from_panel = True
                    self.selected_x = -1  # Indicating panel
                    self.selected_y = index
    
            elif event.x > (self.board_width + self.panel_width):  # Right panel
                index = (event.y - self.PANEL_PAD + adjustment) // self.piece_spacing
                if 0 <= index < len(self.panel_pieces_black):
                    self.selected_piece = self.panel_pieces_black[index]
                    self.selected_x, self.selected_y = None, None
                    self.dragging_from_panel = True
                    self.selected_x = -1  # Indicating panel
                    self.selected_y = index

        self.draw_board()

    def mouse_drag(self, event):
        if self.selected_piece:
            # Draw the selected piece at the mouse position
            self.canvas.delete("drag_piece")  # Clear previous piece
            self.canvas.create_text(event.x, event.y, text=self.selected_piece, font=("Arial", 24), fill="black" if self.selected_piece.islower() else "white", tags="drag_piece")

    def mouse_released(self, event):
        grid_x, grid_y=self.get_grid_coordinates(event);
    
        if self.selected_piece:
            # Check if the new position is valid
            if not self.double_click_active:  # Only move if it's not a double-click
                if 0 <= grid_x < self.WIDTH and 0 <= grid_y < self.HEIGHT:
                    # Move the piece to the new location
                    self.pieces[grid_y][grid_x] = self.selected_piece
                    
                    # Only remove the piece from its old position if it's actually moved
                    if ((self.selected_x, self.selected_y) != (grid_x, grid_y)) and self.selected_y!=None and self.selected_x!=None:
                        if not self.dragging_from_panel:
                            self.pieces[self.selected_y][self.selected_x] = ""  # Remove from old position
                            self.game_active=True
    
        # Clear the drag indicator
        self.canvas.delete("drag_piece")
    
        # Update the selected coordinates
        self.selected_x = grid_x
        self.selected_y = grid_y
        
        self.dragging_from_panel = False  # Reset the flag after mouse release
        
        self.draw_board()

    def right_click(self, event):
        grid_x, grid_y=self.get_grid_coordinates(event);

        if 0 <= grid_x < self.WIDTH and 0 <= grid_y < self.HEIGHT:
            index = (grid_x, grid_y)  # Ensure we are checking the right tile
            current_color = self.tile_colors[index]
            
            # Set the tile color to the current color in the dynamic list
            self.tile_colors[index] = self.click_colors[self.current_click_color_indices[index]]
    
            # Update the index to the next color
            self.current_click_color_indices[index] += 1
            
            # Reset to the original color if we reach the end of the dynamic colors
            if self.current_click_color_indices[index] >= len(self.click_colors):
                self.current_click_color_indices[index] = 0
                self.tile_colors[index] = self.original_tile_colors[index]  # Reset to original color

        self.draw_board()

    def double_click(self, event):
        self.double_click_active = True  # Set the flag
        
        grid_x, grid_y=self.get_grid_coordinates(event);
    
        if 0 <= grid_x < self.WIDTH and 0 <= grid_y < self.HEIGHT:
            if self.pieces[grid_y][grid_x]:  # Check if there is a piece
                self.pieces[grid_y][grid_x] = ""  # Remove the piece
                self.game_active=True
                self.draw_board()  # Redraw the board to reflect the change
        
        # Reset the flag after a brief delay
        self.root.after(100, self.reset_double_click_flag)

    def reset_double_click_flag(self):
        self.double_click_active = False

    def toggle_fullscreen(self, event=None):
        self.state = not self.state  # Just toggling the boolean
        self.root.attributes("-fullscreen", self.state)
        return "break"

    def end_fullscreen(self, event=None):
        self.state = False
        self.root.attributes("-fullscreen", False)
        return "break"
    
    def keybindings(self, event):
        if event.char == 'l':
            self.lowercase_score += 1
        elif event.char == 'L':
            self.lowercase_score -= 1
        elif event.char == 'u':
            self.uppercase_score += 1
        elif event.char == 'U':
            self.uppercase_score -= 1
        elif event.char == 's':
            #if "" in self.pieces[0] or "" in self.pieces[1] or "" in self.pieces[-1] or "" in self.pieces[-2]:
            if self.game_active==True:
                pass; #Don't shuffle them if the first two rows aren't fully occupied.
            else:
                self.shuffle_pieces();
        elif event.char == 'p':
            #Check for update
            url="https://vertex.alwaysdata.net/downloads/branchchess.py"
            try:
                with urllib.request.urlopen(url) as response:
                    version=re.sub(r"[\s\S]+\bVERSION=\"([\d\.]+)[\s\S]+", r"\1", response.read().decode('utf-8'), 1);
                    if packaging.version.Version(VERSION)>packaging.version.Version(version):
                        showinfo("Version", "You have a 𝒏𝒆𝒘𝒆𝒓 version than is currently available online.");
                    elif packaging.version.Version(VERSION)<packaging.version.Version(version):
                        showinfo("Version", f"A new version ({version}) is available.");
                    elif packaging.version.Version(VERSION)==packaging.version.Version(version):
                        showinfo("Version", f"You currently have the latest version ({version}).");
                    else:
                        showinfo("Error", "The regular expression search must have failed to parse the version correctly.");
            except Exception as e:
                #showinfo("Error", f"Update check failed: {e}");
                raise;
        elif event.char == 'r': #Reset the board
            yn=askyesno("Reset game", "Would you like to reset the game?")
            if yn:
                self.game_active=False
                self.lowercase_score=0;
                self.uppercase_score=0;
                self.WIDTH=self.CWIDTH;
                self.HEIGHT=8;
                self.board_width = self.WIDTH * self.tile_size
                self.board_height = self.HEIGHT * self.tile_size
                self.canvas.config(width=self.board_width + 2 * self.panel_width, height=self.board_height)
                self.init_board()
                self.pieces=self.initial_pieces.copy();
                #self.draw_board();
                self.flip_board(); #This calls self.draw_board(); remove this and uncomment the above if you start with black
                self.draw_panels()
                self.shuffle_pieces();
        elif 1==2 and event.char == 'a':
            #I disabled this; it's broken/incomplete in this version, which isn't used for regular Chess.
            #Toggle A-Z setup with regular Chess pieces
            yn=askyesno("Toggle board style", "Would you like to toggle the board style? This will reset your game.")
            if yn:
                self.game_active=False
                self.lowercase_score=0;
                self.uppercase_score=0;
                if self.WIDTH==self.CWIDTH:
                    self.WIDTH=13;
                    self.pieces = [
                        ["Z", "Y", "X", "W", "V", "U", "T", "S", "R", "Q", "P", "O", "N"],
                        ["M", "L", "K", "J", "I", "H", "G", "F", "E", "D", "C", "B", "A"],
                        ["", "", "", "", "", "", "", "", "", "", "", "", ""],  # Empty row
                        ["", "", "", "", "", "", "", "", "", "", "", "", ""],  # Empty row
                        ["", "", "", "", "", "", "", "", "", "", "", "", ""],  # Empty row
                        ["", "", "", "", "", "", "", "", "", "", "", "", ""],  # Empty row
                        ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m"],
                        ["n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"] 
                    ]
                elif self.WIDTH==13:
                    self.WIDTH=self.CWIDTH;
                    self.pieces = [
                        ["R", "N", "B", "K", "Q", "B", "N", "R"],  # White pieces
                        ["P", "P", "P", "P", "P", "P", "P", "P"],  # White pawns
                        ["", "", "", "", "", "", "", ""],  # Empty row
                        ["", "", "", "", "", "", "", ""],  # Empty row
                        ["", "", "", "", "", "", "", ""],  # Empty row
                        ["", "", "", "", "", "", "", ""],  # Empty row
                        ["p", "p", "p", "p", "p", "p", "p", "p"],  # Black pawns
                        ["r", "n", "b", "k", "q", "b", "n", "r"]   # Black pieces
                    ]
                self.board_width = self.WIDTH * self.tile_size
                self.board_height = self.HEIGHT * self.tile_size
                
                self.canvas.config(width=self.board_width + 2 * self.panel_width, height=self.board_height)
                self.init_board()
                #self.init_pieces()
                #self.draw_board()
                self.flip_board(); #This calls self.draw_board()
                self.draw_panels()
    
        # Update the labels to display the new scores
        self.lowercase_label.config(text=f"Lowercase: {self.lowercase_score}")
        self.uppercase_label.config(text=f"Uppercase: {self.uppercase_score}")

if __name__ == "__main__":
    root = tkinter.Tk()
    app = ChessApp(root)
    root.mainloop()
