#!/usr/bin/env python3

import argparse
import os
import random
import sys
import math
import numpy as np
from lxml import etree
from svgpathtools import parse_path, svg2paths2
from shapely.geometry import Polygon, MultiPolygon, box
from shapely.affinity import translate, scale as shapely_scale, rotate as shapely_rotate
from shapely.ops import unary_union

NSMAP = {'svg': 'http://www.w3.org/2000/svg'}

def parse_svg_file(svg_file):
    paths, attributes, svg_attributes = svg2paths2(svg_file)
    shapes = []
    for path_obj, attr in zip(paths, attributes):
        # Extract the path data and convert it to a Shapely object
        path_d = attr.get('d')
        if path_d:
            shape = path_to_polygon(parse_path(path_d))
            shapes.append((shape, attr))
    return shapes

def path_to_polygon(path):
    # Sample points along the path to approximate it as a polygon
    NUM_POINTS = 100
    points = [path.point(t / NUM_POINTS) for t in range(NUM_POINTS + 1)]
    coords = [(pt.real, pt.imag) for pt in points]
    polygon = Polygon(coords)
    return polygon

def random_position_within_canvas(shape_bounds, canvas_width, canvas_height, border_spacing):
    min_x = border_spacing
    max_x = canvas_width - shape_bounds[2] - border_spacing
    min_y = border_spacing
    max_y = canvas_height - shape_bounds[3] - border_spacing
    if max_x <= min_x or max_y <= min_y:
        return None  # Shape cannot fit within the canvas
    x = random.uniform(min_x, max_x)
    y = random.uniform(min_y, max_y)
    return x, y

def random_scale_factor():
    return random.choice([0.6, 0.8, 1.0, 1.2, 1.4])

def random_rotation_angle():
    return random.choice([0, 15, -15, 30, -30])

def apply_genetic_algorithm(shapes, canvas_boundary, shape_spacing, canvas_width, canvas_height, border_spacing):
    # Placeholder: Implement Genetic Algorithm here to optimize placement
    return shapes  # Just return the input for now

def main():
    parser = argparse.ArgumentParser(description="Pack multiple SVG files into one SVG canvas.")
    parser.add_argument('--input_files', nargs='+', required=True, help='Input SVG files')
    parser.add_argument('--output_file', required=True, help='Output SVG file')
    parser.add_argument('--width', type=float, required=True, help='Canvas width')
    parser.add_argument('--height', type=float, required=True, help='Canvas height')
    parser.add_argument('--spacing', type=float, default=40.0, help='Spacing between shapes')
    parser.add_argument('--border_spacing', type=float, default=40.0, help='Spacing between shapes and canvas borders')
    parser.add_argument('--algorithm', type=str, choices=['genetic', 'simulated_annealing'], default='genetic', help='Algorithm for shape placement')
    args = parser.parse_args()

    input_files = args.input_files
    output_file = args.output_file
    canvas_width = args.width
    canvas_height = args.height
    shape_spacing = args.spacing
    border_spacing = args.border_spacing

    # Parse input SVG files
    shapes = []
    for svg_file in input_files:
        svg_shapes = parse_svg_file(svg_file)
        if not svg_shapes:
            print(f"Warning: No shapes found in {svg_file}")
            continue
        for shape, attr in svg_shapes:
            shapes.append({'shape': shape, 'attr': attr, 'file': svg_file})

    if not shapes:
        print("No valid shapes to place.")
        sys.exit(1)

    # Initialize placed shapes list
    placed_shapes = []

    # Create canvas boundary
    canvas_boundary = box(0, 0, canvas_width, canvas_height)

    # Select shape placement algorithm
    if args.algorithm == 'genetic':
        shapes = apply_genetic_algorithm(shapes, canvas_boundary, shape_spacing, canvas_width, canvas_height, border_spacing)

    # Attempt to place each shape
    for shape_dict in shapes:
        shape = shape_dict['shape']
        attr = shape_dict['attr']
        filename = shape_dict['file']

        # Apply random scaling and rotation
        scale_factor = random_scale_factor()
        rotation_angle = random_rotation_angle()

        shape = shapely_scale(shape, xfact=scale_factor, yfact=scale_factor, origin=(0, 0))
        shape = shapely_rotate(shape, rotation_angle, origin='centroid')

        shape_bounds = shape.bounds
        shape_width = shape_bounds[2] - shape_bounds[0]
        shape_height = shape_bounds[3] - shape_bounds[1]

        # Increase shape dimensions by spacing for collision detection
        shape_for_collision = shape.buffer(shape_spacing / 2.0)
        max_attempts = 1000
        attempt = 0
        while attempt < max_attempts:
            position = random_position_within_canvas(shape_bounds, canvas_width, canvas_height, border_spacing)
            if position is None:
                print(f"Shape from {filename} is too large to fit in the canvas.")
                break
            x_offset = position[0] - shape_bounds[0]
            y_offset = position[1] - shape_bounds[1]
            # Move shape to the new position
            moved_shape = translate(shape, xoff=x_offset, yoff=y_offset)
            moved_shape_for_collision = translate(shape_for_collision, xoff=x_offset, yoff=y_offset)
            # Check for collision with placed shapes
            collision = False
            for placed in placed_shapes:
                if moved_shape_for_collision.intersects(placed['collision_shape']):
                    collision = True
                    break
            if collision:
                attempt += 1
                continue
            # Ensure shape is within canvas
            if not canvas_boundary.contains(moved_shape):
                attempt += 1
                continue
            # Place the shape
            shape_dict['placed_shape'] = moved_shape
            shape_dict['collision_shape'] = moved_shape_for_collision
            shape_dict['x_offset'] = x_offset
            shape_dict['y_offset'] = y_offset
            shape_dict['scale_factor'] = scale_factor
            shape_dict['rotation_angle'] = rotation_angle
            placed_shapes.append(shape_dict)
            break
        else:
            print(f"Could not place shape from {filename} after {max_attempts} attempts.")

    if not placed_shapes:
        print("No shapes were placed.")
        sys.exit(1)

    # Create the output SVG
    svg_ns = "http://www.w3.org/2000/svg"
    svg_root = etree.Element('svg', nsmap={None: svg_ns})
    svg_root.set('width', str(canvas_width))
    svg_root.set('height', str(canvas_height))
    svg_root.set('viewBox', f"0 0 {canvas_width} {canvas_height}")

    for placed in placed_shapes:
        svg_file = placed['file']
        x_offset = placed['x_offset']
        y_offset = placed['y_offset']
        scale_factor = placed['scale_factor']
        rotation_angle = placed['rotation_angle']
        # Parse the SVG file and get its root
        tree = etree.parse(svg_file)
        svg_elem = tree.getroot()
        # Namespace fix
        nsmap = svg_elem.nsmap
        if None in nsmap:
            nsmap['svg'] = nsmap.pop(None)
        # Get all child elements
        for elem in svg_elem.iter():
            if '}' in elem.tag:
                elem.tag = etree.QName(elem).localname
        # Create a group to apply transformations
        g_elem = etree.SubElement(svg_root, 'g')
        # Set the transformation
        transform = f"translate({x_offset},{y_offset}) scale({scale_factor}) rotate({rotation_angle})"
        g_elem.set("transform", transform)
        # Append all elements from the SVG file
        for child in svg_elem:
            g_elem.append(child)

    # Write the output SVG
    tree = etree.ElementTree(svg_root)
    tree.write(output_file, pretty_print=True, xml_declaration=True, encoding='UTF-8')
    print(f"Output SVG saved as {output_file}")

if __name__ == '__main__':
    main()