#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (c) 2024 Arm Limited.
#
# SPDX-License-Identifier: MIT
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""
Updates the Doxygen documentation pages with a table of operators supported by Compute Library.

The script builds up a table in XML format internally containing the different operators and their respective supported
compute backends, data types and layouts, and the equivalent operator in the Android Neural Networks API. The list of
operators is pulled from the OperatorList.h header file and further implementation details are provided in the function
headers for the backend-specific operator e.g., NEStridedSlice.h.

Usage:
    python update_supported_ops.py
"""

import argparse
import logging
import re
from enum import Enum
from pathlib import Path


class States(Enum):
    INIT = 0
    DESCRIPTION = 1
    DESCRIPTION_END = 2
    IN_CLASS = 3
    DATA_TYPE_START = 4
    DATA_TYPE_END = 5
    NN_OPERATOR = 6
    NN_OPERATOR_END = 7
    SKIP_OPERATOR = 8
    DATA_LAYOUT_START = 9
    DATA_LAYOUT_END = 10


class OperatorsTable:
    def __init__(self):
        self.project_dir = Path(__file__).resolve().parents[1]  # ComputeLibrary directory
        self.xml = ""

    def generate_operator_list(self):
        operator_list_head_file = self.project_dir / "arm_compute" / "runtime" / "OperatorList.h"
        neon_file_name_prefix = str(self.project_dir / "arm_compute" / "runtime" / "NEON" / "functions" / "NE")
        cl_file_name_prefix = str(self.project_dir / "arm_compute" / "runtime" / "CL" / "functions" / "CL")

        logging.debug(operator_list_head_file)

        f = open(operator_list_head_file, 'r')
        # Iterates over the lines of the file
        state = States.INIT
        operator_desc = ""
        nn_op_list = []
        for line in f:
            # /** ActivationLayer
            # *
            # * Description:
            # * Function to simulate an activation layer with the specified activation function.
            # *
            # * Equivalent Android NNAPI Op:
            # * ANEURALNETWORKS_ELU
            # * ANEURALNETWORKS_HARD_SWISH
            # * ANEURALNETWORKS_LOGISTIC
            # * ANEURALNETWORKS_RELU
            # * ANEURALNETWORKS_RELU1
            # * ANEURALNETWORKS_RELU6
            # * ANEURALNETWORKS_TANH
            # *
            # */
            # Check for "/**" of the start of the operator
            r = re.search('^\s*/\*\*(.*)', line)
            if r and state == States.INIT:
                # Skip below ones
                if re.search('.*\(not ported\)', line):
                    state = States.SKIP_OPERATOR
                    continue
                if re.search('.*\(only CL\)', line):
                    state = States.SKIP_OPERATOR
                    continue
                if re.search('.*\(no CL\)', line):
                    state = States.SKIP_OPERATOR
                    continue
                if re.search('.*\(skip\)', line):
                    state = States.SKIP_OPERATOR
                    continue
            # Check" */"
            r = re.match('\s*\*/\s*$', line)
            if r and state == States.SKIP_OPERATOR:
                state = States.INIT
                continue
            # Check " *"
            r = re.match('\s*\*\s*$', line)
            if r and state == States.SKIP_OPERATOR:
                continue
            # Check non " *" lines
            r = re.search('^\s*\*(.*)', line)
            if r and state == States.SKIP_OPERATOR:
                continue

            # Check for "/**" of the start of the operator
            r = re.search('^\s*/\*\*(.*)', line)
            if r and state == States.INIT:
                tmp = r.groups()[0]
                class_name = tmp.strip()
                logging.debug(class_name)
                continue

            # Check whether "Description: " exists
            r = re.search('\s*\*\s*Description:\s*', line)
            if r and state == States.INIT:
                state = States.DESCRIPTION
                continue
            # Treat description ends with a blank line only with " *"
            r = re.match('\s*\*\s*$', line)
            if r and state == States.DESCRIPTION:
                logging.debug(operator_desc)
                state = States.DESCRIPTION_END
                continue
            # Find continuing class description in the following lines
            r = re.search('^\s*\*(.*)', line)
            if r and state == States.DESCRIPTION:
                tmp = r.groups()[0]
                operator_desc = operator_desc + ' ' + tmp.strip()
                continue

            # Check whether "Equivalent AndroidNN Op: " exists
            r = re.search('\s*\*\s*Equivalent Android NNAPI Op:\s*', line)
            if r and state == States.DESCRIPTION_END:
                state = States.NN_OPERATOR
                continue
            # Treat AndroidNN Op ends with a blank line only with " *"
            r = re.match('\s*\*\s*$', line)
            if r and state == States.NN_OPERATOR:
                logging.debug(nn_op_list)
                state = States.NN_OPERATOR_END
                # Check NE#class_name
                neon_file_name = neon_file_name_prefix + class_name + ".h"
                logging.debug(neon_file_name)
                # Check CL#class_name
                cl_file_name = cl_file_name_prefix + class_name + ".h"
                logging.debug(cl_file_name)
                # Check whether CL/Neon file exists
                if Path(neon_file_name).is_file() and Path(cl_file_name).is_file():
                    if neon_file_name.find("NEElementwiseOperations.h") != -1:
                        logging.debug(neon_file_name)
                        self.generate_operator_common_info(class_name, operator_desc, nn_op_list, "13")
                    elif neon_file_name.find("NEElementwiseUnaryLayer.h") != -1:
                        logging.debug(neon_file_name)
                        self.generate_operator_common_info(class_name, operator_desc, nn_op_list, "8")
                    else:
                        self.generate_operator_common_info(class_name, operator_desc, nn_op_list, "2")
                    self.generate_operator_info(neon_file_name)
                    self.generate_operator_cl_begin()
                    self.generate_operator_info(cl_file_name)
                else:
                    if neon_file_name.find("NELogical.h") != -1:
                        logging.debug(neon_file_name)
                        self.generate_operator_common_info(class_name, operator_desc, nn_op_list, "3")
                    else:
                        self.generate_operator_common_info(class_name, operator_desc, nn_op_list, "1")
                    if Path(neon_file_name).is_file():
                        self.generate_operator_info(neon_file_name)
                    if Path(cl_file_name).is_file():
                        self.generate_operator_info(cl_file_name)
                continue

            # Find continuing AndroidNN Op in the following lines
            r = re.search('^\s*\*(.*)', line)
            if r and state == States.NN_OPERATOR:
                tmp = r.groups()[0]
                nn_op = tmp.strip()
                nn_op_list.append(nn_op)
                continue

            # Treat operator ends with a blank line only with " */"
            r = re.match('\s*\*/\s*$', line)
            if r and state == States.NN_OPERATOR_END:
                operator_desc = ""
                nn_op_list = []
                state = States.INIT
                continue
        f.close()

    def generate_operator_info(self, file_name):
        logging.debug(file_name)
        f = open(file_name, 'r')
        # iterates over the lines of the file
        state = States.INIT
        data_type_list = []
        data_layout_list = []
        io_list = []
        class_no = 0
        for line in f:
            # Locate class definition by "class...: public IFunction",
            # There are also exceptions, which will need to support in later version
            r = re.match("\s*class\s+(\S+)\s*:\s*(public)*", line)
            if r and state == States.INIT:
                class_name = r.groups()[0]
                logging.debug("class name is %s" % (class_name))
                state = States.IN_CLASS
                continue

            r = re.match("\s*\}\;", line)
            if r and state == States.IN_CLASS:
                state = States.INIT
                continue

            # * Valid data layouts:
            # * - All
            r = re.search('\s*\*\s*Valid data layouts:', line)
            if r and state == States.IN_CLASS:
                state = States.DATA_LAYOUT_START
                continue
            # Treat data configuration ends with a blank line only with " *"
            r = re.match('\s*\*\s*$', line)
            if r and state == States.DATA_LAYOUT_START:
                state = States.DATA_LAYOUT_END
                continue
            # Data layout continues
            r = re.search('\s*\*\s*\-\s*(.*)', line)
            if r and state == States.DATA_LAYOUT_START:
                tmp = r.groups()[0]
                tmp = tmp.strip()
                logging.debug(tmp)
                data_layout_list.append(tmp)

            # * Valid data type configurations:
            # * |src0           |dst            |
            # * |:--------------|:--------------|
            # * |QASYMM8        |QASYMM8        |
            # * |QASYMM8_SIGNED |QASYMM8_SIGNED |
            # * |QSYMM16        |QSYMM16        |
            # * |F16            |F16            |
            # * |F32            |F32            |
            r = re.search('\s*\*\s*Valid data type configurations:\s*', line)
            if r and state == States.DATA_LAYOUT_END:
                state = States.DATA_TYPE_START
                logging.debug(line)
                continue
            # Treat data configuration ends with a blank line only with " *"
            r = re.match('\s*\*\s*$', line)
            if r and state == States.DATA_TYPE_START:
                logging.debug(class_name)
                logging.debug(data_layout_list)
                logging.debug(io_list)
                logging.debug(data_type_list)
                class_no = class_no + 1
                if class_no > 1:
                    logging.debug(class_no)
                    self.generate_operator_cl_begin()
                self.generate_operator_dl_dt_info(class_name, data_layout_list, io_list, data_type_list)
                state = States.INIT
                data_type_list = []
                data_layout_list = []
                continue
            # Data type continues
            r = re.search('\s*\*(.*)', line)
            if r and state == States.DATA_TYPE_START:
                tmp = r.groups()[0]
                tmp = tmp.strip()
                if re.search('\|\:\-\-\-', tmp):
                    # Skip the table split row "|:-----"
                    continue
                else:
                    tmp = tmp.strip()
                    if re.search('.*(src|input|dst)', tmp):
                        io_list = tmp.split('|')
                    else:
                        data_type = tmp.split('|')
                        logging.debug(data_type)
                        data_type_list.append(data_type)
                    continue

        f.close()

    def generate_operator_cl_begin(self):
        self.xml += "<tr>\n"

    def generate_operator_common_info(self, class_name, operator_desc, nn_op_list, rowspan):
        tmp = "<tr>\n"
        # Store class name
        tmp += "  <td rowspan=\"" + rowspan + "\">" + class_name + "\n"
        tmp += "  <td rowspan=\"" + rowspan + "\" style=\"width:200px;\">" + operator_desc + "\n"
        tmp += "  <td rowspan=\"" + rowspan + "\">\n"
        tmp += "      <ul>\n"
        for item in nn_op_list:
            tmp += "       <li>"
            tmp += item.strip()
            tmp += "\n"
        tmp += "      </ul>\n"
        self.xml += tmp

    def generate_operator_dl_dt_info(self, class_name, data_layout, io_list, data_type_list):
        tmp = "  <td>" + class_name + "\n"
        # Store data layout info
        tmp += "  <td>\n"
        tmp += "      <ul>\n"
        for item in data_layout:
            tmp += "       <li>"
            tmp += item.strip()
            tmp += "\n"
        tmp += "      </ul>\n"
        tmp += "  <td>\n"
        # Store data type table
        tmp += "    <table>\n"
        tmp += "    <tr>"
        for io in io_list:
            # Make sure it's not empty string
            if len(io) != 0:
                tmp += "<th>"
                tmp += io.strip()
        tmp += "\n"
        for item in data_type_list:
            tmp += "    <tr>"
            for i in item:
                # Make sure it's not empty string
                if len(i) != 0:
                    tmp += "<td>"
                    tmp += i.strip()
            tmp += "\n"
        tmp += "    </table>\n"
        self.xml += tmp

    def generate_table_prefix(self):
        tmp = "<table>\n"
        tmp += "<caption id=\"multi_row\"></caption>\n"
        tmp += "<tr>\n"
        tmp += "  <th>Function\n"
        tmp += "  <th>Description\n"
        tmp += "  <th>Equivalent Android NNAPI Op\n"
        tmp += "  <th>Backends\n"
        tmp += "  <th>Data Layouts\n"
        tmp += "  <th>Data Types\n"
        self.xml += tmp

    def generate_table_ending(self):
        self.xml += "</table>\n"

    def dump_xml(self):
        print(self.xml)

    def update_dox_file(self):
        operator_list_dox = self.project_dir / "docs" / "user_guide" / "operator_list.dox"

        with open(operator_list_dox, "r") as f:
            dox_content = f.read()

        # Check that there is only one non-indented table (This table should be the operator list)
        x = re.findall("\n<table>", dox_content)
        y = re.findall("\n</table>", dox_content)
        if len(x) != 1 or len(y) != 1:
            raise RuntimeError("Invalid .dox file")

        repl_str = "\n" + self.xml[:-1]  # Extra / removed "\n" characters needed to make up for search regex
        new_file = re.sub("\n<table>(.|\n)*\n<\/table>", repl_str, dox_content)

        with open(operator_list_dox, "w") as f:
            f.write(new_file)
            print("Successfully updated operator_list.dox with the XML table of supported operators.")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Updates the Compute Library documentation with a table of supported operators."
    )
    parser.add_argument(
        "--dump_xml",
        type=bool,
        default=False,
        required=False,
        help="Dump the supported operators table XML to stdout",
    )
    parser.add_argument(
        "--debug",
        type=bool,
        default=False,
        required=False,
        help="Enables logging, helpful for debugging. Default: False",
    )
    args = parser.parse_args()

    if args.debug:
        logging.basicConfig(format="%(message)s", level=logging.DEBUG)

    table_xml = OperatorsTable()
    table_xml.generate_table_prefix()
    table_xml.generate_operator_list()
    table_xml.generate_table_ending()
    table_xml.update_dox_file()

    if args.dump_xml:
        table_xml.dump_xml()
