#!/usr/bin/env python3
#
# Copyright 2019 - The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""native_module_info

Module Info class used to hold cached module_bp_cc_deps.json.
"""

import logging
import os
import re

from aidegen import constant
from aidegen.lib import common_util
from aidegen.lib import module_info

_CLANG = 'clang'
_CPPLANG = 'clang++'
_MODULES = 'modules'
_INCLUDE_TAIL = '_genc++_headers'
_SRC_GEN_CHECK = r'^out/soong/.intermediates/.+/gen/.+\.(c|cc|cpp)'
_INC_GEN_CHECK = r'^out/soong/.intermediates/.+/gen($|/.+)'


class NativeModuleInfo(module_info.AidegenModuleInfo):
    """Class that offers fast/easy lookup for module related details.

    Class Attributes:
        c_lang_path: Make C files compiler path.
        cpp_lang_path: Make C++ files compiler path.
    """

    c_lang_path = ''
    cpp_lang_path = ''

    def __init__(self, force_build=False, module_file=None):
        """Initialize the NativeModuleInfo object.

        Load up the module_bp_cc_deps.json file and initialize the helper vars.
        """
        if not module_file:
            module_file = common_util.get_blueprint_json_path(
                constant.BLUEPRINT_CC_JSONFILE_NAME)
        if not os.path.isfile(module_file):
            force_build = True
        super().__init__(force_build, module_file)

    def _load_module_info_file(self, force_build, module_file):
        """Load the module file.

        Args:
            force_build: Boolean to indicate if we should rebuild the
                         module_info file regardless if it's created or not.
            module_file: String of path to file to load up. Used for testing.

        Returns:
            Tuple of module_info_target and dict of json.
        """
        if force_build:
            self._discover_mod_file_and_target(True)
        mod_info = common_util.get_json_dict(module_file)
        NativeModuleInfo.c_lang_path = mod_info.get(_CLANG, '')
        NativeModuleInfo.cpp_lang_path = mod_info.get(_CPPLANG, '')
        name_to_module_info = mod_info.get(_MODULES, {})
        root_dir = common_util.get_android_root_dir()
        module_info_target = os.path.relpath(module_file, root_dir)
        return module_info_target, name_to_module_info

    def get_module_names_in_targets_paths(self, targets):
        """Gets module names exist in native_module_info.

        Args:
            targets: A list of build targets to be checked.

        Returns:
            A list of native projects' names if native projects exist otherwise
            return None.
        """
        projects = []
        for target in targets:
            if target == constant.WHOLE_ANDROID_TREE_TARGET:
                print('Do not deal with whole source tree in native projects.')
                continue
            rel_path, _ = common_util.get_related_paths(self, target)
            for path in self.path_to_module_info:
                if common_util.is_source_under_relative_path(path, rel_path):
                    projects.extend(self.get_module_names(path))
        return projects

    def get_module_includes(self, mod_name):
        """Gets module's include paths from module name.

        The include paths contain in 'header_search_path' and
        'system_search_path' of all flags in native module info.

        Args:
            mod_name: A string of module name.

        Returns:
            A set of module include paths relative to android root.
        """
        includes = set()
        mod_info = self.name_to_module_info.get(mod_name, {})
        if not mod_info:
            logging.warning('%s module name %s does not exist.',
                            common_util.COLORED_INFO('Warning:'), mod_name)
            return includes
        for flag in mod_info:
            for header in (constant.KEY_HEADER, constant.KEY_SYSTEM):
                if header in mod_info[flag]:
                    includes.update(set(mod_info[flag][header]))
        return includes

    def is_module_need_build(self, mod_name):
        """Checks if a module need to be built by its module name.

        If a module's source files or include files contain a path looks like,
        'out/soong/.intermediates/../gen/sysprop/charger.sysprop.cpp' or
        'out/soong/.intermediates/../android.bufferhub@1.0_genc++_headers/gen'
        and the paths do not exist, that means the module needs to be built to
        generate relative source or include files.

        Args:
            mod_name: A string of module name.

        Returns:
            A boolean, True if it needs to be generated else False.
        """
        mod_info = self.name_to_module_info.get(mod_name, {})
        if not mod_info:
            logging.warning('%s module name %s does not exist.',
                            common_util.COLORED_INFO('Warning:'), mod_name)
            return False
        if self._is_source_need_build(mod_info):
            return True
        if self._is_include_need_build(mod_info):
            return True
        return False

    def _is_source_need_build(self, mod_info):
        """Checks if a module's source files need to be built.

        If a module's source files contain a path looks like,
        'out/soong/.intermediates/../gen/sysprop/charger.sysprop.cpp'
        and the paths do not exist, that means the module needs to be built to
        generate relative source files.

        Args:
            mod_info: A dictionary of module info to check.

        Returns:
            A boolean, True if it needs to be generated else False.
        """
        if constant.KEY_SRCS not in mod_info:
            return False
        for src in mod_info[constant.KEY_SRCS]:
            if re.search(_INC_GEN_CHECK, src) and not os.path.isfile(src):
                return True
        return False

    def _is_include_need_build(self, mod_info):
        """Checks if a module needs to be built by its module name.

        If a module's include files contain a path looks like,
        'out/soong/.intermediates/../android.bufferhub@1.0_genc++_headers/gen'
        and the paths do not exist, that means the module needs to be built to
        generate relative include files.

        Args:
            mod_info: A dictionary of module info to check.

        Returns:
            A boolean, True if it needs to be generated else False.
        """
        for flag in mod_info:
            for header in (constant.KEY_HEADER, constant.KEY_SYSTEM):
                if header not in mod_info[flag]:
                    continue
                for include in mod_info[flag][header]:
                    match = re.search(_INC_GEN_CHECK, include)
                    if match and not os.path.isdir(include):
                        return True
        return False

    def is_suite_in_compatibility_suites(self, suite, mod_info):
        """Check if suite exists in the compatibility_suites of module-info.

        Args:
            suite: A string of suite name.
            mod_info: Dict of module info to check.

        Returns:
            True if it exists in mod_info, False otherwise.
        """
        raise NotImplementedError()

    def get_testable_modules(self, suite=None):
        """Return the testable modules of the given suite name.

        Args:
            suite: A string of suite name. Set to None to return all testable
            modules.

        Returns:
            List of testable modules. Empty list if non-existent.
            If suite is None, return all the testable modules in module-info.
        """
        raise NotImplementedError()

    def is_testable_module(self, mod_info):
        """Check if module is something we can test.

        A module is testable if:
          - it's installed, or
          - it's a robolectric module (or shares path with one).

        Args:
            mod_info: Dict of module info to check.

        Returns:
            True if we can test this module, False otherwise.
        """
        raise NotImplementedError()

    def has_test_config(self, mod_info):
        """Validate if this module has a test config.

        A module can have a test config in the following manner:
          - AndroidTest.xml at the module path.
          - test_config be set in module-info.json.
          - Auto-generated config via the auto_test_config key in
            module-info.json.

        Args:
            mod_info: Dict of module info to check.

        Returns:
            True if this module has a test config, False otherwise.
        """
        raise NotImplementedError()

    def get_robolectric_test_name(self, module_name):
        """Returns runnable robolectric module name.

        There are at least 2 modules in every robolectric module path, return
        the module that we can run as a build target.

        Arg:
            module_name: String of module.

        Returns:
            String of module that is the runnable robolectric module, None if
            none could be found.
        """
        raise NotImplementedError()

    def is_robolectric_test(self, module_name):
        """Check if module is a robolectric test.

        A module can be a robolectric test if the specified module has their
        class set as ROBOLECTRIC (or shares their path with a module that does).

        Args:
            module_name: String of module to check.

        Returns:
            True if the module is a robolectric module, else False.
        """
        raise NotImplementedError()

    def is_auto_gen_test_config(self, module_name):
        """Check if the test config file will be generated automatically.

        Args:
            module_name: A string of the module name.

        Returns:
            True if the test config file will be generated automatically.
        """
        raise NotImplementedError()

    def is_robolectric_module(self, mod_info):
        """Check if a module is a robolectric module.

        Args:
            mod_info: ModuleInfo to check.

        Returns:
            True if module is a robolectric module, False otherwise.
        """
        raise NotImplementedError()

    def is_native_test(self, module_name):
        """Check if the input module is a native test.

        Args:
            module_name: A string of the module name.

        Returns:
            True if the test is a native test, False otherwise.
        """
        raise NotImplementedError()