"""Prototype ingestion: read a graphics/*.svg file and extract its top-level
<g id="face"> or <g id="back"> fragment as a compact single-line string,
matching the format of the hand-written entries in svg_data.py."""
import re
import xml.etree.ElementTree as ET

from django.conf import settings

from . import svg_data as data

SVG_NS = 'http://www.w3.org/2000/svg'
ET.register_namespace('', SVG_NS)

GRAPHICS_DIR = settings.BASE_DIR / 'graphics'

FILL_RE = re.compile(r'fill="#[0-9A-Fa-f]{6}"')
STROKE_RE = re.compile(r'stroke="#[0-9A-Fa-f]{6}"')
FILL_OPACITY_RE = re.compile(r'fill-opacity="[^"]*"')
TSPAN_RE = re.compile(r'(<tspan\b[^>]*>).*?(</tspan>)')
TSPAN_OPEN_RE = re.compile(r'<tspan\b[^>]*>')
FONT_FAMILY_RE = re.compile(r'font-family="[^"]*"')
FONT_SIZE_RE = re.compile(r'font-size="[^"]*"')
Y_ATTR_RE = re.compile(r'\by="[^"]*"')


def _tokenize_tspan_y(match):
    return Y_ATTR_RE.sub('y="%y%"', match.group(0))


def _strip_whitespace(elem):
    """Collapse source-file indentation between tags: no space after an
    opening tag before its first child, a single space between sibling
    children, and no trailing space before the closing tag."""
    if elem.text is not None:
        elem.text = elem.text.strip() or None
    children = list(elem)
    for i, child in enumerate(children):
        if child.tail is not None and child.tail.strip() == '':
            child.tail = ' ' if i < len(children) - 1 else None
        _strip_whitespace(child)


def extract_group(svg_path):
    """Return the file's <g>...</g> element as a compact single-line string."""
    root = ET.parse(svg_path).getroot()
    group = root.find(f'{{{SVG_NS}}}g')
    if group is None:
        raise ValueError(f'{svg_path}: no <g> element found')
    _strip_whitespace(group)
    group.tail = None
    xml_str = ET.tostring(group, encoding='unicode')
    # ET declares the default namespace on whatever element it serializes as
    # the root; drop it here since the wrapping SVG_HEADER already declares it.
    return xml_str.replace(f' xmlns="{SVG_NS}"', '', 1)


def ingest_die(svg_path):
    """Extract a graphics/die_*.svg file's <g id="back"> fragment, tokenizing
    fill -> %diecolor%, stroke -> %outcolor%, and fill-opacity -> %fillopacity%
    (normal rendering fills in '1'; the hold/free-hover CSS fills in '.25' to
    fade the shape), matching DIE_BACK's format."""
    fragment = extract_group(svg_path)
    fragment = FILL_RE.sub('fill="%diecolor%"', fragment)
    fragment = STROKE_RE.sub('stroke="%outcolor%"', fragment)
    fragment = FILL_OPACITY_RE.sub('fill-opacity="%fillopacity%"', fragment)
    return fragment


def ingest_pip(svg_path):
    """Extract a graphics/pip_*.svg file's <g id="face"> fragment, tokenizing
    fill/stroke -> %pipcolor%, fill-opacity -> %fillopacity%, font-family/
    font-size -> %font%/%fontsize%, a <tspan>'s y attribute -> %y%, and any
    <tspan> text content -> %text%, matching DIE_PIP's format. These tokens
    only matter for pip_numr.svg and overlay_text.svg (the only two files
    with font-family/font-size/fill-opacity/tspan-y attributes); for every
    other pip file these substitutions are no-ops."""
    fragment = extract_group(svg_path)
    fragment = FILL_RE.sub('fill="%pipcolor%"', fragment)
    fragment = STROKE_RE.sub('stroke="%pipcolor%"', fragment)
    fragment = FILL_OPACITY_RE.sub('fill-opacity="%fillopacity%"', fragment)
    fragment = FONT_FAMILY_RE.sub('font-family="%font%"', fragment)
    fragment = FONT_SIZE_RE.sub('font-size="%fontsize%"', fragment)
    fragment = TSPAN_OPEN_RE.sub(_tokenize_tspan_y, fragment)
    fragment = TSPAN_RE.sub(r'\g<1>%text%\g<2>', fragment)
    return fragment


def iter_die_files():
    """Yield (key, path) for each graphics/die_*.svg that is a DIE_BACK shape
    (excludes non-shape assets like the *_hold overlay files)."""
    for path in sorted(GRAPHICS_DIR.glob('die_*.svg')):
        key = path.stem[len('die_'):]
        if key.endswith('_hold'):
            continue
        yield key, path


def iter_pip_files():
    """Yield (key, path) for each graphics/pip_*.svg file."""
    for path in sorted(GRAPHICS_DIR.glob('pip_*.svg')):
        yield path.stem[len('pip_'):], path


def build_die_back():
    return {key: ingest_die(path) for key, path in iter_die_files()}


def build_die_pip():
    return {key: ingest_pip(path) for key, path in iter_pip_files()}


OVERLAY_PATH = GRAPHICS_DIR / 'overlay_text.svg'


def build_die_overlay():
    """Ingest graphics/overlay_text.svg -- the 'Hold' label template used to
    build the hold/free-hover CSS backgrounds -- with the same tokenizing as
    any other pip file."""
    return ingest_pip(OVERLAY_PATH)


