#!/usr/bin/env python3
#
# Copyright 2018 - 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.

"""module_info_util

This module receives a module path which is relative to its root directory and
makes a command to generate two json files, one for mk files and one for bp
files. Then it will load these two json files into two json dictionaries,
merge them into one dictionary and return the merged dictionary to its caller.

Example usage:
merged_dict = generate_merged_module_info()
"""

import glob
import logging
import os
import sys

from aidegen import constant
from aidegen.lib import common_util
from aidegen.lib import errors
from aidegen.lib import project_config

from atest import atest_utils

_MERGE_NEEDED_ITEMS = [
    constant.KEY_CLASS,
    constant.KEY_PATH,
    constant.KEY_INSTALLED,
    constant.KEY_DEPENDENCIES,
    constant.KEY_SRCS,
    constant.KEY_SRCJARS,
    constant.KEY_CLASSES_JAR,
    constant.KEY_TAG,
    constant.KEY_COMPATIBILITY,
    constant.KEY_AUTO_TEST_CONFIG,
    constant.KEY_MODULE_NAME,
    constant.KEY_TEST_CONFIG
]
_INTELLIJ_PROJECT_FILE_EXT = '*.iml'
_LAUNCH_PROJECT_QUERY = (
    'There exists an IntelliJ project file: %s. Do you want '
    'to launch it (yes/No)?')
_BUILD_BP_JSON_ENV_ON = {
    constant.GEN_JAVA_DEPS: 'true',
    constant.GEN_CC_DEPS: 'true',
    constant.GEN_COMPDB: 'true',
    constant.GEN_RUST: 'true'
}
_GEN_JSON_FAILED = (
    'Generate new {0} failed, AIDEGen will proceed and reuse the old {1}.')
_TARGET = 'nothing'
_LINKFILE_WARNING = (
    'File {} does not exist and we can not make a symbolic link for it.')
_RUST_PROJECT_JSON = 'out/soong/rust-project.json'


# Generators are slightly more inefficient when all the values have to be
# traversed most of the time.
# pylint: disable=use-a-generator
# pylint: disable=dangerous-default-value
@common_util.back_to_cwd
@common_util.time_logged
def generate_merged_module_info(env_on=_BUILD_BP_JSON_ENV_ON):
    """Generate a merged dictionary.

    Linked functions:
        _build_bp_info(module_info, project, skip_build)
        _get_soong_build_json_dict()
        _merge_dict(mk_dict, bp_dict)

    Args:
        env_on: A dictionary of environment settings to be turned on, the
                default value is _BUILD_BP_JSON_ENV_ON.

    Returns:
        A merged dictionary from module-info.json and module_bp_java_deps.json.
    """
    config = project_config.ProjectConfig.get_instance()
    module_info = config.atest_module_info
    projects = config.targets
    verbose = True
    skip_build = config.is_skip_build
    main_project = projects[0] if projects else None
    _build_bp_info(
        module_info, main_project, skip_build, env_on)
    json_path = common_util.get_blueprint_json_path(
        constant.BLUEPRINT_JAVA_JSONFILE_NAME)
    bp_dict = common_util.get_json_dict(json_path)
    return _merge_dict(module_info.name_to_module_info, bp_dict)


def _build_bp_info(module_info, main_project=None,
                   skip_build=False, env_on=_BUILD_BP_JSON_ENV_ON):
    """Make nothing to create module_bp_java_deps.json, module_bp_cc_deps.json.

    Use atest build method to build the target 'nothing' by setting env config
    SOONG_COLLECT_JAVA_DEPS to true to trigger the process of collecting
    dependencies and generate module_bp_java_deps.json etc.

    Args:
        module_info: A ModuleInfo instance contains data of module-info.json.
        main_project: A string of the main project name.
        skip_build: A boolean, if true, skip building if
                    get_blueprint_json_path(file_name) file exists, otherwise
                    build it.
        env_on: A dictionary of environment settings to be turned on, the
                default value is _BUILD_BP_JSON_ENV_ON.

    Build results:
        1. Build successfully return.
        2. Build failed:
           1) There's no project file, raise BuildFailureError.
           2) There exists a project file, ask users if they want to
              launch IDE with the old project file.
              a) If the answer is yes, return.
              b) If the answer is not yes, sys.exit(1)
    """
    file_paths = _get_generated_json_files(env_on)
    files_exist = all([os.path.isfile(fpath) for fpath in file_paths])
    files = '\n'.join(file_paths)
    if skip_build and files_exist:
        logging.info('Files:\n%s exist, skipping build.', files)
        return
    original_file_mtimes = {f: None for f in file_paths}
    if files_exist:
        original_file_mtimes = {f: os.path.getmtime(f) for f in file_paths}

    logging.warning(
        '\nGenerate files:\n %s by atest build method.', files)
    atest_utils.update_build_env(env_on)
    build_with_on_cmd = atest_utils.build([_TARGET])

    # For Android Rust projects, we need to create a symbolic link to the file
    # out/soong/rust-project.json to launch the rust projects in IDEs.
    _generate_rust_project_link()

    if build_with_on_cmd:
        logging.info('Generate blueprint json successfully.')
    else:
        if not all([_is_new_json_file_generated(
                f, original_file_mtimes[f]) for f in file_paths]):
            if files_exist:
                _show_files_reuse_message(file_paths)
            else:
                _show_build_failed_message(module_info, main_project)


