#!/usr/bin/env python3

import numpy as np
from PIL import Image
import argparse
from pathlib import Path

def gaussian_kernel(size, sigma):
    x = np.arange(-size // 2 + 1, size // 2 + 1)
    y = np.arange(-size // 2 + 1, size // 2 + 1)
    xx, yy = np.meshgrid(x, y)
    kernel = np.exp(-(xx**2 + yy**2) / (2 * sigma**2))
    return kernel / kernel.sum()

def apply_periodic_filter(binary_pattern, kernel):
    from scipy.signal import fftconvolve
    return fftconvolve(binary_pattern, kernel, mode='same')

def generate_blue_noise_void_cluster(width, height, sigma=1.5, iterations=None):
    if iterations is None:
        iterations = width * height

    size = width * height
    pattern = np.zeros((height, width), dtype=np.float32)

    kernel_size = int(6 * sigma)
    if kernel_size % 2 == 0:
        kernel_size += 1
    kernel = gaussian_kernel(kernel_size, sigma)

    initial_pattern = np.random.rand(height, width)

    print(f"Generating {width}x{height} blue noise texture...")
    print(f"Kernel size: {kernel_size}x{kernel_size}, sigma: {sigma}")

    dither_array = np.zeros(size, dtype=np.int32)
    binary_pattern = np.zeros((height, width), dtype=np.float32)

    for i in range(size):
        if i % (size // 10) == 0:
            print(f"Progress: {i}/{size} ({100*i//size}%)")

        filtered = apply_periodic_filter(binary_pattern, kernel)

        if i < size // 2:
            initial_energy = initial_pattern + filtered
            coords = np.unravel_index(np.argmax(initial_energy), initial_energy.shape)
        else:
            coords = np.unravel_index(np.argmin(filtered), filtered.shape)

        dither_array[i] = coords[0] * width + coords[1]
        binary_pattern[coords[0], coords[1]] = 1.0

    print("Converting to threshold map...")

    threshold_map = np.zeros((height, width), dtype=np.float32)
    for rank, pos in enumerate(dither_array):
        y = pos // width
        x = pos % width
        threshold_map[y, x] = rank / size

    print("Done!")
    return threshold_map

def generate_blue_noise_simulated_annealing(width, height, iterations=10000):
    print(f"Generating {width}x{height} blue noise using simulated annealing...")

    pattern = np.random.rand(height, width)

    def energy(pattern):
        fft = np.fft.fft2(pattern)
        power = np.abs(fft) ** 2

        h, w = pattern.shape
        cy, cx = h // 2, w // 2
        y, x = np.ogrid[:h, :w]
        dist = np.sqrt((x - cx)**2 + (y - cy)**2)

        low_freq_mask = dist < min(h, w) * 0.1
        low_freq_energy = np.sum(power * low_freq_mask)

        return low_freq_energy

    current_energy = energy(pattern)
    temperature = 1.0
    cooling_rate = 0.9995

    for i in range(iterations):
        if i % (iterations // 10) == 0:
            print(f"Iteration {i}/{iterations}, Energy: {current_energy:.2f}, Temp: {temperature:.4f}")

        y1, x1 = np.random.randint(0, height), np.random.randint(0, width)
        y2, x2 = np.random.randint(0, height), np.random.randint(0, width)

        pattern[y1, x1], pattern[y2, x2] = pattern[y2, x2], pattern[y1, x1]

        new_energy = energy(pattern)
        delta_energy = new_energy - current_energy

        if delta_energy < 0 or np.random.rand() < np.exp(-delta_energy / temperature):
            current_energy = new_energy
        else:
            pattern[y1, x1], pattern[y2, x2] = pattern[y2, x2], pattern[y1, x1]

        temperature *= cooling_rate

    print("Done!")
    return pattern

def main():
    parser = argparse.ArgumentParser(description='Generate blue noise texture for dithering')
    parser.add_argument('--width', type=int, default=128, help='Texture width (default: 128)')
    parser.add_argument('--height', type=int, default=128, help='Texture height (default: 128)')
    parser.add_argument('--method', choices=['void_cluster', 'annealing'], default='void_cluster',
                        help='Generation method (default: void_cluster)')
    parser.add_argument('--sigma', type=float, default=1.5,
                        help='Gaussian kernel sigma for void_cluster method (default: 1.5)')
    parser.add_argument('--iterations', type=int, default=None,
                        help='Number of iterations (optional)')
    parser.add_argument('--output', type=str, default='../blue_noise.png',
                        help='Output file path (default: ../blue_noise.png)')

    args = parser.parse_args()

    try:
        from scipy.signal import fftconvolve
    except ImportError:
        print("Error: scipy is required for this script.")
        print("Install it with: pip install scipy")
        return

    if args.method == 'void_cluster':
        noise = generate_blue_noise_void_cluster(args.width, args.height, args.sigma, args.iterations)
    else:
        noise = generate_blue_noise_simulated_annealing(args.width, args.height,
                                                         args.iterations or 10000)

    noise_normalized = ((noise - noise.min()) / (noise.max() - noise.min()) * 255).astype(np.uint8)

    img = Image.fromarray(noise_normalized, mode='L')

    output_path = Path(__file__).parent / args.output
    output_path.parent.mkdir(parents=True, exist_ok=True)
    img.save(output_path)

    print(f"\nBlue noise texture saved to: {output_path}")
    print(f"Size: {args.width}x{args.height}")
    print(f"Method: {args.method}")

    fft = np.fft.fft2(noise)
    power_spectrum = np.abs(np.fft.fftshift(fft)) ** 2
    print(f"Power spectrum range: {power_spectrum.min():.2e} - {power_spectrum.max():.2e}")

if __name__ == '__main__':
    main()