def _format_dict(name, mapping):
    lines = [f'{name} = {{']
    for key, value in mapping.items():
        lines.append(f"    '{key}': '{value}',")
    lines.append('}')
    return '\n'.join(lines) + '\n'


def _format_value(name, value):
    return f"{name} = '{value}'\n"


OVERLAY_DEFAULTS = {
    '%font%': 'Malgun Gothic',
    '%fontsize%': '210px',
    '%text%': 'Hold',
}
HOLD_FREE_VARIANTS = (('hold', 'red'), ('free:hover', 'green'))


def build_hold_free_css(die_back, die_overlay):
    """Build the '.{shape} span.hold' / '.{shape} span.free:hover' CSS rules
    from DIE_BACK + DIE_OVERLAY: a red-tinted 'Hold' overlay while a die is
    held, a green-tinted preview on hover while free. fill-opacity is set to
    .25 on both layers so the die's face still shows through; stroke-opacity
    is left as DIE_BACK already has it (always fully opaque), so the shape's
    outline stays crisp."""
    blocks = []
    for shape, key in data.DIE_BACK_FILE.items():
        overlay = (die_overlay
                   .replace('%font%', OVERLAY_DEFAULTS['%font%'])
                   .replace('%fontsize%', OVERLAY_DEFAULTS['%fontsize%'])
                   .replace('%text%', OVERLAY_DEFAULTS['%text%'])
                   .replace('%y%', data.DIE_BACK_OVERLAY_Y[shape]))
        for variant, color in HOLD_FREE_VARIANTS:
            back_svg = (die_back[key]
                        .replace('%diecolor%', color)
                        .replace('%outcolor%', color)
                        .replace('%fillopacity%', '.25'))
            overlay_svg = overlay.replace('%pipcolor%', color).replace('%fillopacity%', '.75')
            svg = f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 557 557">{back_svg}{overlay_svg}</svg>'
            rule = [f'.{shape} span.{variant} {{']
            if variant == 'hold':
                rule.append('  display: none;')
            rule.append(f"  background: url('data:image/svg+xml;utf8,{svg}') no-repeat;")
            rule.append('  background-size: cover;')
            rule.append('}')
            blocks.append('\n'.join(rule))
    return '\n'.join(blocks) + '\n'


DIE_BACK_MARKER_BEGIN = '# BEGIN GENERATED DIE_BACK'
DIE_BACK_MARKER_END = '# END GENERATED DIE_BACK'
DIE_PIP_MARKER_BEGIN = '# BEGIN GENERATED DIE_PIP'
DIE_PIP_MARKER_END = '# END GENERATED DIE_PIP'
DIE_OVERLAY_MARKER_BEGIN = '# BEGIN GENERATED DIE_OVERLAY'
DIE_OVERLAY_MARKER_END = '# END GENERATED DIE_OVERLAY'
CSS_MARKER_BEGIN = '/* BEGIN GENERATED HOLD_FREE_CSS */'
CSS_MARKER_END = '/* END GENERATED HOLD_FREE_CSS */'


def _replace_block(content, begin_marker, end_marker, new_body):
    """Replace the text between a single begin_marker/end_marker pair with
    new_body, keeping the markers themselves. Raises if the markers aren't
    found exactly once (so a drifted svg_data.py fails loudly)."""
    pattern = re.compile(re.escape(begin_marker) + r'.*?' + re.escape(end_marker), re.S)
    replacement = f'{begin_marker}\n{new_body}{end_marker}'
    new_content, count = pattern.subn(replacement, content)
    if count != 1:
        raise ValueError(f'expected exactly one {begin_marker} ... {end_marker} block, found {count}')
    return new_content


def regenerate(svg_data_path=None, css_path=None):
    """Scan graphics/, ingest every die_*/pip_*.svg file plus overlay_text.svg,
    and rewrite the generated DIE_BACK/DIE_PIP/DIE_OVERLAY blocks in
    svg_data.py and the hold/free-hover CSS block in dice.css, all in place
    (everything else in either file -- SVG_HEADER, STANDARD_DIE, etc. -- is
    left untouched). Returns svg_data_path."""
    if svg_data_path is None:
        svg_data_path = settings.BASE_DIR / 'dice' / 'svg_data.py'
    if css_path is None:
        css_path = settings.BASE_DIR / 'dice' / 'static' / 'dice' / 'css' / 'dice.css'
    die_back = build_die_back()
    die_pip = build_die_pip()
    die_overlay = build_die_overlay()

    content = svg_data_path.read_text(encoding='utf-8')
    content = _replace_block(
        content, DIE_BACK_MARKER_BEGIN, DIE_BACK_MARKER_END, _format_dict('DIE_BACK', die_back) + '\n'
    )
    content = _replace_block(
        content, DIE_PIP_MARKER_BEGIN, DIE_PIP_MARKER_END, _format_dict('DIE_PIP', die_pip) + '\n'
    )
    content = _replace_block(
        content, DIE_OVERLAY_MARKER_BEGIN, DIE_OVERLAY_MARKER_END, _format_value('DIE_OVERLAY', die_overlay) + '\n'
    )
    svg_data_path.write_text(content, encoding='utf-8', newline='\n')

    css_content = css_path.read_text(encoding='utf-8')
    css_content = _replace_block(
        css_content, CSS_MARKER_BEGIN, CSS_MARKER_END, build_hold_free_css(die_back, die_overlay)
    )
    css_path.write_text(css_content, encoding='utf-8', newline='\n')

    return svg_data_path
