# tk_5x5_Sudoku_Slider.py

from tkinter import *
from PIL import Image, ImageDraw, ImageFont, ImageTk
from random import randint
import itertools

BACKGROUND = 'orange'
TSIZE = 90
MARGIN = 7
ROWS = 5
COLUMNS = 5
board = {}

colors = ['#ff8888', '#8888ff', '#88ff88', '#ffff66']
ARC_COLORS = list(itertools.permutations(colors, 4))

instructions = '''
# tk_5x5_Sudoku_Slider.py

To win this game, the player must satisfy all the following conditions:

1. Valid 5×5 Sudoku Grid
- Each number from 1 to 5 must appear exactly once in each row and each column.

2. Matching Edge Colors
- The top half-circle of a tile must match the bottom half-circle of the tile above it.
- The left half-circle must match the right half-circle of the tile to its left.
'''

def create_tile_image(n, arc_colors_4, idx):
	img_size = TSIZE + 2 * MARGIN
	img = Image.new('RGBA', (img_size, img_size))
	draw = ImageDraw.Draw(img)

	draw.rectangle([MARGIN, MARGIN, img_size - MARGIN, img_size - MARGIN], outline=None, fill='white')

	r = TSIZE - 50
	cx, cy = img_size // 2, img_size // 2

	draw.pieslice([cx - r//2, MARGIN - r//2, cx + r//2, MARGIN + r//2], start=0, end=180, fill=arc_colors_4[0])
	draw.pieslice([cx - r//2, img_size - MARGIN - r//2, cx + r//2, img_size - MARGIN + r//2], start=180, end=360, fill=arc_colors_4[1])
	draw.pieslice([MARGIN - r//2, cy - r//2, MARGIN + r//2, cy + r//2], start=270, end=90, fill=arc_colors_4[2])
	draw.pieslice([img_size - MARGIN - r//2, cy - r//2, img_size - MARGIN + r//2, cy + r//2], start=90, end=270, fill=arc_colors_4[3])

	font = ImageFont.truetype("arial.ttf", int(TSIZE * 0.7))
	text_size = draw.textsize(str(n), font=font)
	text_pos = (cx - text_size[0] // 2, cy - text_size[1] // 2 - 6)
	draw.text(text_pos, str(n), font=font, fill='black')
	
	#draw.text(text_pos, str(idx), font=font, fill='gray') # testing purpose

	draw.rectangle([MARGIN, MARGIN, img_size - MARGIN, img_size - MARGIN], outline='black', fill=None)

	return ImageTk.PhotoImage(img)
	
def generate_latin_square(n=5):
    import random
    base = list(range(1, n + 1))
    square = []
    for i in range(n):
        row = base[i:] + base[:i]
        square.append(row)

    for _ in range(10):
        i, j = random.sample(range(n), 2)
        square[i], square[j] = square[j], square[i]
        for row in square:
            row[i], row[j] = row[j], row[i]
    return square

def permute(array):
    n = len(array)
    signature = +1
    for i in range(n - 2):
        j = randint(0, n - 1)
        if j != i:
            signature = -signature
            array[i], array[j] = array[j], array[i]
    if signature < 0:
        array[-2], array[-1] = array[-1], array[-2]

def create_tile(idx, arc_colors_4, n):
	image = create_tile_image(n, arc_colors_4, idx)
	tile_id = canvas.create_image(0, 0, image=image, anchor='nw')
	return {'id': idx, 'canvas_id': tile_id, 'image': image, 'row': 0, 'column': 0}

def move_tile(tile, dx, dy):
    canvas.move(tile['canvas_id'], dx, dy)

def moveto_tile(tile, row, column):
    S = MARGIN + TSIZE
    x = MARGIN + column * S
    y = MARGIN + row * S
    current_x = MARGIN + tile['column'] * S
    current_y = MARGIN + tile['row'] * S
    dx = x - current_x
    dy = y - current_y
    move_tile(tile, dx, dy)
    tile['row'] = row
    tile['column'] = column

def create_timer(parent):
    label = Label(parent)
    label.elapsed = 0
    label.config(text='Time: 0s')
    label.running = True
    def update():
        if label.running:
            label.elapsed += 1
            label.config(text=f'Time: {label.elapsed}s')
            label.after_id = label.after(1000, update)
    label.after_id = label.after(1000, update)
    return label

def stop_timer():
    timer = board['timer']
    timer.running = False
    if hasattr(timer, 'after_id'):
        timer.after_cancel(timer.after_id)

def reset_timer():
    timer = board['timer']
    timer.running = False
    if hasattr(timer, 'after_id'):
        timer.after_cancel(timer.after_id)
    timer.elapsed = 0
    timer.config(text='Time: 0s')
    timer.running = True
    def update():
        if timer.running:
            timer.elapsed += 1
            timer.config(text=f'Time: {timer.elapsed}s')
            timer.after_id = timer.after(1000, update)
    timer.after_id = timer.after(1000, update)

def assign_matching_combos(rows, columns):
    grid = [[None for _ in range(columns)] for _ in range(rows)]
    used = set()

    def is_valid(combo, r, c):
        if r > 0 and grid[r - 1][c] and grid[r - 1][c][1] != combo[0]:
            return False
        if c > 0 and grid[r][c - 1] and grid[r][c - 1][3] != combo[2]:
            return False
        return True

    def backtrack(r, c):
        if r == rows:
            return True

        next_r, next_c = (r, c + 1) if c + 1 < columns else (r + 1, 0)

        if r == rows - 1 and c == columns - 1:
            return backtrack(next_r, next_c)

        for combo in ARC_COLORS:
            if combo in used:
                continue
            if is_valid(combo, r, c):
                grid[r][c] = combo
                used.add(combo)
                if backtrack(next_r, next_c):
                    return True
                grid[r][c] = None
                used.remove(combo)

        return False

    backtrack(0, 0)

    return grid

def create_board(timer, rows, columns):
    combo_grid = assign_matching_combos(rows, columns)
    latin_square = generate_latin_square(rows)
    tiles = []
    for r in range(rows):
        for c in range(columns):
            if r == rows - 1 and c == columns - 1:
                continue
            arc_colors_4 = combo_grid[r][c]
            number = latin_square[r][c]
            tile = create_tile(len(tiles) + 1, arc_colors_4, number)
            moveto_tile(tile, r, c)
            tiles.append(tile)
    board['tiles'] = tiles
    board['rows'] = rows
    board['columns'] = columns
    board['row'] = rows - 1
    board['col'] = columns - 1
    board['timer'] = timer
    board['locked'] = False
    canvas.bind('<Button-1>', lambda e: clickhandler(e))
    canvas.pack()
    shuffle_board()

def shuffle_board():
    canvas.delete('end')
    board['locked'] = False
    positions = [(r, c) for r in range(ROWS) for c in range(COLUMNS)]
    positions.remove((ROWS - 1, COLUMNS - 1))
    permute(positions)
    for tile, (r, c) in zip(board['tiles'], positions):
        moveto_tile(tile, r, c)
    board['row'] = ROWS - 1
    board['col'] = COLUMNS - 1
    reset_timer()

def update_timer():
    timer = board['timer']
    if timer.running:
        timer.elapsed += 1
        timer.config(text=f'Time: {timer.elapsed}s')
        timer.after_id = timer.after(1000, lambda: update_timer())

def clickhandler(event):
    if board['locked']:
        return
    S = MARGIN + TSIZE
    row = max(0, min((event.y - MARGIN // 2) // S, board['rows'] - 1))
    column = max(0, min((event.x - MARGIN // 2) // S, board['columns'] - 1))
    if row == board['row'] and column != board['col']:
        move_row(row, column)
    elif column == board['col'] and row != board['row']:
        move_column(row, column)
    check_win()

def move_row(r, c):
    delta = 1 if c > board['col'] else -1
    for col in range(board['col'], c, delta):
        move_tile_at(r, col + delta, 0, -delta)
    board['col'] = c

def move_column(r, c):
    delta = 1 if r > board['row'] else -1
    for row in range(board['row'], r, delta):
        move_tile_at(row + delta, c, -delta, 0)
    board['row'] = r

def move_tile_at(r, c, tr, tc):
    for tile in board['tiles']:
        if tile['row'] == r and tile['column'] == c:
            moveto_tile(tile, r + tr, c + tc)
            break

def check_win():
	for tile in board['tiles']:
		expected_id = tile['row'] * board['columns'] + tile['column'] + 1
		if tile['id'] != expected_id:
			return

	stop_timer()
	board['locked'] = True
	S = MARGIN + TSIZE
	canvas.create_rectangle(0, 40, S * ROWS + MARGIN, 140, fill='gray', stipple='gray75', outline='', tag='end')
	canvas.create_text(canvas.winfo_width() // 2, TSIZE - 5, text="Puzzle Complete!", font='Helvetica 40', fill='white', tag='end')

def change_bg(color):
    canvas['background'] = color

def show_about():
    top = Toplevel(root)
    top.title("Program Info")
    Label(top, text=instructions, padx=20, pady=20).pack()
    Button(top, text="OK", command=top.destroy).pack()

root = Tk()
root.geometry('+0+0')
root.title("# tk_5x5_Sudoku_Slider.py")

S = MARGIN + TSIZE
canvas = Canvas(root, background=BACKGROUND, height=S * ROWS + MARGIN - 2, width=S * COLUMNS + MARGIN - 2)

menubar = Frame(root, relief=RAISED, bd=2)
menubutton1 = Menubutton(menubar, text='Options')
menubutton1.pack(side=LEFT)
menubutton2 = Menubutton(menubar, text='Colors')
menubutton2.pack(side=LEFT)
timer = create_timer(menubar)
timer.pack(side=RIGHT, padx=MARGIN)
menubar.pack(side=TOP, fill=X)
create_board(timer, ROWS, COLUMNS)

menu1 = Menu(menubutton1)
menu1.add_command(label='About this program...', command=show_about)
menu1.add_command(label='New game', command=lambda: shuffle_board())
menu1.add_command(label='Quit', command=root.quit)
menubutton1['menu'] = menu1

menu2 = Menu(menubutton2)
menu2.add_command(label='Burlywood', command=lambda: change_bg('burlywood'))
menu2.add_command(label='Forest Green', command=lambda: change_bg('Forest Green'))
menu2.add_command(label='Orange', command=lambda: change_bg('orange'))
menubutton2['menu'] = menu2

root.mainloop()