#!/usr/bin/env python3
"""Generate an editable exploded-view SVG (and optionally an accessible HTML page) from a JSON list of components.

Works for any machine, equipment or material that can be described as parts along an assembly axis.
It draws a schematic, not a realistic model: use it for didactic block diagrams and combine it with an
AI-generated or photographic illustration when realism is needed. Python 3 standard library only.

Usage:  python xerador-despiece.py spec.json salida.svg [--html salida.html]
Spec:   {"title": "...", "subtitle": "...", "axis": "horizontal|vertical", "language": "gl",
         "components": [{"name": "...", "function": "...", "shape": "cylinder|plate|block|ring",
                         "length": 60, "diameter": 100, "color": "#8fb0dc"}]}
Only "name" is required for a component. Lengths run along the axis and diameters across it; only their
proportions matter (the drawing is scaled to fit). Output uses numbered badges and a text legend, so the
labels remain editable text and never overlap the drawing.
"""
import argparse
import json
import sys
import textwrap
from html import escape

PALETTE = ['#8fb0dc', '#f2b134', '#7fc4c9', '#8dc79a', '#b7c4c9', '#e9a090', '#c4b5fd', '#f9a8d4']
NOTICE = {'gl': 'Contido elaborado co apoio de intelixencia artificial e revisado polo/a docente antes da súa publicación.',
          'es': 'Contenido elaborado con apoyo de inteligencia artificial y revisado por el/la docente antes de su publicación.'}
TEXT = {'gl': ('Lenda', 'Figura. Despiece esquemático sen escala. A liña descontinua é o eixe de montaxe.', 'Vista de despiece de %s: %d compoñentes separados sobre un eixe de montaxe, numerados de %s.'),
        'es': ('Leyenda', 'Figura. Despiece esquemático sin escala. La línea discontinua es el eje de montaje.', 'Vista de despiece de %s: %d componentes separados sobre un eje de montaje, numerados de %s.')}


def mix(color, amount):
    """Lighten (amount>0) or darken (amount<0) a #rrggbb colour."""
    c = color.lstrip('#')
    rgb = [int(c[i:i + 2], 16) for i in (0, 2, 4)]
    out = [round(v + (255 - v) * amount) if amount > 0 else round(v * (1 + amount)) for v in rgb]
    return '#%02x%02x%02x' % tuple(max(0, min(255, v)) for v in out)


def shape_svg(shape, length, diameter, color):
    """Draw one part centred on the origin with its axis along +x (stroke #1e293b)."""
    L, D = length, diameter
    stroke = 'stroke="#1e293b" stroke-width="1.5" stroke-linejoin="round"'
    light, dark = mix(color, .35), mix(color, -.3)
    if shape in ('plate', 'block'):
        dx = min(L * .6, 22) if shape == 'block' else min(L * .5, 14)
        dy = dx * .6
        return ('<polygon points="%g,%g %g,%g %g,%g %g,%g" fill="%s" %s/>' % (-L / 2, -D / 2, -L / 2 + dx, -D / 2 - dy, L / 2 + dx, -D / 2 - dy, L / 2, -D / 2, light, stroke) +
                '<polygon points="%g,%g %g,%g %g,%g %g,%g" fill="%s" %s/>' % (L / 2, -D / 2, L / 2 + dx, -D / 2 - dy, L / 2 + dx, D / 2 - dy, L / 2, D / 2, dark, stroke) +
                '<rect x="%g" y="%g" width="%g" height="%g" fill="%s" %s/>' % (-L / 2, -D / 2, L, D, color, stroke))
    cap = max(2, min(D * .16, L * .45))
    body = '<rect x="%g" y="%g" width="%g" height="%g" fill="%s" %s/>' % (-L / 2, -D / 2, L, D, color, stroke)
    right = '<ellipse cx="%g" cy="0" rx="%g" ry="%g" fill="%s" %s/>' % (L / 2, cap, D / 2, dark, stroke)
    left = '<ellipse cx="%g" cy="0" rx="%g" ry="%g" fill="%s" %s/>' % (-L / 2, cap, D / 2, light, stroke)
    inner = '<ellipse cx="%g" cy="0" rx="%g" ry="%g" fill="#f8fafc" %s/>' % (-L / 2, cap * .55, D * .3, stroke) if shape == 'ring' else ''
    return body + right + left + inner


