from itertools import product, combinations
import matplotlib.pyplot as plt
import matplotlib.patheffects as PathEffects
from utils_workspace_manger import *
from utils_evolution_tools import *
from utils_coordinates import *
from utils_server_client import *
from utils_block_color import *
from blocks_by_tags import MINEABLE_SHOVEL_TAG

SC = ServerClient()
DEFAULT_PONIT = Coordinates(0,0,697)

COLORS = [
    ImageColor.getrgb("#FF0000"),  # Red
    ImageColor.getrgb("#00FF00"),  # Green
    ImageColor.getrgb("#0000FF"),  # Blue
    ImageColor.getrgb("#FFFF00"),  # Yellow
    ImageColor.getrgb("#FF00FF"),  # Magenta
    ImageColor.getrgb("#00FFFF"),  # Cyan
    ImageColor.getrgb("#000000"),  # Black
    ImageColor.getrgb("#FFFFFF")   # White
]

def create_color_dict(block_dict:dict, blocks = None):
    if blocks is None:
        blocks = block_dict.keys()
    
    color_dict = defaultdict(list)
    for block_enum in blocks:
        block_color = block_dict.get(block_enum)
        if block_color is not None:
            hex_color = ColorConverter.rgb_to_str(block_color)
            color_dict[hex_color].append(block_enum)
    
    return color_dict

def get_unique_colors_and_counts(color_dict, min_count=0):
    color_counts = {color: len(block) for color, block in color_dict.items()}

    # Sort colors by their counts
    sorted_colors = sorted(color_counts.items(), key=lambda x: (x[1],x[0]), reverse=True)

    # Extract colors and counts for plotting
    colors = [color for color, count in sorted_colors if count>=min_count]
    counts = [count for color, count in sorted_colors if count>=min_count]

    return colors,counts

def place_blocks_by_color(color_dict, coor, min_count=2, x_spacing=1):
    colors, counts = get_unique_colors_and_counts(color_dict, min_count)
    request = []
    sign_request = []
    s_coor = coor
    s_coor_signs = Coordinates.move_north(s_coor)
    x_spacing +=1
    
    for x,color in enumerate(reversed(colors)):
        for y,block in enumerate(color_dict[color]):
            
            request.append(
                ServerClient.create_block(
                    s_coor+Coordinates(x*x_spacing,y,0),
                    block,
                )
            )
            
            sign_request.append(
                ServerClient.create_sign_descriptor(
                    ServerClient.create_block(
                        s_coor_signs+Coordinates(x*x_spacing,y,0),
                        OAK_WALL_SIGN,
                        NORTH
                    ),
                    BlockType.Name(block)
                )
            )
    
    SC.place_blocks_from_list(request)
    SC.place_signs(sign_request)
    SC.ask_for_response("Send \"/response\" to continue")
    
    #clean up
    SC.place_blocks_from_loc(
        s_coor_signs,
        Coordinates(len(colors)*x_spacing, counts[0] +5 ,2),
        AIR
    )
    
def plot_color_counts(color_dict, min_count=2, figsize=(10, 6)):
    colors, counts = get_unique_colors_and_counts(color_dict, min_count) 
    # Plotting
    plt.figure(figsize=figsize)
    plt.bar(np.arange(len(colors)), counts, color=colors)
    plt.xticks(np.arange(len(colors)), ["" for _ in range(len(colors))])  # Removing x-axis ticks
    plt.ylabel('Number of Strings')
    plt.title('Number of Strings by Color')
    plt.show()

def generate_different_gradients(color_metrics, gradient_funcs, colors, num_steps):
    block_gradients = []
    for rep,dist,inter in color_metrics:
        bc.set_color_metrics(rep,dist,inter)
        colors = [convert_color(c,rep) for c in colors]
        
        for get_gradient in gradient_funcs:
            block_gradients.append([])
            for start_color, end_color in combinations(colors, r=2):
                block_gradients[-1].append(get_gradient(start_color, end_color, num_steps))
    return block_gradients

def place_blocks_in_gradients(block_gradients, coor, spacing=3, labels =[]):
    request = []
    spacing +=1
    max_x = len(block_gradients)
    max_y = 0
    max_z = 0
    for x,gradients in enumerate(block_gradients):
        max_z = max(max_z,len(gradients))
        for z,gradient in enumerate(gradients):
            for y,block in enumerate(gradient):
                request.append(
                    ServerClient.create_block(
                        coor+Coordinates(x*spacing,y,z*spacing),
                        block,
                    )
                )
            
            request.append(
                ServerClient.create_block(
                    coor+Coordinates(x*spacing,-1,z*spacing),
                    BARRIER,
                )
            )
            max_y= max(max_y,len(gradient))

    signs = [
        ServerClient.create_sign_descriptor(
            ServerClient.create_block(
                coor+Coordinates(i*spacing,-1,-1),
                OAK_WALL_SIGN,
                NORTH
            ),
            label
        )
        for i,label in enumerate(labels )
    ]
        
    shape = Coordinates(max_x*spacing,max_y,max_z*spacing)
    SC.place_blocks_from_list(request)
    SC.place_signs(signs)
    SC.ask_for_response("Send \"/response\" to continue")
    #clean up
    one = Coordinates(1,1,1)
    SC.place_blocks_from_loc( coor-one, shape+one*2, AIR)

