#!/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
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 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')
    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)

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

        # Scale shape if it is larger than the canvas
        shape_bounds = shape.bounds  # (minx, miny, maxx, maxy)
        shape_width = shape_bounds[2] - shape_bounds[0]
        shape_height = shape_bounds[3] - shape_bounds[1]
        scale_factor = min(
            (canvas_width - 2 * border_spacing) / shape_width,
            (canvas_height - 2 * border_spacing) / shape_height,
            1.0  # Do not scale up, only down
        )
        shape = shapely_scale(shape, xfact=scale_factor, yfact=scale_factor, origin=(0, 0))
        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
            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']
        # 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})"
        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() keep the code functionality and extend it by Improved Placement Algorithm:

    Genetic Algorithms or Simulated Annealing: More sophisticated algorithms can be used to find better packing solutions by exploring a larger search space.
    Greedy Algorithm with Sorting: Placing larger shapes first may improve the packing efficiency.

Rotation of Shapes:

    Allowing shapes to be rotated can improve the packing density.
    Implementing rotations would require additional calculations for collision detection.

make sure to use shapes of different size that could scale like 60, 80, 100, 120, 140 percent of the original size, and allow for rotation in preselected degrees like 15, -15, 30, -30, make sure that the functionality of the original is preserved, double check it and double check that the code uses irregular bin packing and not square packing, make sure to use irregular bin packing