"""
comes from:
https://github.com/eriwen/lcov-to-cobertura-xml/blob/master/lcov_cobertura/lcov_cobertura.py
"""
import re
import sys
import os
import time
import subprocess
from xml.dom import minidom
from optparse import OptionParser

from distutils.spawn import find_executable

CPPFILT = "c++filt"
HAVE_CPPFILT = False

if find_executable(CPPFILT) is not None:
    HAVE_CPPFILT = True

VERSION = "1.6"
__all__ = ["LcovCobertura"]


class Demangler(object):
    def __init__(self):
        self.pipe = subprocess.Popen(
            CPPFILT, stdin=subprocess.PIPE, stdout=subprocess.PIPE
        )

    def demangle(self, name):
        newname = name + "\n"
        self.pipe.stdin.write(newname.encode("utf-8"))
        res = self.pipe.stdout.readline()
        return str(res.rstrip())


class LcovCobertura(object):
    """
    Converts code coverage report files in lcov format to Cobertura's XML
    report format so that CI servers like Jenkins can aggregate results and
    determine build stability etc.

    >>> from lcov_cobertura import LcovCobertura
    >>> LCOV_INPUT = 'your lcov input'
    >>> converter = LcovCobertura(LCOV_INPUT)
    >>> cobertura_xml = converter.convert()
    >>> print(cobertura_xml)
    """

    def __init__(self, lcov_data, base_dir=".", excludes=None, demangle=False):
        """
        Create a new :class:`LcovCobertura` object using the given `lcov_data`
        and `options`.

        :param lcov_data: Path to LCOV data file
        :type lcov_data: string
        :param base_dir: Path upon which to base all sources
        :type base_dir: string
        :param excludes: list of regexes to packages as excluded
        :type excludes: [string]
        :param demangle: whether to demangle function names using c++filt
        :type demangle: bool
        """

        if not excludes:
            excludes = []
        self.lcov_data = lcov_data
        self.base_dir = base_dir
        self.excludes = excludes
        if demangle:
            demangler = Demangler()
            self.format = demangler.demangle
        else:
            self.format = lambda x: x

    def convert(self):
        """
        Convert lcov file to cobertura XML using options from this instance.
        """
        coverage_data = self.parse()
        return self.generate_cobertura_xml(coverage_data)

    def parse(self):
        """
        Generate a data structure representing it that can be serialized in any
        logical format.
        """

        coverage_data = {
            "packages": {},
            "summary": {
                "lines-total": 0,
                "lines-covered": 0,
                "branches-total": 0,
                "branches-covered": 0,
            },
            "timestamp": str(int(time.time())),
        }
        package = None
        current_file = None
        file_lines_total = 0
        file_lines_covered = 0
        file_lines = {}
        file_methods = {}
        file_branches_total = 0
        file_branches_covered = 0

        for line in self.lcov_data.split("\n"):
            if line.strip() == "end_of_record":
                if current_file is not None:
                    package_dict = coverage_data["packages"][package]
                    package_dict["lines-total"] += file_lines_total
                    package_dict["lines-covered"] += file_lines_covered
                    package_dict["branches-total"] += file_branches_total
                    package_dict["branches-covered"] += file_branches_covered
                    file_dict = package_dict["classes"][current_file]
                    file_dict["lines-total"] = file_lines_total
                    file_dict["lines-covered"] = file_lines_covered
                    file_dict["lines"] = dict(file_lines)
                    file_dict["methods"] = dict(file_methods)
                    file_dict["branches-total"] = file_branches_total
                    file_dict["branches-covered"] = file_branches_covered
                    coverage_data["summary"]["lines-total"] += file_lines_total
                    coverage_data["summary"]["lines-covered"] += file_lines_covered
                    coverage_data["summary"]["branches-total"] += file_branches_total
                    coverage_data["summary"][
                        "branches-covered"
                    ] += file_branches_covered

            line_parts = line.split(":", 1)
            input_type = line_parts[0]

            if input_type == "SF":
                # Get file name
                file_name = line_parts[-1].strip()
                relative_file_name = os.path.relpath(file_name, self.base_dir)
                package = ".".join(relative_file_name.split(os.path.sep)[0:-1])
                class_name = ".".join(relative_file_name.split(os.path.sep))
                if package not in coverage_data["packages"]:
                    coverage_data["packages"][package] = {
                        "classes": {},
                        "lines-total": 0,
                        "lines-covered": 0,
                        "branches-total": 0,
                        "branches-covered": 0,
                    }
                coverage_data["packages"][package]["classes"][relative_file_name] = {
                    "name": class_name,
                    "lines": {},
                    "lines-total": 0,
                    "lines-covered": 0,
                    "branches-total": 0,
                    "branches-covered": 0,
                }
                package = package
                current_file = relative_file_name
                file_lines_total = 0
                file_lines_covered = 0
                file_lines.clear()
                file_methods.clear()
                file_branches_total = 0
                file_branches_covered = 0
            elif input_type == "DA":
                # DA:2,0
                (line_number, line_hits) = line_parts[-1].strip().split(",")
                line_number = int(line_number)
                if line_number not in file_lines:
                    file_lines[line_number] = {
                        "branch": "false",
                        "branches-total": 0,
                        "branches-covered": 0,
                    }
                file_lines[line_number]["hits"] = line_hits
                # Increment lines total/covered for class and package
                try:
                    if int(line_hits) > 0:
                        file_lines_covered += 1
                except:
                    pass
                file_lines_total += 1
            elif input_type == "BRDA":
                # BRDA:1,1,2,0
                (line_number, block_number, branch_number, branch_hits) = (
                    line_parts[-1].strip().split(",")
                )
                line_number = int(line_number)
                if line_number not in file_lines:
                    file_lines[line_number] = {
                        "branch": "true",
                        "branches-total": 0,
                        "branches-covered": 0,
                        "hits": 0,
                    }
                file_lines[line_number]["branch"] = "true"
                file_lines[line_number]["branches-total"] += 1
                file_branches_total += 1
                if branch_hits != "-" and int(branch_hits) > 0:
                    file_lines[line_number]["branches-covered"] += 1
                    file_branches_covered += 1
            elif input_type == "BRF":
                file_branches_total = int(line_parts[1])
            elif input_type == "BRH":
                file_branches_covered = int(line_parts[1])
            elif input_type == "FN":
                # FN:5,(anonymous_1)
                function_line, function_name = line_parts[-1].strip().split(",")
                file_methods[function_name] = [function_line, "0"]
            elif input_type == "FNDA":
                # FNDA:0,(anonymous_1)
                (function_hits, function_name) = line_parts[-1].strip().split(",")
                if function_name not in file_methods:
                    file_methods[function_name] = ["0", "0"]
                file_methods[function_name][-1] = function_hits

        # Exclude packages
        excluded = [
            x
            for x in coverage_data["packages"]
            for e in self.excludes
            if re.match(e, x)
        ]
        for package in excluded:
            del coverage_data["packages"][package]

        # Compute line coverage rates
        for package_data in list(coverage_data["packages"].values()):
            package_data["line-rate"] = self._percent(
                package_data["lines-total"], package_data["lines-covered"]
            )
            package_data["branch-rate"] = self._percent(
                package_data["branches-total"], package_data["branches-covered"]
            )

        return coverage_data

    def generate_cobertura_xml(self, coverage_data):
        """
        Given parsed coverage data, return a String cobertura XML representation.

        :param coverage_data: Nested dict representing coverage information.
        :type coverage_data: dict
        """

        dom_impl = minidom.getDOMImplementation()
        doctype = dom_impl.createDocumentType(
            "coverage", None, "http://cobertura.sourceforge.net/xml/coverage-04.dtd"
        )
        document = dom_impl.createDocument(None, "coverage", doctype)
        root = document.documentElement
        summary = coverage_data["summary"]
        self._attrs(
            root,
            {
                "branch-rate": self._percent(
                    summary["branches-total"], summary["branches-covered"]
                ),
                "branches-covered": str(summary["branches-covered"]),
                "branches-valid": str(summary["branches-total"]),
                "complexity": "0",
                "line-rate": self._percent(
                    summary["lines-total"], summary["lines-covered"]
                ),
                "lines-covered": str(summary["lines-covered"]),
                "lines-valid": str(summary["lines-total"]),
                "timestamp": coverage_data["timestamp"],
                "version": "1.0",
            },
        )

        sources = self._el(document, "sources", {})
        source = self._el(document, "source", {})
        source.appendChild(document.createTextNode(self.base_dir))
        sources.appendChild(source)

        root.appendChild(sources)

        packages_el = self._el(document, "packages", {})

        packages = coverage_data["packages"]
        for package_name, package_data in list(packages.items()):
            package_el = self._el(
                document,
                "package",
                {
                    "line-rate": package_data["line-rate"],
                    "branch-rate": package_data["branch-rate"],
                    "name": package_name,
                    "complexity": "0",
                },
            )
            classes_el = self._el(document, "classes", {})
            for class_name, class_data in list(package_data["classes"].items()):
                class_el = self._el(
                    document,
                    "class",
                    {
                        "branch-rate": self._percent(
                            class_data["branches-total"], class_data["branches-covered"]
                        ),
                        "complexity": "0",
                        "filename": class_name,
                        "line-rate": self._percent(
                            class_data["lines-total"], class_data["lines-covered"]
                        ),
                        "name": class_data["name"],
                    },
                )

                # Process methods
                methods_el = self._el(document, "methods", {})
                for method_name, (line, hits) in list(class_data["methods"].items()):
                    method_el = self._el(
                        document,
                        "method",
                        {
                            "name": self.format(method_name),
                            "signature": "",
                            "line-rate": "1.0" if int(hits) > 0 else "0.0",
                            "branch-rate": "1.0" if int(hits) > 0 else "0.0",
                        },
                    )
                    method_lines_el = self._el(document, "lines", {})
                    method_line_el = self._el(
                        document,
                        "line",
                        {
                            "hits": hits,
                            "number": line,
                            "branch": "false",
                        },
                    )
                    method_lines_el.appendChild(method_line_el)
                    method_el.appendChild(method_lines_el)
                    methods_el.appendChild(method_el)

                # Process lines
                lines_el = self._el(document, "lines", {})
                lines = list(class_data["lines"].keys())
                lines.sort()
                for line_number in lines:
                    line_el = self._el(
                        document,
                        "line",
                        {
                            "branch": class_data["lines"][line_number]["branch"],
                            "hits": str(class_data["lines"][line_number]["hits"]),
                            "number": str(line_number),
                        },
                    )
                    if class_data["lines"][line_number]["branch"] == "true":
                        total = int(class_data["lines"][line_number]["branches-total"])
                        covered = int(
                            class_data["lines"][line_number]["branches-covered"]
                        )
                        percentage = int((covered * 100.0) / total)
                        line_el.setAttribute(
                            "condition-coverage",
                            "{0}% ({1}/{2})".format(percentage, covered, total),
                        )
                    lines_el.appendChild(line_el)

                class_el.appendChild(methods_el)
                class_el.appendChild(lines_el)
                classes_el.appendChild(class_el)
            package_el.appendChild(classes_el)
            packages_el.appendChild(package_el)
        root.appendChild(packages_el)

        return document.toprettyxml()

    def _el(self, document, name, attrs):
        """
        Create an element within document with given name and attributes.

        :param document: Document element
        :type document: Document
        :param name: Element name
        :type name: string
        :param attrs: Attributes for element
        :type attrs: dict
        """
        return self._attrs(document.createElement(name), attrs)

    def _attrs(self, element, attrs):
        """
        Set attributes on given element.

        :param element: DOM Element
        :type element: Element
        :param attrs: Attributes for element
        :type attrs: dict
        """
        for attr, val in list(attrs.items()):
            element.setAttribute(attr, val)
        return element

    def _percent(self, lines_total, lines_covered):
        """
        Get the percentage of lines covered in the total, with formatting.

        :param lines_total: Total number of lines in given module
        :type lines_total: number
        :param lines_covered: Number of lines covered by tests in module
        :type lines_covered: number
        """

        if lines_total == 0:
            return "0.0"
        return str(float(float(lines_covered) / float(lines_total)))


def lcov2cobertura(data: str) -> str:
    converter = LcovCobertura(data)
    cobertura_xml = converter.convert()
    return cobertura_xml