def plot_gradient(block_colors:BlockColors, block_gradient, title="Minecraft Block Color Gradient"):
    num_steps = len(block_gradient)
    color_gradient = [block_colors.get_block_color(block) for block in block_gradient]
    # Plot the color gradient
    plt.figure(figsize=(num_steps*2, 2))
    plt.title(title)

    for i, color in enumerate(color_gradient):
        rgb = convert_color(color,sRGBColor).get_value_tuple()
        rgb = tuple(GeneralUtils.clip(c,0.0,1.0) for c in rgb)
        plt.fill_between([i, i + 1], 0, 1, color=rgb)
        # Add label below the color
        text = plt.text(i + 0.5, 0.5, BlockType.Name(block_gradient[i]), 
                ha='center', va='center', fontsize=10, rotation=45,
                color='black')
        text.set_path_effects([PathEffects.withStroke(linewidth=3, foreground='white')])
        
        if i>0:
            text = plt.text(i , 0.5,
                    "{0:.2f}".format(block_colors.get_color_distance(color_gradient[i-1],color_gradient[i])), 
                    ha='center', va='center', fontsize=10, rotation=0,
                    color='black')
            text.set_path_effects([PathEffects.withStroke(linewidth=3, foreground='white')])

    plt.xlim(0, num_steps)
    plt.ylim(0, 1)  # Adjust ylim to make space for labels
    plt.yticks([])  # Remove y-axis ticks
    plt.show()

def place_palette(bc:BlockColors, coor:Coordinates, rep_change=None):
    request = []
    shape = Coordinates(256,256,256)
    coor = coor + shape
    coor.crop_y()
    coor -= shape
    supports = []
    color_dict = create_color_dict(bc.get_block_dict(), bc.get_gradient_set())
    for color,blocks in color_dict.items():
        block = blocks[0]
        if len(blocks)>1:
            valid_blocks = None
            
            for block_set in ALL_BLOCK_LIST:
                valid_blocks = set(block_set).intersection(blocks)
                if valid_blocks is not None:
                    break
            
            if len(valid_blocks)>1:
                print([BlockType.Name(b) for b in valid_blocks])
            if len(valid_blocks)>0:
                block = valid_blocks.pop()
        rgb_color = ColorConverter.str_to_rgb(color)
        color_offset = Coordinates.from_tuple(
            rgb_color if rep_change is None else rep_change(rgb_color)
        )
        request.append(
            ServerClient.create_block(
                coor + color_offset,
                block,
            )
        )
        if block in MINEABLE_SHOVEL_TAG:
            supports.append(
                ServerClient.create_block(
                    coor + color_offset + Coordinates.cardinal_down(),
                    BARRIER,
                )
            ) 
        
    SC.place_blocks_from_list(supports + request)
    SC.ask_for_response(f"Color palette for {bc._color_rep.__name__} representation\n Send \"/response\" to continue")
    
    #clean up
    ground_y = Coordinates.y_min + 4
    SC.place_blocks_from_loc(
        Coordinates(coor.x, ground_y, coor.z),
        Coordinates(shape.x, Coordinates.y_max -ground_y, shape.z),
        AIR
    )

def rgb_to_coor(rgb):
    return tuple(int(val*100) for val in rgb.get_value_tuple())
 
def lab_to_coor(lab):
    return (int(lab.lab_l),int(lab.lab_a)+128,int(lab.lab_b)+128)

def hsv_to_coor(hsv):
    radius = 50
    theta = math.radians(hsv.hsv_h)
    r = hsv.hsv_s*radius
    x = r * math.cos(theta)
    z = r * math.sin(theta)
    return (int(x+radius),int(hsv.hsv_v*100),int(z+radius))
    
if __name__ == "__main__":
    bc = BlockColors.load_from_default_file()
    block_dict = bc.get_block_dict()
    color_dict = create_color_dict(block_dict)

    palette = set(ALL_FULL_BLOCKS)
    bc.set_block_pallete(palette)
    
    # Unique Test
    place_blocks_by_color(color_dict, DEFAULT_PONIT)

    # Gradient Test
    num_steps = 7
    colors = [ColorConverter.rgb_to_obj(c, sRGBColor) for c in COLORS]
    gradient_func = [bc.generate_gradient,bc.generate_recurcive_gradient,bc.generate_gradient_via_graph]
    grad_labels = ["straight","recursive"]
    color_metrics_map = [
        (sRGBColor, ColorMetrics.rgb_distance, ColorMetrics.get_intermediate_color),
        (HSVColor, ColorMetrics.hsv_distance, ColorMetrics.get_intermediate_color),
        (LabColor, ColorMetrics.lab_distance, ColorMetrics.get_intermediate_color),
    ]
    labels  = [f"{cm[0].__name__} {grad}" for cm,grad in product(color_metrics_map,grad_labels)]
    block_gradients = generate_different_gradients(color_metrics_map, gradient_func, colors, num_steps)
    place_blocks_in_gradients(block_gradients, DEFAULT_PONIT, labels =labels )
    
    # Lab Distace Test
    lab_dists = [delta_e_cie1976, delta_e_cie1994, delta_e_cie2000, delta_e_cmc]
    color_metrics_dist =[(LabColor, dist, ColorMetrics.get_intermediate_color) for dist in lab_dists]
    labels  = [f"{dist.__name__} {grad}" for dist,grad in product(lab_dists, grad_labels)]
    block_gradients = generate_different_gradients(color_metrics_dist, gradient_func, colors, num_steps)
    place_blocks_in_gradients(block_gradients, DEFAULT_PONIT, labels =labels )
    
    # Palette Test
    place_palette(bc, DEFAULT_PONIT, lambda c: rgb_to_coor(ColorConverter.rgb_to_obj(c,sRGBColor)))
    place_palette(bc, DEFAULT_PONIT, lambda c: lab_to_coor(ColorConverter.rgb_to_obj(c,LabColor)))
    place_palette(bc, DEFAULT_PONIT, lambda c: hsv_to_coor(ColorConverter.rgb_to_obj(c,HSVColor)))