#!/usr/bin/python3
import sys
import lxml.etree as et
import argparse
import os
import subprocess
import re
import tempfile

def assert_valid_sb2_name(name):
    if not os.path.isdir(os.path.expanduser("~/.scratchbox2/%s" % name)):
        raise Exception("%s is not a valid scratchbox2 target name" % name)

def sb2_command(sb2, *args, stderr=None):
    data = subprocess.check_output(["sb2", "-t", sb2] + list(args), stderr=stderr, text=True)
    if data:
        return data.strip()
    raise Exception("'%s' returned no data" % " ".join(args))

def rpmvalidation_list_suites(target):
    data = subprocess.check_output(["rpmvalidation", "-t", target, "--list-suites"])
    return data.strip()

# GCC's system include paths are under the tooling. Map them to the respective
# directories under the target.
# See GccToolChain::gccHeaderPaths() in Qt Creator sources.
def process_gcc_dump_includes(dump):
    retv = []
    inside_search_list = False
    for line in dump.splitlines():
        if line.startswith("#include"):
            inside_search_list = True
        if not inside_search_list:
            continue
        if line.startswith("End of search list."):
            break

        if line.startswith(" /srv/mer/toolings/"):
            path = line.strip()
            realpath = os.path.realpath(path)
            replaced = re.sub(r"^/srv/mer/toolings/[^/]+/opt/cross/", "/opt/cross/", realpath)
            if replaced == realpath:
                raise Exception("Unhandled path in GCC includes dump: '%s'" % path)
            retv.append(" " + replaced)
        else:
            retv.append(line);
    return "\n".join(retv)

# See gccInstallDir in gcctoolchain.cpp in Qt Creator sources. We are only
# interested in "install" directory. The path is under tooling and needs to be
# mapped to the respective directory under the target.
def process_gcc_dump_install_dir(dump):
    prefix = "install: "
    line = dump.splitlines()[0]
    if not line.startswith(prefix):
        raise Exception("Unexpected content in GCC search dirs dump: '%s'" % line)

    path = line[len(prefix):]
    realpath = os.path.realpath(path)
    replaced = re.sub(r"^/srv/mer/toolings/[^/]+/opt/cross/", "/opt/cross/", realpath)
    if replaced == realpath:
        raise Exception("Unhandled path in GCC search dirs dump: '%s'" % path)

    return prefix + replaced

def get_xml(args):
    # http://lxml.de/FAQ.html#why-doesn-t-the-pretty-print-option-reformat-my-xml-output
    parser = et.XMLParser(remove_blank_text=True)
    try:
        tree = et.parse(args.target_xml, parser)
    except :
        print("Error loading %s, used new blank xml" % args.target_xml)
        with open(args.target_xml, "w") as f:
                f.write("""<targets version="4">
</targets>
""")
        tree = et.parse(args.target_xml, parser)

    root = tree.getroot()

    if root.tag != "targets":
        raise Exception("xml must have <targets> as root element")
    if int(root.get("version")) != 4:
        raise Exception("Only version 4 of MerTarget XML is supported")

    return root


def update(args, root):
    # Make sure the names are valid
    assert_valid_sb2_name(args.name)
    if args.origin:
        assert_valid_sb2_name(args.origin)

    # Create a brand new node so we don't leave any junk behind
    new = et.Element("target", name=args.name, origin=args.origin)

    # See if there is a target using this name already
    existing =  root.find(".//target[@name='%s']" % args.name)

    if et.iselement(existing):
        # replace existing node with new node
        root[root.index(existing)] = new
    else:
        root.append(new)

    # Now build the target info
    et.SubElement(new, "output", name="GccDumpMachine"
                  ).text = sb2_command(args.name, "gcc", "-dumpmachine")

    # Try to report the latest published standard this compiler version
    # supports - the assumption here is that published standards use proper
    # names.  Like c++11 vs c++0x or c++14 vs c++1y.
    for std in ["gnu++17", "gnu++14", "gnu++11", "gnu++03"]:
        try:
            macros = sb2_command(args.name, "gcc", "-x", "c++", "-std=" + std,
                    "-E", "-dM", "-D_REENTRANT", "-fPIC", "/dev/null")
            break
        except subprocess.CalledProcessError:
            continue
    if not macros:
        raise Exception("All attempts to dump GCC macros failed")
    et.SubElement(new, "output", name="GccDumpMacros").text = macros

    et.SubElement(new, "output", name="GccDumpIncludes"
                  ).text = process_gcc_dump_includes(
                          sb2_command(args.name, "gcc", "-x", "c++", "-E", "-v", "/dev/null",
                              stderr=subprocess.STDOUT))

    et.SubElement(new, "output", name="GccDumpInstallDir"
                  ).text = process_gcc_dump_install_dir(
                          sb2_command(args.name, "gcc", "-print-search-dirs"))

    et.SubElement(new, "output", name="QmakeQuery"
                  ).text = sb2_command(args.name, "qmake", "-query")

    # Older targets do not have CMake preinstalled
    try:
        et.SubElement(new, "output", name="CMakeCapabilities"
                      ).text = sb2_command(args.name, "cmake", "-E", "capabilities")
        et.SubElement(new, "output", name="CMakeVersion"
                      ).text = sb2_command(args.name, "cmake", "--version")
    except subprocess.CalledProcessError:
        pass

    et.SubElement(new, "output", name="RpmValidationSuites"
                  ).text = rpmvalidation_list_suites(args.name)

def delete(args, root):
    existing = root.find(".//target[@name='%s']" % args.name)
    if existing is not None:
        del root[root.index(existing)]
    else:
        print("target %s is not present" % args.name)

def save(args,root):
    # Save XML
    with tempfile.NamedTemporaryFile(mode='w', prefix=args.target_xml, delete=False) as f:
        f.write(et.tostring(root, encoding='unicode', pretty_print=True))
        f.close()
        os.rename(f.name, args.target_xml)


parser = argparse.ArgumentParser(description='Updates QtCreator Target XML file with a new Mer Target')

parser.add_argument('--name', required=True,
                    help="Name of SDK Target (snapshot) to add/update (sb2 name)")
parser.add_argument('--origin',
                    help="Name of the corresponding original SDK Target in case of a snapshot (sb2 name)")
parser.add_argument('--delete', action='store_true',
                    help="Delete named SDK Target from xml")
parser.add_argument('--target-xml', required=True,
                    help="The QtCreator Targets xml file")

args = parser.parse_args()

root = get_xml(args)

if args.delete:
    delete(args, root)
else:
    update(args, root)

save(args, root)