def _get_generated_json_files(env_on=_BUILD_BP_JSON_ENV_ON):
    """Gets the absolute paths of the files which is going to be generated.

    Determine the files which will be generated by the environment on dictionary
    and the default blueprint json files' dictionary.
    The generation of json files depends on env_on. If the env_on looks like,
    _BUILD_BP_JSON_ENV_ON = {
        'SOONG_COLLECT_JAVA_DEPS': 'true',
        'SOONG_COLLECT_CC_DEPS': 'true',
        'SOONG_GEN_COMPDB': 'true',
        'SOONG_GEN_RUST_PROJECT': 'true'
    }
    We want to generate 4 files: module_bp_java_deps.json,
    module_bp_cc_deps.json, compile_commands.json and rust-project.json. And in
    get_blueprint_json_files_relative_dict function, there are 4 json files
    by default and return a result list of the absolute paths of the existent
    files.

    Args:
        env_on: A dictionary of environment settings to be turned on, the
                default value is _BUILD_BP_JSON_ENV_ON.

    Returns:
        A list of the absolute paths of the files which is going to be
        generated.
    """
    json_files_dict = common_util.get_blueprint_json_files_relative_dict()
    file_paths = []
    for key in env_on:
        if not env_on[key] == 'true' or key not in json_files_dict:
            continue
        file_paths.append(json_files_dict[key])
    return file_paths


def _show_files_reuse_message(file_paths):
    """Shows the message of build failure but files existing and reusing them.

    Args:
        file_paths: A list of absolute file paths to be checked.
    """
    failed_or_file = ' or '.join(file_paths)
    failed_and_file = ' and '.join(file_paths)
    message = _GEN_JSON_FAILED.format(failed_or_file, failed_and_file)
    print(constant.WARN_MSG.format(
        common_util.COLORED_INFO('Warning:'), message))


def _show_build_failed_message(module_info, main_project=None):
    """Show build failed message.

    Args:
        module_info: A ModuleInfo instance contains data of module-info.json.
        main_project: A string of the main project name.
    """
    if main_project:
        _, main_project_path = common_util.get_related_paths(
            module_info, main_project)
        _build_failed_handle(main_project_path)


def _is_new_json_file_generated(json_path, original_file_mtime):
    """Check the new file is generated or not.

    Args:
        json_path: The path of the json file being to check.
        original_file_mtime: the original file modified time.

    Returns:
        A boolean, True if the json_path file is new generated, otherwise False.
    """
    if not os.path.isfile(json_path):
        return False
    return original_file_mtime != os.path.getmtime(json_path)


def _build_failed_handle(main_project_path):
    """Handle build failures.

    Args:
        main_project_path: The main project directory.

    Handle results:
        1) There's no project file, raise BuildFailureError.
        2) There exists a project file, ask users if they want to
           launch IDE with the old project file.
           a) If the answer is yes, return.
           b) If the answer is not yes, sys.exit(1)
    """
    project_file = glob.glob(
        os.path.join(main_project_path, _INTELLIJ_PROJECT_FILE_EXT))
    if project_file:
        query = _LAUNCH_PROJECT_QUERY % project_file[0]
        input_data = input(query)
        if not input_data.lower() in ['yes', 'y']:
            sys.exit(1)
    else:
        raise errors.BuildFailureError(
            'Failed to generate %s.' % common_util.get_blueprint_json_path(
                constant.BLUEPRINT_JAVA_JSONFILE_NAME))


def _merge_module_keys(m_dict, b_dict):
    """Merge a module's dictionary into another module's dictionary.

    Merge b_dict module data into m_dict.

    Args:
        m_dict: The module dictionary is going to merge b_dict into.
        b_dict: Soong build system module dictionary.
    """
    for key, b_modules in b_dict.items():
        m_dict[key] = sorted(list(set(m_dict.get(key, []) + b_modules)))


def _copy_needed_items_from(mk_dict):
    """Shallow copy needed items from Make build system module info dictionary.

    Args:
        mk_dict: Make build system dictionary is going to be copied.

    Returns:
        A merged dictionary.
    """
    merged_dict = {}
    for module in mk_dict.keys():
        merged_dict[module] = {}
        for key in mk_dict[module].keys():
            if key in _MERGE_NEEDED_ITEMS and mk_dict[module][key] != []:
                merged_dict[module][key] = mk_dict[module][key]
    return merged_dict


def _merge_dict(mk_dict, bp_dict):
    """Merge two dictionaries.

    Linked function:
        _merge_module_keys(m_dict, b_dict)

    Args:
        mk_dict: Make build system module info dictionary.
        bp_dict: Soong build system module info dictionary.

    Returns:
        A merged dictionary.
    """
    merged_dict = _copy_needed_items_from(mk_dict)
    for module in bp_dict.keys():
        if module not in merged_dict:
            merged_dict[module] = {}
        _merge_module_keys(merged_dict[module], bp_dict[module])
    return merged_dict


def _generate_rust_project_link():
    """Generates out/soong/rust-project.json symbolic link in Android root."""
    root_dir = common_util.get_android_root_dir()
    rust_project = os.path.join(
        root_dir, common_util.get_blueprint_json_path(
            constant.RUST_PROJECT_JSON))
    if not os.path.isfile(rust_project):
        message = _LINKFILE_WARNING.format(_RUST_PROJECT_JSON)
        print(constant.WARN_MSG.format(
            common_util.COLORED_INFO('Warning:'), message))
        return
    link_rust = os.path.join(root_dir, constant.RUST_PROJECT_JSON)
    if os.path.islink(link_rust):
        os.remove(link_rust)
    os.symlink(rust_project, link_rust)
