#! /usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import json
import os
import sys
import urllib
from urllib import request
from urllib import parse

try:
    from bs4 import BeautifulSoup
except ImportError:
    raise ImportError(
        "Please install BeautifulSoup (apt-get install python3-bs4 or pip install beautifulsoup4 should do it)")

parser = argparse.ArgumentParser(description='Docenizes HTML version of the official Python documentation')
parser.add_argument('-i', '--inputfolder', type=str,
                    help='Folder where the input files reside as .html. Default is ./python-inst-docs/',
                    default='python-inst-docs')
parser.add_argument('-o', '--outputpath', type=str, help='Final path of the .ts file. Default is ./asm-docs-python.ts',
                    default='./asm-docs-python.ts')
parser.add_argument('-d', '--downloadfolder', type=str,
                    help='Folder where the archive will be downloaded and extracted', default='python-inst-docs')

# The maximum number of paragraphs from the description to copy.
MAX_DESC_PARAS = 5

# Where to extract the asmdoc archive.
ARCHIVE_URL = "https://docs.python.org/3/library/dis.html"
ARCHIVE_NAME = "dis.html"


class Instruction(object):
    def __init__(self, name, names, tooltip, body):
        self.name = name
        self.names = names
        self.tooltip = tooltip.rstrip(': ,')
        self.body = body

    def __str__(self):
        return f"{self.name} = {self.tooltip}\n{self.body}"


def get_url_for_instruction(instr):
    return f"https://docs.python.org/3/library/dis.html#opcode-{urllib.parse.quote(instr.name)}"


def download_asm_doc_archive(downloadfolder):
    if not os.path.exists(downloadfolder):
        print(f"Creating {downloadfolder} as download folder")
        os.makedirs(downloadfolder)
    elif not os.path.isdir(downloadfolder):
        print(f"Error: download folder {downloadfolder} is not a directory")
        sys.exit(1)
    archive_name = os.path.join(downloadfolder, ARCHIVE_NAME)
    print("Downloading archive...")
    urllib.request.urlretrieve(ARCHIVE_URL, archive_name)


def get_description(opcode):
    dd = opcode.find('dd')
    for d in list(dd.descendants):
        if not hasattr(d, 'name') or not hasattr(d, 'attrs'):
            # Must be a string node. Ignore.
            continue
        # Sanitize HTML. Keep some known tags but remove all attributes. Remove
        # all other tags.
        if d.name in ('p', 'code', 'pre', 'table', 'tr', 'td', 'th', 'ul', 'li'):
            for k in list(d.attrs.keys()):
                del d[k]
        else:
            d.unwrap()
    return ''.join(str(c) for c in dd.children)


def get_first_description_paragraph(opcode):
    p = opcode.find('dd').find('p')
    return p.text if p else ''


def parse(f):
    doc = BeautifulSoup(f, 'html.parser')
    table = doc.find('section', {'id': 'python-bytecode-instructions'})

    opcodes = table.findAll('dl', {'class': 'std opcode'})
    instructions = []
    for opcode in opcodes:
        opcode_name = opcode.find('dt').find('span').text
        opcode_tooltip = get_first_description_paragraph(opcode)
        opcode_desc = get_description(opcode)
        instructions.append(Instruction(
            opcode_name,
            [opcode_name],
            opcode_tooltip,
            opcode_desc,
        ))
    return instructions


def parse_html(directory):
    print("Parsing instructions...")
    instructions = []
    try:
        with open(os.path.join(directory, ARCHIVE_NAME), encoding='utf-8') as f:
            instructions = parse(f)
    except Exception as e:
        print(f"Error parsing {ARCHIVE_NAME}:\n{e}")
        sys.exit(1)

    return instructions


def main():
    args = parser.parse_args()
    print(f"Called with: {args}")
    # If we don't have the html folder already...
    if not os.path.exists(os.path.join(args.inputfolder, ARCHIVE_NAME)):
        try:
            download_asm_doc_archive(args.downloadfolder)
        except IOError as e:
            print("Error when downloading archive:")
            print(e)
            sys.exit(1)
    instructions = parse_html(args.inputfolder)
    instructions.sort(key=lambda b: b.name)
    all_inst = set()
    for inst in instructions:
        if not all_inst.isdisjoint(inst.names):
            print(f"Overlap in instruction names: {inst.names.intersection(all_inst)} for {inst.name}")
        all_inst = all_inst.union(inst.names)
    print(f"Writing {len(instructions)} instructions")
    with open(args.outputpath, 'w') as f:
        f.write("""
import {AssemblyInstructionInfo} from '../base.js';

export function getAsmOpcode(opcode: string | undefined): AssemblyInstructionInfo | undefined {
    if (!opcode) return;
    switch (opcode.toUpperCase()) {
""".lstrip())
        for inst in instructions:
            for name in sorted(inst.names):
                f.write(f'        case "{name}":\n')
            f.write('            return {}'.format(json.dumps({
                "tooltip": inst.tooltip,
                "html": inst.body,
                "url": get_url_for_instruction(inst)
            }, indent=16, separators=(',', ': '), sort_keys=True))[:-1] + '            };\n\n')
        f.write("""
    }
}
""")


if __name__ == '__main__':
    main()