def build(spec):
    lang = spec.get('language', 'gl') if spec.get('language', 'gl') in TEXT else 'gl'
    comps = spec['components']
    if not 1 <= len(comps) <= 12:
        raise SystemExit('Use between 1 and 12 components.')
    vertical = spec.get('axis', 'horizontal') == 'vertical'
    for i, c in enumerate(comps):
        c.setdefault('shape', 'cylinder'); c.setdefault('length', 60); c.setdefault('diameter', 100)
        c.setdefault('color', PALETTE[i % len(PALETTE)]); c.setdefault('function', '')
    gap = 44
    total = sum(c['length'] for c in comps) + gap * (len(comps) - 1)
    widest = max(c['diameter'] for c in comps)
    scale = min(1.6, (640 if vertical else 780) / total, 150 / widest if not vertical else 260 / widest)
    legend_lines = []
    for i, c in enumerate(comps, 1):
        text = '%d. %s' % (i, c['name']) + (' - ' + c['function'] if c['function'] else '')
        legend_lines.append((i, textwrap.wrap(text, 62 if not vertical else 44) or [text]))
    title, subtitle = spec.get('title', '[Equipo]'), spec.get('subtitle', 'Vista esquemática sen escala')
    parts, badges = [], []
    pos = 0
    if vertical:
        W, cx = 940, 250
        top = 110
        H = max(top + total * scale + 60, top + 30 + sum(len(l) for _, l in legend_lines) * 20 + 40)
        axis = '<line x1="%g" y1="%g" x2="%g" y2="%g" stroke="#475569" stroke-width="1.5" stroke-dasharray="6 6"/>' % (cx, top - 20, cx, top + total * scale + 20)
    else:
        legend_h = sum(len(l) for _, l in legend_lines) * 20 + 30
        H = 270 + widest * scale + legend_h + 20
        W, cy = 940, 130 + widest * scale / 2 + 20
        left = (W - total * scale) / 2
        axis = '<line x1="%g" y1="%g" x2="%g" y2="%g" stroke="#475569" stroke-width="1.5" stroke-dasharray="6 6"/>' % (left - 20, cy, left + total * scale + 20, cy)
    for i, c in enumerate(comps, 1):
        L, D = c['length'] * scale, c['diameter'] * scale
        centre = pos + L / 2
        pos += L + gap * scale
        if vertical:
            y = top + centre
            parts.append('<g transform="translate(%g,%g) rotate(90)">%s</g>' % (cx, y, shape_svg(c['shape'], L, D, c['color'])))
            side = -1 if i % 2 else 1
            x1, x2 = cx + side * (D / 2 + 10), cx + side * (widest * scale / 2 + 60)
            badges.append('<line x1="%g" y1="%g" x2="%g" y2="%g" stroke="#475569" stroke-width="1.5"/><circle cx="%g" cy="%g" r="15" fill="#1e293b"/><text x="%g" y="%g" text-anchor="middle" fill="#fff" font-size="16" font-weight="700">%d</text>' % (x1, y, x2, y, x2, y, x2, y + 6, i))
        else:
            x = left + centre
            parts.append('<g transform="translate(%g,%g)">%s</g>' % (x, cy, shape_svg(c['shape'], L, D, c['color'])))
            up = i % 2 == 1
            y1 = cy - (D / 2 + 14) if up else cy + D / 2 + 14
            y2 = cy - (widest * scale / 2 + 50) if up else cy + widest * scale / 2 + 50
            badges.append('<line x1="%g" y1="%g" x2="%g" y2="%g" stroke="#475569" stroke-width="1.5"/><circle cx="%g" cy="%g" r="15" fill="#1e293b"/><text x="%g" y="%g" text-anchor="middle" fill="#fff" font-size="16" font-weight="700">%d</text>' % (x, y1, x, y2, x, y2, x, y2 + 6, i))
    legend_x, legend_y = (520, 110) if vertical else (60, 150 + widest * scale + 110)
    legend = [('<text x="%d" y="%d" font-size="18" font-weight="700" fill="#1e40af">%s</text>' % (legend_x, legend_y - 14, TEXT[lang][0]))]
    y = legend_y + 10
    for i, lines in legend_lines:
        for k, line in enumerate(lines):
            legend.append('<text x="%d" y="%d" font-size="14" fill="#1e293b"%s>%s</text>' % (legend_x + (0 if k == 0 else 16), y, ' font-weight="600"' if k == 0 else '', escape(line)))
            y += 20
    description = TEXT[lang][2] % (title, len(comps), '1 a %d' % len(comps) if lang == 'es' else '1 a %d' % len(comps)) + ' ' + ' '.join('%d: %s.' % (i, c['name']) for i, c in enumerate(comps, 1))
    svg = ('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" width="%d" height="%d" font-family="system-ui,Arial,sans-serif">\n<title>Despiece de %s</title>\n<desc>%s</desc>\n'
           '<rect width="%d" height="%d" fill="#f8fafc"/>\n<text x="40" y="46" font-size="26" font-weight="700" fill="#1e40af">%s</text><text x="40" y="72" font-size="14" fill="#475569">%s</text>\n%s\n%s\n%s\n%s\n'
           '<text x="40" y="%d" font-size="12" fill="#475569">%s</text>\n</svg>\n') % (W, H, W, H, escape(title), escape(description), W, H, escape(title), escape(subtitle), axis, '\n'.join(parts), '\n'.join(badges), '\n'.join(legend), H - 14, escape(TEXT[lang][1]))
    return svg, lang, title, comps


