# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2024 Intel Corporation

import argparse
import json
import re

import ijson
import xlsxwriter
import yaml


class ComponentStats:
    def __init__(
        self,
        testsuites: int,
        runnable: int,
        build_only: int,
        sim_only: int,
        hw_only: int,
        mixed: int,
    ):
        self.testsuites = testsuites
        self.runnable = runnable
        self.build_only = build_only
        self.sim_only = sim_only
        self.hw_only = hw_only
        self.mixed = mixed


class Json_report:
    json_object = {"components": []}

    simulators = [
        'unit_testing',
        'native',
        'qemu',
        'mps2/an385',
    ]

    report_json = {}

    def __init__(self):
        args = parse_args()
        self.parse_testplan(args.testplan)
        self.maintainers_file = self.get_maintainers_file(args.maintainers)
        self.report_json = self.generate_json_report(args.coverage)

        if args.format == "json":
            self.save_json_report(args.output, self.report_json)
        elif args.format == "xlsx":
            self.generate_xlsx_report(self.report_json, args.output)
        elif args.format == "all":
            self.save_json_report(args.output, self.report_json)
            self.generate_xlsx_report(self.report_json, args.output)
        else:
            print("Format incorrect")

    def get_maintainers_file(self, maintainers):
        maintainers_file = ""
        with open(maintainers) as file:
            maintainers_file = yaml.safe_load(file)
            file.close()
        return maintainers_file

    def _parse_testcase(self, testsuite, testcase):
        if testcase['status'] == 'None':
            testcase_name = testsuite['name']
            component_name = testcase_name[: testcase_name.find('.')]
            component = {"name": component_name, "sub_components": [], "files": []}
            features = self.json_object['components']
            known_component_flag = False
            for item in features:
                if component_name == item['name']:
                    component = item
                    known_component_flag = True
                    break
            sub_component_name = testcase_name[testcase_name.find('.') :]
            sub_component_name = sub_component_name[1:]
            if idx := sub_component_name.find(".") > 0:
                sub_component_name = sub_component_name[:idx]
            if known_component_flag is False:
                sub_component = {"name": sub_component_name, "test_suites": []}
                test_suite = {
                    "name": testsuite['name'],
                    "path": testsuite['path'],
                    "platforms": [],
                    "runnable": testsuite['runnable'],
                    "status": "",
                    "test_cases": [],
                }
                test_case = {"name": testcase_name}
                if any(platform in testsuite['platform'] for platform in self.simulators):
                    if test_suite['status'] == "":
                        test_suite['status'] = 'sim_only'

                    if test_suite['status'] == 'hw_only':
                        test_suite['status'] = 'mixed'
                else:
                    if test_suite['status'] == "":
                        test_suite['status'] = 'hw_only'

                    if test_suite['status'] == 'sim_only':
                        test_suite['status'] = 'mixed'
                test_suite['test_cases'].append(test_case)
                test_suite['platforms'].append(testsuite['platform'])
                sub_component["test_suites"].append(test_suite)
                component['sub_components'].append(sub_component)
                self.json_object['components'].append(component)
            else:
                sub_component = {}
                sub_components = component['sub_components']
                known_sub_component_flag = False
                for i_sub_component in sub_components:
                    if sub_component_name == i_sub_component['name']:
                        sub_component = i_sub_component
                        known_sub_component_flag = True
                        break
                if known_sub_component_flag is False:
                    sub_component = {"name": sub_component_name, "test_suites": []}
                    test_suite = {
                        "name": testsuite['name'],
                        "path": testsuite['path'],
                        "platforms": [],
                        "runnable": testsuite['runnable'],
                        "status": "",
                        "test_cases": [],
                    }
                    test_case = {"name": testcase_name}
                    if any(platform in testsuite['platform'] for platform in self.simulators):
                        if test_suite['status'] == "":
                            test_suite['status'] = 'sim_only'

                        if test_suite['status'] == 'hw_only':
                            test_suite['status'] = 'mixed'
                    else:
                        if test_suite['status'] == "":
                            test_suite['status'] = 'hw_only'

                        if test_suite['status'] == 'sim_only':
                            test_suite['status'] = 'mixed'
                    test_suite['test_cases'].append(test_case)
                    test_suite['platforms'].append(testsuite['platform'])
                    sub_component["test_suites"].append(test_suite)
                    component['sub_components'].append(sub_component)
                else:
                    test_suite = {}
                    test_suites = sub_component['test_suites']
                    known_testsuite_flag = False
                    for i_testsuite in test_suites:
                        if testsuite['name'] == i_testsuite['name']:
                            test_suite = i_testsuite
                            known_testsuite_flag = True
                            break
                    if known_testsuite_flag is False:
                        test_suite = {
                            "name": testsuite['name'],
                            "path": testsuite['path'],
                            "platforms": [],
                            "runnable": testsuite['runnable'],
                            "status": "",
                            "test_cases": [],
                        }
                        test_case = {"name": testcase_name}
                        if any(platform in testsuite['platform'] for platform in self.simulators):
                            if test_suite['status'] == "":
                                test_suite['status'] = 'sim_only'

                            if test_suite['status'] == 'hw_only':
                                test_suite['status'] = 'mixed'
                        else:
                            if test_suite['status'] == "":
                                test_suite['status'] = 'hw_only'

                            if test_suite['status'] == 'sim_only':
                                test_suite['status'] = 'mixed'
                        test_suite['test_cases'].append(test_case)
                        test_suite['platforms'].append(testsuite['platform'])
                        sub_component["test_suites"].append(test_suite)
                    else:
                        if any(platform in testsuite['platform'] for platform in self.simulators):
                            if test_suite['status'] == "":
                                test_suite['status'] = 'sim_only'

                            if test_suite['status'] == 'hw_only':
                                test_suite['status'] = 'mixed'
                        else:
                            if test_suite['status'] == "":
                                test_suite['status'] = 'hw_only'

                            if test_suite['status'] == 'sim_only':
                                test_suite['status'] = 'mixed'
                        test_case = {}
                        test_cases = test_suite['test_cases']
                        known_testcase_flag = False
                        for i_testcase in test_cases:
                            if testcase_name == i_testcase['name']:
                                test_case = i_testcase
                                known_testcase_flag = True
                                break
                        if known_testcase_flag is False:
                            test_case = {"name": testcase_name}
                            test_suite['test_cases'].append(test_case)

    def parse_testplan(self, testplan_path):
        with open(testplan_path) as file:
            parser = ijson.items(file, 'testsuites')
            for element in parser:
                for testsuite in element:
                    for testcase in testsuite['testcases']:
                        self._parse_testcase(testsuite, testcase)

    def get_files_from_maintainers_file(self, component_name):
        files_path = []
        for item in self.maintainers_file:
            _found_flag = False
            try:
                tests = self.maintainers_file[item].get('tests', [])
                for i_test in tests:
                    if component_name in i_test:
                        _found_flag = True

                if _found_flag is True:
                    for path in self.maintainers_file[item]['files']:
                        path = path.replace('*', '.*')
                        files_path.append(path)
            except TypeError:
                print("ERROR: Fail while parsing MAINTAINERS file at %s", component_name)
        return files_path

    def _generate_component_report(self, element, component) -> dict:
        json_component = {}
        json_component["name"] = component["name"]
        json_component["sub_components"] = component["sub_components"]
        json_component["Comment"] = ""
        files_path = self.get_files_from_maintainers_file(component["name"])

        if len(files_path) == 0:
            json_component["files"] = []
            json_component["Comment"] = "Missed in maintainers.yml file."
            return json_component

        json_files = []
        for i_file in files_path:
            for covered_file in element:
                x = re.search(('.*' + i_file + '.*'), covered_file['file'])
                if not x:
                    continue

                file_name = covered_file['file'][covered_file['file'].rfind('/') + 1 :]
                file_path = covered_file['file']
                file_coverage, file_lines, file_hit = self._calculate_coverage_of_file(covered_file)
                json_file = {
                    "Name": file_name,
                    "Path": file_path,
                    "Lines": file_lines,
                    "Hit": file_hit,
                    "Coverage": file_coverage,
                    "Covered_Functions": [],
                    "Uncovered_Functions": [],
                }
                for i_fun in covered_file['functions']:
                    if i_fun['execution_count'] != 0:
                        json_covered_funciton = {"Name": i_fun['name']}
                        json_file['Covered_Functions'].append(json_covered_funciton)
                for i_fun in covered_file['functions']:
                    if i_fun['execution_count'] == 0:
                        json_uncovered_funciton = {"Name": i_fun['name']}
                        json_file['Uncovered_Functions'].append(json_uncovered_funciton)
                comp_exists = [x for x in json_files if x['Path'] == json_file['Path']]
                if not comp_exists:
                    json_files.append(json_file)
        json_component['files'] = json_files
        return json_component

    def generate_json_report(self, coverage):
        output_json = {"components": []}

        with open(coverage) as file:
            parser = ijson.items(file, 'files')
            for element in parser:
                for i_json_component in self.json_object['components']:
                    json_component = self._generate_component_report(element, i_json_component)
                    output_json['components'].append(json_component)

        return output_json

    def _calculate_coverage_of_file(self, file):
        tracked_lines = len(file['lines'])
        covered_lines = 0
        for line in file['lines']:
            if line['count'] != 0:
                covered_lines += 1
        return ((covered_lines / tracked_lines) * 100), tracked_lines, covered_lines

    def save_json_report(self, output_path, json_object):
        json_object = json.dumps(json_object, indent=4)
        with open(output_path + '.json', "w") as outfile:
            outfile.write(json_object)

    def _find_char(self, path, str, n):
        sep = path.split(str, n)
        if len(sep) <= n:
            return -1
        return len(path) - len(sep[-1]) - len(str)

    def _component_calculate_stats(self, json_component) -> ComponentStats:
        testsuites_count = 0
        runnable_count = 0
        build_only_count = 0
        sim_only_count = 0
        hw_only_count = 0
        mixed_count = 0
        for i_sub_component in json_component['sub_components']:
            for i_testsuit in i_sub_component['test_suites']:
                testsuites_count += 1
                if i_testsuit['runnable'] is True:
                    runnable_count += 1
                else:
                    build_only_count += 1

                if i_testsuit['status'] == "hw_only":
                    hw_only_count += 1
                elif i_testsuit['status'] == "sim_only":
                    sim_only_count += 1
                else:
                    mixed_count += 1
        return ComponentStats(
            testsuites_count,
            runnable_count,
            build_only_count,
            sim_only_count,
            hw_only_count,
            mixed_count,
        )

    def _xlsx_generate_summary_page(self, workbook, json_report):
        # formats
        header_format = workbook.add_format(
            {
                "bold": True,
                "fg_color": "#538DD5",
                "font_color": "white",
            }
        )
        cell_format = workbook.add_format(
            {
                'valign': 'vcenter',
            }
        )

        # generate summary page
        worksheet = workbook.add_worksheet('Summary')
        row = 0
        col = 0
        worksheet.write(row, col, "Components", header_format)
        worksheet.write(row, col + 1, "TestSuites", header_format)
        worksheet.write(row, col + 2, "Runnable", header_format)
        worksheet.write(row, col + 3, "Build only", header_format)
        worksheet.write(row, col + 4, "Simulators only", header_format)
        worksheet.write(row, col + 5, "Hardware only", header_format)
        worksheet.write(row, col + 6, "Mixed", header_format)
        worksheet.write(row, col + 7, "Coverage [%]", header_format)
        worksheet.write(row, col + 8, "Total Functions", header_format)
        worksheet.write(row, col + 9, "Uncovered Functions", header_format)
        worksheet.write(row, col + 10, "Comment", header_format)
        row = 1
        col = 0
        for item in json_report['components']:
            worksheet.write(row, col, item['name'], cell_format)
            stats = self._component_calculate_stats(item)
            worksheet.write(row, col + 1, stats.testsuites, cell_format)
            worksheet.write(row, col + 2, stats.runnable, cell_format)
            worksheet.write(row, col + 3, stats.build_only, cell_format)
            worksheet.write(row, col + 4, stats.sim_only, cell_format)
            worksheet.write(row, col + 5, stats.hw_only, cell_format)
            worksheet.write(row, col + 6, stats.mixed, cell_format)
            lines = 0
            hit = 0
            coverage = 0.0
            total_funs = 0
            uncovered_funs = 0
            for i_file in item['files']:
                lines += i_file['Lines']
                hit += i_file['Hit']
                total_funs += len(i_file['Covered_Functions']) + len(i_file['Uncovered_Functions'])
                uncovered_funs += len(i_file['Uncovered_Functions'])

            if lines != 0:
                coverage = (hit / lines) * 100

            worksheet.write_number(
                row, col + 7, coverage, workbook.add_format({'num_format': '#,##0.00'})
            )
            worksheet.write_number(row, col + 8, total_funs)
            worksheet.write_number(row, col + 9, uncovered_funs)
            worksheet.write(row, col + 10, item["Comment"], cell_format)
            row += 1
            col = 0
        worksheet.conditional_format(
            1,
            col + 7,
            row,
            col + 7,
            {
                'type': 'data_bar',
                'min_value': 0,
                'max_value': 100,
                'bar_color': '#3fd927',
                'bar_solid': True,
            },
        )
        worksheet.autofit()
        worksheet.set_default_row(15)

    def generate_xlsx_report(self, json_report, output):
        self.report_book = xlsxwriter.Workbook(output + ".xlsx")
        header_format = self.report_book.add_format(
            {"bold": True, "fg_color": "#538DD5", "font_color": "white"}
        )

        # Create a format to use in the merged range.
        merge_format = self.report_book.add_format(
            {
                "bold": 1,
                "align": "center",
                "valign": "vcenter",
                "fg_color": "#538DD5",
                "font_color": "white",
            }
        )
        cell_format = self.report_book.add_format({'valign': 'vcenter'})

        self._xlsx_generate_summary_page(self.report_book, self.report_json)
        row = 0
        col = 0
        for item in json_report['components']:
            worksheet = self.report_book.add_worksheet(item['name'])
            row = 0
            col = 0
            worksheet.write(row, col, "File Name", header_format)
            worksheet.write(row, col + 1, "File Path", header_format)
            worksheet.write(row, col + 2, "Coverage [%]", header_format)
            worksheet.write(row, col + 3, "Lines", header_format)
            worksheet.write(row, col + 4, "Hits", header_format)
            worksheet.write(row, col + 5, "Diff", header_format)
            row += 1
            col = 0
            for i_file in item['files']:
                worksheet.write(
                    row, col, i_file['Path'][i_file['Path'].rfind('/') + 1 :], cell_format
                )
                worksheet.write(
                    row,
                    col + 1,
                    i_file["Path"][(self._find_char(i_file["Path"], '/', 3) + 1) :],
                    cell_format,
                )
                worksheet.write_number(
                    row,
                    col + 2,
                    i_file["Coverage"],
                    self.report_book.add_format({'num_format': '#,##0.00'}),
                )
                worksheet.write(row, col + 3, i_file["Lines"], cell_format)
                worksheet.write(row, col + 4, i_file["Hit"], cell_format)
                worksheet.write(row, col + 5, i_file["Lines"] - i_file["Hit"], cell_format)
                row += 1
                col = 0
            row += 1
            col = 0
            worksheet.conditional_format(
                1,
                col + 2,
                row,
                col + 2,
                {
                    'type': 'data_bar',
                    'min_value': 0,
                    'max_value': 100,
                    'bar_color': '#3fd927',
                    'bar_solid': True,
                },
            )
            worksheet.merge_range(row, col, row, col + 2, "Uncovered Functions", merge_format)
            row += 1
            worksheet.write(row, col, 'Function Name', header_format)
            worksheet.write(row, col + 1, 'Implementation File', header_format)
            worksheet.write(row, col + 2, 'Comment', header_format)
            row += 1
            col = 0
            for i_file in item['files']:
                for i_uncov_fun in i_file['Uncovered_Functions']:
                    worksheet.write(row, col, i_uncov_fun["Name"], cell_format)
                    worksheet.write(
                        row,
                        col + 1,
                        i_file["Path"][self._find_char(i_file["Path"], '/', 3) + 1 :],
                        cell_format,
                    )
                    row += 1
                    col = 0
            row += 1
            col = 0
            worksheet.write(row, col, "Components", header_format)
            worksheet.write(row, col + 1, "Sub-Components", header_format)
            worksheet.write(row, col + 2, "TestSuites", header_format)
            worksheet.write(row, col + 3, "Runnable", header_format)
            worksheet.write(row, col + 4, "Build only", header_format)
            worksheet.write(row, col + 5, "Simulation only", header_format)
            worksheet.write(row, col + 6, "Hardware only", header_format)
            worksheet.write(row, col + 7, "Mixed", header_format)
            row += 1
            col = 0
            worksheet.write(row, col, item['name'], cell_format)
            for i_sub_component in item['sub_components']:
                testsuites_count = 0
                runnable_count = 0
                build_only_count = 0
                sim_only_count = 0
                hw_only_count = 0
                mixed_count = 0
                worksheet.write(row, col + 1, i_sub_component['name'], cell_format)
                for i_testsuit in i_sub_component['test_suites']:
                    testsuites_count += 1
                    if i_testsuit['runnable'] is True:
                        runnable_count += 1
                    else:
                        build_only_count += 1

                    if i_testsuit['status'] == "hw_only":
                        hw_only_count += 1
                    elif i_testsuit['status'] == "sim_only":
                        sim_only_count += 1
                    else:
                        mixed_count += 1
                worksheet.write(row, col + 2, testsuites_count, cell_format)
                worksheet.write(row, col + 3, runnable_count, cell_format)
                worksheet.write(row, col + 4, build_only_count, cell_format)
                worksheet.write(row, col + 5, sim_only_count, cell_format)
                worksheet.write(row, col + 6, hw_only_count, cell_format)
                worksheet.write(row, col + 7, mixed_count, cell_format)
                row += 1
                col = 0

            worksheet.autofit()
            worksheet.set_default_row(15)
        self.report_book.close()


def parse_args():
    parser = argparse.ArgumentParser(allow_abbrev=False)
    parser.add_argument(
        '-m', '--maintainers', help='Path to maintainers.yml [Required]', required=True
    )
    parser.add_argument('-t', '--testplan', help='Path to testplan [Required]', required=True)
    parser.add_argument(
        '-c', '--coverage', help='Path to components file [Required]', required=True
    )
    parser.add_argument('-o', '--output', help='Report name [Required]', required=True)
    parser.add_argument(
        '-f', '--format', help='Output format (json, xlsx, all) [Required]', required=True
    )

    args = parser.parse_args()
    return args


if __name__ == '__main__':
    Json_report()
