import argparse
import sys
from typing import Dict, List, Optional

from bemani.protocol import EAmuseProtocol, Node


def generate_node_name(node: Node, used_names: Dict[str, Node]) -> str:
    potential_name = node.name
    if potential_name not in used_names:
        used_names[potential_name] = node
        return potential_name

    loop = 1
    while f'{potential_name}_{loop}' in used_names:
        loop = loop + 1

    potential_name = f'{potential_name}_{loop}'
    used_names[potential_name] = node
    return potential_name


def generate_node_create(node: Node) -> str:
    dtype = node.data_type
    if dtype == 'str':
        method = 'string'
    elif dtype == 'bin':
        method = 'binary'
    elif dtype == 'ip4':
        method = 'ipv4'
    elif dtype == '4u8':
        method = 'fouru8'
    else:
        method = dtype
    if node.is_array:
        method = f'{method}_array'

    if dtype != 'void':
        # Format the type for display
        if dtype == 'str':
            value = f', \'{node.value}\''
        elif dtype == 'ip4':
            value = f', \'{node.value[0]}.{node.value[1]}.{node.value[2]}.{node.value[3]}\''
        else:
            value = f', {node.value}'
    else:
        value = ''

    return f'Node.{method}(\'{node.name}\'{value})'


def generate_node_link(node_name: str, used_names: Dict[str, Node], parent: Node) -> str:
    # Find the node that parents this, link to it
    found_parent = None
    for parent_name in used_names:
        if used_names[parent_name] is parent:
            found_parent = parent_name
            break

    if found_parent is None:
        raise Exception(f'Failed to find parent name for {parent}')

    return f'{found_parent}.add_child({node_name})'


def generate_lines(node: Node, used_names: Dict[str, Node], parent: Optional[Node]=None) -> List[str]:
    # First, generate node itself
    create = generate_node_create(node)
    if not node.children and not node.attributes and parent:
        # Just directly hook this up to parent
        return [generate_node_link(create, used_names, parent)]

    # Print the node generate itself
    out = []
    node_name = generate_node_name(node, used_names)
    out.append(f'{node_name} = {create}')

    # Now, generate add to parent if exists
    if parent is not None:
        out.append(generate_node_link(node_name, used_names, parent))

    # Now generate node attributes
    for attr in node.attributes:
        out.append(f'{node_name}.set_attribute(\'{attr}\', \'{node.attributes[attr]}\')')

    # Now, do the same for all children
    for child in node.children:
        out.extend(generate_lines(child, used_names, node))

    return out


def generate_code(infile: str, outfile: str, encoding: str) -> None:
    if infile == '-':
        # Load from stdin
        packet = sys.stdin.buffer.read()
    else:
        with open(infile, mode="rb") as infp:
            packet = infp.read()
            infp.close

    # Add an XML special node to force encoding (will be overwritten if there
    # is one in the packet).
    packet = b''.join([
        f'<?xml encoding="{encoding}"?>'.encode(encoding),
        packet,
    ])

    # Attempt to decode it
    proto = EAmuseProtocol()
    req = proto.decode(
        None,
        None,
        packet,
    )

    if req is None:
        # Can't decode, exit
        raise Exception("Unable to decode packet!")

    # Walk through, outputting each node and attaching it to its parent
    code = '\n'.join(generate_lines(req, {}))

    if outfile == '-':
        print(code)
    else:
        with open(outfile, mode="a") as outfp:
            outfp.write(code)
            outfp.close


def main() -> None:
    parser = argparse.ArgumentParser(description="A utility to generate code that will generate a packet given an example packet from a log or binary dump.")
    parser.add_argument("-i", "--infile", help="File containing an XML or binary node structure. Use - for stdin.", type=str, default=None, required=True)
    parser.add_argument("-o", "--outfile", help="File to write python code to. Use - for stdout.", type=str, default=None, required=True)
    parser.add_argument("-e", "--encoding", help="Encoding for the packet, defaults to UTF-8.", type=str, default='utf-8')
    args = parser.parse_args()

    generate_code(args.infile, args.outfile, args.encoding)


if __name__ == '__main__':
    main()
