"""Adobe-compatible IMM0008 saved-form checksum over template and datasets.

The normalization follows historical PDFium's SAX checksum implementation:
https://github.com/chromium/pdfium/blob/chromium/3002/xfa/fxfa/app/xfa_checksum.cpp
https://github.com/chromium/pdfium/blob/chromium/3002/xfa/fde/xml/cfx_saxreader.cpp

Copyright 2014 PDFium Authors. BSD license: imm0008_checksum.LICENSE.
Only the reviewed IMM0008 packets use this module; shared writers are unchanged.
"""
import base64
import hashlib


def checksum_bytes(raw):
    """Normalize packet bytes without XML newline or numeric-entity conversion.

    This is intentionally not W3C C14N: attribute order, CRLF, numeric entities
    and XFA processing-instruction target data must follow Adobe's checksum.
    The reviewed EN/FR templates have no XML declarations or CDATA sections.
    """
    output, pending = bytearray(), bytearray()
    stack = ['none']
    mode, previous = 'Text', 'Text'
    data, name = bytearray(), bytearray()
    tagname = b''
    skip, tails, entity = 0, 0, -1

    def flush(check_space=False):
        if not check_space or any(char > 32 for char in pending):
            output.extend(pending)
        pending.clear()

    def character(char):
        nonlocal entity
        data.append(char)
        if entity >= 0 and char == 59:
            value = bytes(data[entity + 1:-1])
            if value in (b'quot', b'apos'):
                data[entity:] = b'"' if value == b'quot' else b"'"
            entity = -1
        elif entity < 0 and char == 38:
            entity = len(data) - 1

    for char in raw:
        repeat = True
        while repeat:
            repeat = False
            if mode == 'Text':
                if char == 60:
                    if data and stack[-1] == 'tag':
                        pending.extend(data)
                    data.clear()
                    stack.append('none')
                    mode = 'NodeStart'
                else:
                    character(char)
            elif mode == 'NodeStart':
                if char == 63:
                    stack[-1], mode = 'pi', 'TagName'
                elif char == 33:
                    mode = 'DeclOrComment'
                elif char == 47:
                    mode = 'TagEnd'
                elif char == 62:
                    stack.pop()
                    mode = 'Text'
                elif char > 32:
                    stack[-1], mode = 'tag', 'TagName'
                    data.append(char)
            elif mode == 'DeclOrComment':
                if char != 45:
                    raise ValueError('Unsupported IMM0008 checksum XML declaration')
                mode, tails = 'Comment', 0
            elif mode == 'Comment':
                mode = 'CommentContent'
            elif mode == 'CommentContent':
                if char == 45:
                    tails += 1
                elif char == 62 and tails == 2:
                    pending.extend(b'<!--' + data + b'-->')
                    data.clear()
                    flush()
                    stack.pop()
                    mode = 'Text'
                else:
                    data.extend(b'-' * tails)
                    tails = 0
                    data.append(char)
            elif mode == 'TagName':
                if char < 33 or char in (47, 62, 63):
                    flush(True)
                    tagname = bytes(data)
                    data.clear()
                    pending.extend(b'<' + (b'?' if stack[-1] == 'pi' else b'') + tagname)
                    if char < 33:
                        mode = 'AttrName'
                        name.clear()
                    elif char in (47, 63):
                        previous, mode = mode, 'MaybeClose'
                    else:
                        if stack[-1] == 'tag':
                            pending.extend(b'>')
                            flush()
                        mode = 'Text'
                else:
                    data.append(char)
            elif mode == 'AttrName':
                if char < 33 or char == 61:
                    if not name and char < 33:
                        continue
                    mode = 'AttrValue' if char == 61 else 'AttrEqual'
                    skip = 0
                elif char in (47, 62, 63):
                    if char in (47, 63):
                        previous, mode = mode, 'MaybeClose'
                    else:
                        if stack[-1] == 'tag':
                            pending.extend(b'>')
                            flush()
                        mode = 'Text'
                else:
                    name.append(char)
            elif mode == 'AttrEqual':
                if char == 61:
                    skip, mode = 0, 'AttrValue'
                elif stack[-1] == 'pi':
                    name.append(32)
                    mode, repeat = 'TargetData', True
            elif mode == 'AttrValue':
                if skip:
                    if char == skip:
                        pending.extend(b' ' + name + b'="' + data + b'"')
                        data.clear()
                        name.clear()
                        skip, mode = 0, 'AttrName'
                    else:
                        character(char)
                elif char >= 33 and char in (39, 34):
                    skip = char
            elif mode == 'TargetData':
                if char == 63:
                    previous, mode = mode, 'MaybeClose'
                else:
                    name.append(char)
            elif mode == 'MaybeClose':
                if char == 62:
                    if stack[-1] == 'pi':
                        pending.extend(b' ' + name + b'?>')
                    else:
                        pending.extend(b'></' + tagname + b'>')
                    flush()
                    stack.pop()
                    data.clear()
                    name.clear()
                    mode = 'Text'
                elif previous == 'TagName':
                    data.append(47)
                    mode, repeat = 'TagName', True
                elif previous == 'AttrName':
                    name.append(47)
                    mode, repeat = 'AttrName', True
                elif previous == 'TargetData':
                    name.append(63)
                    mode, repeat = 'TargetData', True
            elif mode == 'TagEnd':
                if char < 33:
                    continue
                if char == 62:
                    stack.pop()
                    pending.extend(b'</' + data + b'>')
                    flush()
                    data.clear()
                    stack.pop()
                    mode = 'Text'
                else:
                    character(char)
    return bytes(output)


def form_checksum(template, datasets):
    digest = hashlib.sha1()
    digest.update(checksum_bytes(template))
    digest.update(checksum_bytes(datasets))
    return base64.b64encode(digest.digest()).decode('ascii')
