#!/usr/bin/env python3

'''
Application for procedural content adaption
*THIS IS VERY EXPERIMENTAL*
Contains a very poor man's implementation of spline mesh warping.

This application parses SVG path data of an outfit sample aligned to a body part.
The outfit is then replicated for other shapes of the same body part.

Example data is geared towards generating a strap outfit for boobs and torso
for all sizes of boobs and all shapes of torsos based on a single outfit for
boobs of size 2 and a hourglass torso respectively.

Limitations:
* handles paths only
* only svg.path Line and CubicBezier are tested
* only the aforementioned examples are tested

Usage:
python3 vector_clothing_replicator.py infile clothing bodypart destinationfile

Usage Example:
python3 vector_clothing_replicator.py vector_source.svg Straps Boob vector_destination.svg
python3 vector_clothing_replicator.py vector_source.svg Straps Torso vector_destination.svg
'''

from svg.path import parse_path
import copy
import lxml.etree as etree
import sys

REFERENCE_PATH_SAMPLES = 200
EMBED_REPLICATIONS = True # whether to embed all replications into the input file or output separate files

input_file = sys.argv[1]
clothing = sys.argv[2]
bodypart = sys.argv[3]
output_file_embed = sys.argv[4]

# TODO: make these configurable
output_file_pattern = '%s_%s_%s.svg' #bodypart, target_id, clothing

if ('Torso' == bodypart):
  xpath_shape = './svg:g[@id="Torso_"]/svg:g[@id="Torso_%s"]/svg:path[@class="skin torso"]/@d' # TODO: formulate more general, independent of style
  xpath_outfit_container = '//svg:g[@id="Torso_Outfit_%s_"]'%(clothing)
  xpath_outfit = '//svg:g[@id="Torso_Outfit_%s_%s"]'%(clothing,'%s')
  target_ids = "Unnatural,Hourglass,Normal".split(",")
  reference_id = "Hourglass"
else:
  raise RuntimeError("Please specify a bodypart for clothing to replicate.")

tree = etree.parse(input_file)
ns = {'svg' : 'http://www.w3.org/2000/svg'}

canvas = copy.deepcopy(tree)
for e in canvas.xpath('./svg:g',namespaces=ns)+canvas.xpath('./svg:path',namespaces=ns):
  # TODO: this should be "remove all objects, preserve document properties"
  e.getparent().remove(e)

def get_points(xpath_shape):
  '''
  This function extracts reference paths by the given xpath selector.
  Each path is used to sample a fixed number of points.
  '''
  paths_data = tree.xpath(xpath_shape,namespaces=ns)
  points = []
  path_length = None
  for path_data in paths_data:
	p = parse_path(path_data)
	points += [
	  p.point(1.0/float(REFERENCE_PATH_SAMPLES)*i)
	  for i in range(REFERENCE_PATH_SAMPLES)
	]
  if (not points):
	raise RuntimeError(
	  'No paths for reference points found by selector "%s".'%(xpath_shape)
	)
  return points

def point_movement(point, reference_points, target_points):
  '''
  For a given point, finds the nearest point in the reference path.
  Gives distance vector from the nearest reference point to the
  respective target reference point.
  '''
  distances = [abs(point-reference_point) for reference_point in reference_points]
  min_ref_dist_idx = min(enumerate(distances), key=lambda x:x[1])[0]
  movement = target_points[min_ref_dist_idx] - reference_points[min_ref_dist_idx]
  return movement

reference_points = get_points(xpath_shape%(reference_id))
container = tree.xpath(xpath_outfit_container,namespaces=ns)
if (len(container) != 1):
  raise RuntimeError('Outfit container selector "%s" does not yield exactly one layer.'%(xpath_outfit_container))
container = container[0]
outfit_source = container.xpath(xpath_outfit%(reference_id),namespaces=ns)
if (len(outfit_source) != 1):
  raise RuntimeError('Outfit source selector "%s" does not yield exactly one outfit layer in container selected by "%s".'%(xpath_outfit%(reference_id), xpath_outfit_container))
outfit_source = outfit_source[0]

for target_id in target_ids:
  print(
	'Generating variant "%s" of clothing "%s" for bodypart "%s"...'%
	(target_id, clothing, bodypart)
  )
  outfit = copy.deepcopy(outfit_source)
  paths = outfit.xpath('./svg:path',namespaces=ns)
  if target_id == reference_id:
	print("This is the source variant. Skipping...")
  else:
	layerid = outfit.get('id').replace('_%s'%(reference_id),'_%s'%(target_id))
	outfit.set('id', layerid)
	outfit.set(etree.QName('http://www.inkscape.org/namespaces/inkscape', 'label'), layerid) # for the Inkscape-users
	target_points = get_points(xpath_shape%(target_id))
	if (len(reference_points) != len(target_points)):
	  raise RuntimeError(
		('Different amounts of sampled points in reference "%s" and target "%s" paths. '+
		'Selector "%s" probably matches different number of paths in the two layers.')%
		(reference_id, target_id, xpath_shape)
	  )
	for path in paths:
	  path_data = path.get("d")
	  p = parse_path(path_data)
	  for segment in p:
		original_distance = abs(segment.end-segment.start)
		start_movement = point_movement(segment.start, reference_points, target_points)
		segment.start += start_movement
		end_movement = point_movement(segment.end, reference_points, target_points)
		segment.end += end_movement
		distance = abs(segment.end-segment.start)
		try:
		  # enhance position of CubicBezier control points
		  # amplification is relative to the distance gained by movement
		  segment.control1 += start_movement
		  segment.control1 += (segment.control1-segment.start)*(distance/original_distance-1.0)
		  segment.control2 += end_movement
		  segment.control2 += (segment.control2-segment.end)*(distance/original_distance-1.0)
		except AttributeError as ae:
		  # segment is not a CubicBezier
		  pass
	  path.set("d", p.d())
	if EMBED_REPLICATIONS:
	  container.append(outfit)
  if not EMBED_REPLICATIONS:
	container = copy.deepcopy(canvas).xpath('.',namespaces=ns)[0]
	container.append(outfit)

  if not EMBED_REPLICATIONS:
	svg = etree.tostring(container, pretty_print=True)
	with open((output_file_pattern%(bodypart, target_id, clothing)).lower(), 'wb') as f:
	  f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
	  f.write(svg)

if EMBED_REPLICATIONS:
  svg = etree.tostring(tree, pretty_print=True)
  with open(output_file_embed, 'wb') as f:
	f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
	f.write(svg)