def html(svg, lang, title, comps):
    items = ''.join('<li><strong>%s</strong>%s</li>' % (escape(c['name']), (': ' + escape(c['function'])) if c['function'] else '') for c in comps)
    return ('<!DOCTYPE html>\n<html lang="%s"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Despiece de %s</title>'
            '<style>body{font-family:system-ui,Arial,sans-serif;color:#1e293b;margin:0;background:#f8fafc;line-height:1.6}.w{max-width:1000px;margin:0 auto;padding:1.5rem}h1{color:#1e40af}figure{margin:0;background:#fff;border:1px solid #cbd5e1;border-radius:10px;padding:.8rem}svg{width:100%%;height:auto}'
            'footer{margin-top:2rem;font-size:.85rem;color:#475569;border-top:1px solid #cbd5e1;padding-top:1rem}@media print{@page{size:A4;margin:12mm}}</style></head><body><div class="w"><main><h1>Despiece de %s</h1><figure>%s<figcaption>%s</figcaption></figure>'
            '<h2>%s</h2><ol>%s</ol></main><footer><p>[Centro] · [Ciclo] · [Módulo] · CC BY-NC-SA 4.0</p><p>%s</p></footer></div></body></html>\n') % (
            lang, escape(title), escape(title), svg.split('?>')[-1], escape(TEXT[lang][1]), TEXT[lang][0], items, NOTICE[lang])


def main():
    parser = argparse.ArgumentParser(description=__doc__.split('\n')[0])
    parser.add_argument('spec'); parser.add_argument('svg'); parser.add_argument('--html')
    args = parser.parse_args()
    with open(args.spec, encoding='utf-8') as handle:
        spec = json.load(handle)
    svg, lang, title, comps = build(spec)
    with open(args.svg, 'w', encoding='utf-8', newline='\n') as handle:
        handle.write(svg)
    if args.html:
        with open(args.html, 'w', encoding='utf-8', newline='\n') as handle:
            handle.write(html(svg, lang, title, comps))
    print('Created', args.svg, '+', args.html if args.html else 'no HTML')


if __name__ == '__main__':
    sys.exit(main())
