#!/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.

"""It is an AIDEGen sub task : IDE operation task!

Takes a project file path as input, after passing the needed check(file
existence, IDE type, etc.), launch the project in related IDE.

    Typical usage example:

    ide_util_obj = IdeUtil()
    if ide_util_obj.is_ide_installed():
        ide_util_obj.config_ide(project_file)
        ide_util_obj.launch_ide()

        # Get the configuration folders of IntelliJ or Android Studio.
        ide_util_obj.get_ide_config_folders()
"""

import glob
import logging
import os
import platform
import re
import subprocess

from xml.etree import ElementTree

from aidegen import constant
from aidegen import templates
from aidegen.lib import aidegen_metrics
from aidegen.lib import android_dev_os
from aidegen.lib import common_util
from aidegen.lib import config
from aidegen.lib import errors
from aidegen.lib import ide_common_util
from aidegen.lib import project_config
from aidegen.lib import project_file_gen
from aidegen.sdk import jdk_table
from aidegen.lib import xml_util

# Add 'nohup' to prevent IDE from being terminated when console is terminated.
_IDEA_FOLDER = '.idea'
_IML_EXTENSION = '.iml'
_JDK_PATH_TOKEN = '@JDKpath'
_COMPONENT_END_TAG = '  </component>'
_ECLIPSE_WS = '~/Documents/AIDEGen_Eclipse_workspace'
_ALERT_CREATE_WS = ('AIDEGen will create a workspace at %s for Eclipse, '
                    'Enter `y` to allow AIDEgen to automatically create the '
                    'workspace for you. Otherwise, you need to select the '
                    'workspace after Eclipse is launched.\nWould you like '
                    'AIDEgen to automatically create the workspace for you?'
                    '(y/n)' % constant.ECLIPSE_WS)
_NO_LAUNCH_IDE_CMD = """
Can not find IDE: {}, in path: {}, you can:
    - add IDE executable to your $PATH
or  - specify the exact IDE executable path by "aidegen -p"
or  - specify "aidegen -n" to generate project file only
"""
_INFO_IMPORT_CONFIG = ('{} needs to import the application configuration for '
                       'the new version!\nAfter the import is finished, rerun '
                       'the command if your project did not launch. Please '
                       'follow the showing dialog to finish the import action.'
                       '\n\n')
CONFIG_DIR = 'config'
LINUX_JDK_PATH = os.path.join(common_util.get_android_root_dir(),
                              'prebuilts/jdk/jdk8/linux-x86')
LINUX_JDK_TABLE_PATH = 'config/options/jdk.table.xml'
LINUX_FILE_TYPE_PATH = 'config/options/filetypes.xml'
LINUX_ANDROID_SDK_PATH = os.path.join(os.getenv('HOME'), 'Android/Sdk')
MAC_JDK_PATH = os.path.join(common_util.get_android_root_dir(),
                            'prebuilts/jdk/jdk8/darwin-x86')
ALTERNAIVE_JDK_TABLE_PATH = 'options/jdk.table.xml'
ALTERNAIVE_FILE_TYPE_XML_PATH = 'options/filetypes.xml'
MAC_ANDROID_SDK_PATH = os.path.join(os.getenv('HOME'), 'Library/Android/sdk')
PATTERN_KEY = 'pattern'
TYPE_KEY = 'type'
_TEST_MAPPING_FILE_TYPE = 'JSON'
TEST_MAPPING_NAME = 'TEST_MAPPING'
_TEST_MAPPING_TYPE = '<mapping pattern="TEST_MAPPING" type="JSON" />'
_XPATH_EXTENSION_MAP = 'component/extensionMap'
_XPATH_MAPPING = _XPATH_EXTENSION_MAP + '/mapping'
_SPECIFIC_INTELLIJ_VERSION = 2020.1
_TEST_MAPPING_FILE_TYPE_ADDING_WARN = '\n{} {}\n'.format(
    common_util.COLORED_INFO('WARNING:'),
    ('TEST_MAPPING file type can\'t be added to filetypes.xml. The reason '
     'might be: lack of the parent tag to add TEST_MAPPING file type.'))


# pylint: disable=too-many-lines
# pylint: disable=invalid-name
class IdeUtil:
    """Provide a set of IDE operations, e.g., launch and configuration.

    Attributes:
        _ide: IdeBase derived instance, the related IDE object.

    For example:
        1. Check if IDE is installed.
        2. Config IDE, e.g. config code style, SDK path, and etc.
        3. Launch an IDE.
    """

    def __init__(self,
                 installed_path=None,
                 ide='j',
                 config_reset=False,
                 is_mac=False):
        logging.debug('IdeUtil with OS name: %s%s', platform.system(),
                      '(Mac)' if is_mac else '')
        self._ide = _get_ide(installed_path, ide, config_reset, is_mac)

    def is_ide_installed(self):
        """Checks if the IDE is already installed.

        Returns:
            True if IDE is installed already, otherwise False.
        """
        return self._ide.is_ide_installed()

    def launch_ide(self):
        """Launches the relative IDE by opening the passed project file."""
        return self._ide.launch_ide()

    def config_ide(self, project_abspath):
        """To config the IDE, e.g., setup code style, init SDK, and etc.

        Args:
            project_abspath: An absolute path of the project.
        """
        self._ide.project_abspath = project_abspath
        if self.is_ide_installed() and self._ide:
            self._ide.apply_optional_config()

    def get_default_path(self):
        """Gets IDE default installed path."""
        return self._ide.default_installed_path

    def ide_name(self):
        """Gets IDE name."""
        return self._ide.ide_name

    def get_ide_config_folders(self):
        """Gets the config folders of IDE."""
        return self._ide.config_folders


class IdeBase:
    """The most base class of IDE, provides interface and partial path init.

    Class Attributes:
        _JDK_PATH: The path of JDK in android project.
        _IDE_JDK_TABLE_PATH: The path of JDK table which record JDK info in IDE.
        _IDE_FILE_TYPE_PATH: The path of filetypes.xml.
        _JDK_CONTENT: A string, the content of the JDK configuration.
        _DEFAULT_ANDROID_SDK_PATH: A string, the path of Android SDK.
        _CONFIG_DIR: A string of the config folder name.
        _SYMBOLIC_VERSIONS: A string list of the symbolic link paths of the
                            relevant IDE.

    Attributes:
        _installed_path: String for the IDE binary path.
        _config_reset: Boolean, True for reset configuration, else not reset.
        _bin_file_name: String for IDE executable file name.
        _bin_paths: A list of all possible IDE executable file absolute paths.
        _ide_name: String for IDE name.
        _bin_folders: A list of all possible IDE installed paths.
        config_folders: A list of all possible paths for the IntelliJ
                        configuration folder.
        project_abspath: The absolute path of the project.

    For example:
        1. Check if IDE is installed.
        2. Launch IDE.
        3. Config IDE.
    """

    _JDK_PATH = ''
    _IDE_JDK_TABLE_PATH = ''
    _IDE_FILE_TYPE_PATH = ''
    _JDK_CONTENT = ''
    _DEFAULT_ANDROID_SDK_PATH = ''
    _CONFIG_DIR = ''
    _SYMBOLIC_VERSIONS = []

    def __init__(self, installed_path=None, config_reset=False):
        self._installed_path = installed_path
        self._config_reset = config_reset
        self._ide_name = ''
        self._bin_file_name = ''
        self._bin_paths = []
        self._bin_folders = []
        self.config_folders = []
        self.project_abspath = ''

    def is_ide_installed(self):
        """Checks if IDE is already installed.

        Returns:
            True if IDE is installed already, otherwise False.
        """
        return bool(self._installed_path)

    def launch_ide(self):
        """Launches IDE by opening the passed project file."""
        ide_common_util.launch_ide(self.project_abspath, self._get_ide_cmd(),
                                   self._ide_name)

    def apply_optional_config(self):
        """Do IDEA global config action.

        Run code style config, SDK config.
        """
        if not self._installed_path:
            return
        # Skip config action if there's no config folder exists.
        _path_list = self._get_config_root_paths()
        if not _path_list:
            return
        self.config_folders = _path_list.copy()

        for _config_path in _path_list:
            jdk_file = os.path.join(_config_path, self._IDE_JDK_TABLE_PATH)
            jdk_xml = jdk_table.JDKTableXML(jdk_file, self._JDK_CONTENT,
                                            self._JDK_PATH,
                                            self._DEFAULT_ANDROID_SDK_PATH)
            if jdk_xml.config_jdk_table_xml():
                project_file_gen.gen_enable_debugger_module(
                    self.project_abspath, jdk_xml.android_sdk_version)

            # Set the max file size in the idea.properties.
            intellij_config_dir = os.path.join(_config_path, self._CONFIG_DIR)
            config.IdeaProperties(intellij_config_dir).set_max_file_size()

            self._add_test_mapping_file_type(_config_path)

    def _add_test_mapping_file_type(self, _config_path):
        """Adds TEST_MAPPING file type.

        IntelliJ can't recognize TEST_MAPPING files as the json file. It needs
        adding file type mapping in filetypes.xml to recognize TEST_MAPPING
        files.

        Args:
            _config_path: the path of IDE config.
        """
        file_type_path = os.path.join(_config_path, self._IDE_FILE_TYPE_PATH)
        if not os.path.isfile(file_type_path):
            logging.warning('The file: filetypes.xml is not found.')
            return

        file_type_xml = xml_util.parse_xml(file_type_path)
        if not file_type_xml:
            logging.warning('Can\'t parse filetypes.xml.')
            return

        root = file_type_xml.getroot()
        add_pattern = True
        for mapping in root.findall(_XPATH_MAPPING):
            attrib = mapping.attrib
            if PATTERN_KEY in attrib and TYPE_KEY in attrib:
                if attrib[PATTERN_KEY] == TEST_MAPPING_NAME:
                    if attrib[TYPE_KEY] != _TEST_MAPPING_FILE_TYPE:
                        attrib[TYPE_KEY] = _TEST_MAPPING_FILE_TYPE
                        file_type_xml.write(file_type_path)
                    add_pattern = False
                    break
        if add_pattern:
            ext_attrib = root.find(_XPATH_EXTENSION_MAP)
            if not ext_attrib:
                print(_TEST_MAPPING_FILE_TYPE_ADDING_WARN)
                return
            ext_attrib.append(ElementTree.fromstring(_TEST_MAPPING_TYPE))
            pretty_xml = common_util.to_pretty_xml(root)
            common_util.file_generate(file_type_path, pretty_xml)

    def _get_config_root_paths(self):
        """Get the config root paths from derived class.

        Returns:
            A string list of IDE config paths, return multiple paths if more
            than one path are found, return an empty list when none is found.
        """
        raise NotImplementedError()

    @property
    def default_installed_path(self):
        """Gets IDE default installed path."""
        return ' '.join(self._bin_folders)

    @property
    def ide_name(self):
        """Gets IDE name."""
        return self._ide_name

    def _get_ide_cmd(self):
        """Compose launch IDE command to run a new process and redirect output.

        Returns:
            A string of launch IDE command.
        """
        return ide_common_util.get_run_ide_cmd(self._installed_path,
                                               self.project_abspath)

    def _init_installed_path(self, installed_path):
        """Initialize IDE installed path.

        Args:
            installed_path: the installed path to be checked.
        """
        if installed_path:
            path_list = ide_common_util.get_script_from_input_path(
                installed_path, self._bin_file_name)
            self._installed_path = path_list[0] if path_list else None
        else:
            self._installed_path = self._get_script_from_system()
        if not self._installed_path:
            logging.error('No %s installed.', self._ide_name)
            return

        self._set_installed_path()

    def _get_script_from_system(self):
        """Get one IDE installed path from internal path.

        Returns:
            The sh full path, or None if not found.
        """
        sh_list = self._get_existent_scripts_in_system()
        return sh_list[0] if sh_list else None

    def _get_possible_bin_paths(self):
        """Gets all possible IDE installed paths."""
        return [os.path.join(f, self._bin_file_name) for f in self._bin_folders]

    def _get_ide_from_environment_paths(self):
        """Get IDE executable binary file from environment paths.

        Returns:
            A string of IDE executable binary path if found, otherwise return
            None.
        """
        env_paths = os.environ['PATH'].split(':')
        for env_path in env_paths:
            path = ide_common_util.get_scripts_from_dir_path(
                env_path, self._bin_file_name)
            if path:
                return path
        return None

    def _setup_ide(self):
        """The callback used to run the necessary setup work of the IDE.

        When ide_util.config_ide is called to set up the JDK, SDK and some
        features, the main thread will callback the Idexxx._setup_ide
        to provide the chance for running the necessary setup of the specific
        IDE. Default is to do nothing.
        """

    def _get_existent_scripts_in_system(self):
        """Gets the relevant IDE run script path from system.

        First get correct IDE installed path from internal paths, if not found
        search it from environment paths.

        Returns:
            The list of script full path, or None if no found.
        """
        return (ide_common_util.get_script_from_internal_path(self._bin_paths,
                                                              self._ide_name) or
                self._get_ide_from_environment_paths())

    def _get_user_preference(self, versions):
        """Make sure the version is valid and update preference if needed.

        Args:
            versions: A list of the IDE script path, contains the symbolic path.

        Returns: An IDE script path, or None is not found.
        """
        if not versions:
            return None
        if len(versions) == 1:
            return versions[0]
        with config.AidegenConfig() as conf:
            if not self._config_reset and (conf.preferred_version(self.ide_name)
                                           in versions):
                return conf.preferred_version(self.ide_name)
            display_versions = self._merge_symbolic_version(versions)
            preferred = ide_common_util.ask_preference(display_versions,
                                                       self.ide_name)
            if preferred:
                conf.set_preferred_version(self._get_real_path(preferred),
                                           self.ide_name)

            return conf.preferred_version(self.ide_name)

    def _set_installed_path(self):
        """Write the user's input installed path into the config file.

        If users input an existent IDE installed path, we should keep it in
        the configuration.
        """
        if self._installed_path:
            with config.AidegenConfig() as aconf:
                aconf.set_preferred_version(self._installed_path, self.ide_name)

    def _merge_symbolic_version(self, versions):
        """Merge the duplicate version of symbolic links.

        Stable and beta versions are a symbolic link to an existing version.
        This function assemble symbolic and real to make it more clear to read.
        Ex:
        ['/opt/intellij-ce-stable/bin/idea.sh',
        '/opt/intellij-ce-2019.1/bin/idea.sh'] to
        ['/opt/intellij-ce-stable/bin/idea.sh ->
        /opt/intellij-ce-2019.1/bin/idea.sh',
        '/opt/intellij-ce-2019.1/bin/idea.sh']

        Args:
            versions: A list of all installed versions.

        Returns:
            A list of versions to show for user to select. It may contain
            'symbolic_path/idea.sh -> original_path/idea.sh'.
        """
        display_versions = versions[:]
        for symbolic in self._SYMBOLIC_VERSIONS:
            if symbolic in display_versions and (os.path.isfile(symbolic)):
                real_path = os.path.realpath(symbolic)
                for index, path in enumerate(display_versions):
                    if path == symbolic:
                        display_versions[index] = ' -> '.join(
                            [display_versions[index], real_path])
                        break
        return display_versions

    @staticmethod
    def _get_real_path(path):
        """ Get real path from merged path.

        Turn the path string "/opt/intellij-ce-stable/bin/idea.sh -> /opt/
        intellij-ce-2019.2/bin/idea.sh" into
        "/opt/intellij-ce-stable/bin/idea.sh"

        Args:
            path: A path string may be merged with symbolic path.
        Returns:
            The real IntelliJ installed path.
        """
        return path.split()[0]


class IdeIntelliJ(IdeBase):
    """Provide basic IntelliJ ops, e.g., launch IDEA, and config IntelliJ.

    For example:
        1. Check if IntelliJ is installed.
        2. Launch an IntelliJ.
        3. Config IntelliJ.
    """
    def __init__(self, installed_path=None, config_reset=False):
        super().__init__(installed_path, config_reset)
        self._ide_name = constant.IDE_INTELLIJ
        self._ls_ce_path = ''
        self._ls_ue_path = ''

    def _get_config_root_paths(self):
        """Get the config root paths from derived class.

        Returns:
            A string list of IDE config paths, return multiple paths if more
            than one path are found, return an empty list when none is found.
        """
        raise NotImplementedError()

    def _get_preferred_version(self):
        """Get the user's preferred IntelliJ version.

        Locates the IntelliJ IDEA launch script path by following rule.

        1. If config file recorded user's preference version, load it.
        2. If config file didn't record, search them form default path if there
           are more than one version, ask user and record it.

        Returns:
            The sh full path, or None if no IntelliJ version is installed.
        """
        ce_paths = ide_common_util.get_intellij_version_path(self._ls_ce_path)
        ue_paths = ide_common_util.get_intellij_version_path(self._ls_ue_path)
        all_versions = self._get_all_versions(ce_paths, ue_paths)
        tmp_versions = all_versions.copy()
        for version in tmp_versions:
            real_version = os.path.realpath(version)
            if config.AidegenConfig.deprecated_intellij_version(real_version):
                all_versions.remove(version)
        return self._get_user_preference(all_versions)

    def _setup_ide(self):
        """The callback used to run the necessary setup work for the IDE.

        IntelliJ has a default flow to let the user import the configuration
        from the previous version, aidegen makes sure not to break the behavior
        by checking in this callback implementation.
        """
        run_script_path = os.path.realpath(self._installed_path)
        app_folder = self._get_application_path(run_script_path)
        if not app_folder:
            logging.warning('\nInvalid IDE installed path.')
            return

        show_hint = False
        ide_version = self._get_ide_version(app_folder)
        folder_path = self._get_config_dir(ide_version, app_folder)
        import_process = None
        while not os.path.isdir(folder_path):
            # Guide the user to go through the IDE flow.
            if not show_hint:
                print('\n{} {}'.format(common_util.COLORED_INFO('INFO:'),
                                       _INFO_IMPORT_CONFIG.format(
                                           self.ide_name)))
                try:
                    import_process = subprocess.Popen(
                        ide_common_util.get_run_ide_cmd(run_script_path, '',
                                                        False), shell=True)
                except (subprocess.SubprocessError, ValueError):
                    logging.warning('\nSubprocess call gets the invalid input.')
                finally:
                    show_hint = True
        if import_process:
            try:
                import_process.wait(1)
            except subprocess.TimeoutExpired:
                import_process.terminate()
        return

    def _get_script_from_system(self):
        """Get correct IntelliJ installed path from internal path.

        Returns:
            The sh full path, or None if no IntelliJ version is installed.
        """
        found = self._get_preferred_version()
        if found:
            logging.debug('IDE internal installed path: %s.', found)
        return found

    @staticmethod
    def _get_all_versions(cefiles, uefiles):
        """Get all versions of launch script files.

        Args:
            cefiles: CE version launch script paths.
            uefiles: UE version launch script paths.

        Returns:
            A list contains all versions of launch script files.
        """
        all_versions = []
        if cefiles:
            all_versions.extend(cefiles)
        if uefiles:
            all_versions.extend(uefiles)
        return all_versions

    @staticmethod
    def _get_application_path(run_script_path):
        """Get the relevant configuration folder based on the run script path.

        Args:
            run_script_path: The string of the run script path for the IntelliJ.

        Returns:
            The string of the IntelliJ application folder name or None if the
            run_script_path is invalid. The returned folder format is as
            follows,
                1. .IdeaIC2019.3
                2. .IntelliJIdea2019.3
                3. IntelliJIdea2020.1
        """
        if not run_script_path or not os.path.isfile(run_script_path):
            return None
        index = str.find(run_script_path, 'intellij-')
        target_path = None if index == -1 else run_script_path[index:]
        if not target_path or '-' not in run_script_path:
            return None
        return IdeIntelliJ._get_config_folder_name(target_path)

    @staticmethod
    def _get_ide_version(config_folder_name):
        """Gets IntelliJ version from the input app folder name.

        Args:
            config_folder_name: A string of the app folder name.

        Returns:
            A string of the IntelliJ version.
        """
        versions = re.findall(r'\d+', config_folder_name)
        if not versions:
            logging.warning('\nInvalid IntelliJ config folder name: %s.',
                            config_folder_name)
            return None
        return '.'.join(versions)

    @staticmethod
    def _get_config_folder_name(script_folder_name):
        """Gets IntelliJ config folder name from the IDE version.

        The config folder name has been changed since 2020.1.

        Args:
            script_folder_name: A string of the script folder name of IntelliJ.

        Returns:
            A string of the IntelliJ config folder name.
        """
        path_data = script_folder_name.split('-')
        if not path_data or len(path_data) < 3:
            return None
        ide_version = path_data[2].split(os.sep)[0]
        numbers = ide_version.split('.')
        if len(numbers) > 2:
            ide_version = '.'.join([numbers[0], numbers[1]])
        try:
            version = float(ide_version)
        except ValueError:
            return None
        pre_folder = '.IdeaIC'
        if version < _SPECIFIC_INTELLIJ_VERSION:
            if path_data[1] == 'ue':
                pre_folder = '.IntelliJIdea'
        else:
            if path_data[1] == 'ce':
                pre_folder = 'IdeaIC'
            elif path_data[1] == 'ue':
                pre_folder = 'IntelliJIdea'
        return ''.join([pre_folder, ide_version])

    @staticmethod
    def _get_config_dir(ide_version, config_folder_name):
        """Gets IntelliJ config directory by the config folder name.

        The IntelliJ config directory is changed from version 2020.1. Get the
        version from app folder name and determine the config directory.
        URL: https://intellij-support.jetbrains.com/hc/en-us/articles/206544519

        Args:
            ide_version: A string of the IntelliJ's version.
            config_folder_name: A string of the IntelliJ's config folder name.

        Returns:
            A string of the IntelliJ config directory.
        """
        try:
            version = float(ide_version)
        except ValueError:
            return None
        if version < _SPECIFIC_INTELLIJ_VERSION:
            return os.path.join(
                os.getenv('HOME'), config_folder_name)
        return os.path.join(
            os.getenv('HOME'), '.config', 'JetBrains', config_folder_name)


class IdeLinuxIntelliJ(IdeIntelliJ):
    """Provide the IDEA behavior implementation for OS Linux.

    Class Attributes:
        _INTELLIJ_RE: Regular expression of IntelliJ installed name in GLinux.

    For example:
        1. Check if IntelliJ is installed.
        2. Launch an IntelliJ.
        3. Config IntelliJ.
    """

    _JDK_PATH = LINUX_JDK_PATH
    # TODO(b/127899277): Preserve a config for jdk version option case.
    _CONFIG_DIR = CONFIG_DIR
    _IDE_JDK_TABLE_PATH = LINUX_JDK_TABLE_PATH
    _IDE_FILE_TYPE_PATH = LINUX_FILE_TYPE_PATH
    _JDK_CONTENT = templates.LINUX_JDK_XML
    _DEFAULT_ANDROID_SDK_PATH = LINUX_ANDROID_SDK_PATH
    _SYMBOLIC_VERSIONS = ['/opt/intellij-ce-stable/bin/idea.sh',
                          '/opt/intellij-ue-stable/bin/idea.sh',
                          '/opt/intellij-ce-beta/bin/idea.sh',
                          '/opt/intellij-ue-beta/bin/idea.sh']
    _INTELLIJ_RE = re.compile(r'intellij-(ce|ue)-')

    def __init__(self, installed_path=None, config_reset=False):
        super().__init__(installed_path, config_reset)
        self._bin_file_name = 'idea.sh'
        self._bin_folders = ['/opt/intellij-*/bin']
        self._ls_ce_path = os.path.join('/opt/intellij-ce-*/bin',
                                        self._bin_file_name)
        self._ls_ue_path = os.path.join('/opt/intellij-ue-*/bin',
                                        self._bin_file_name)
        self._init_installed_path(installed_path)

    def _get_config_root_paths(self):
        """To collect the global config folder paths of IDEA as a string list.

        The config folder of IntelliJ IDEA is under the user's home directory,
        .IdeaIC20xx.x and .IntelliJIdea20xx.x are folder names for different
        versions.

        Returns:
            A string list for IDE config root paths, and return an empty list
            when none is found.
        """
        if not self._installed_path:
            return None

        _config_folders = []
        _config_folder = ''
        if IdeLinuxIntelliJ._INTELLIJ_RE.search(self._installed_path):
            _path_data = os.path.realpath(self._installed_path)
            _config_folder = self._get_application_path(_path_data)
            if not _config_folder:
                return None
            ide_version = self._get_ide_version(_config_folder)
            if not ide_version:
                return None
            try:
                version = float(ide_version)
            except ValueError:
                return None
            folder_path = self._get_config_dir(ide_version, _config_folder)
            if version >= _SPECIFIC_INTELLIJ_VERSION:
                self._IDE_JDK_TABLE_PATH = ALTERNAIVE_JDK_TABLE_PATH
                self._IDE_FILE_TYPE_PATH = ALTERNAIVE_FILE_TYPE_XML_PATH

            if not os.path.isdir(folder_path):
                logging.debug("\nThe config folder: %s doesn't exist",
                              _config_folder)
                self._setup_ide()

            _config_folders.append(folder_path)
        else:
            # TODO(b/123459239): For the case that the user provides the IDEA
            # binary path, we now collect all possible IDEA config root paths.
            _config_folders = glob.glob(
                os.path.join(os.getenv('HOME'), '.IdeaI?20*'))
            _config_folders.extend(
                glob.glob(os.path.join(os.getenv('HOME'), '.IntelliJIdea20*')))
            _config_folders.extend(
                glob.glob(os.path.join(os.getenv('HOME'), '.config',
                                       'IntelliJIdea202*')))
            logging.debug('The config path list: %s.', _config_folders)

        return _config_folders


class IdeMacIntelliJ(IdeIntelliJ):
    """Provide the IDEA behavior implementation for OS Mac.

    For example:
        1. Check if IntelliJ is installed.
        2. Launch an IntelliJ.
        3. Config IntelliJ.
    """

    _JDK_PATH = MAC_JDK_PATH
    _IDE_JDK_TABLE_PATH = ALTERNAIVE_JDK_TABLE_PATH
    _IDE_FILE_TYPE_PATH = ALTERNAIVE_FILE_TYPE_XML_PATH
    _JDK_CONTENT = templates.MAC_JDK_XML
    _DEFAULT_ANDROID_SDK_PATH = MAC_ANDROID_SDK_PATH

    def __init__(self, installed_path=None, config_reset=False):
        super().__init__(installed_path, config_reset)
        self._bin_file_name = 'idea'
        self._bin_folders = ['/Applications/IntelliJ IDEA.app/Contents/MacOS']
        self._bin_paths = self._get_possible_bin_paths()
        self._ls_ce_path = os.path.join(
            '/Applications/IntelliJ IDEA CE.app/Contents/MacOS',
            self._bin_file_name)
        self._ls_ue_path = os.path.join(
            '/Applications/IntelliJ IDEA.app/Contents/MacOS',
            self._bin_file_name)
        self._init_installed_path(installed_path)

    def _get_config_root_paths(self):
        """To collect the global config folder paths of IDEA as a string list.

        Returns:
            A string list for IDE config root paths, and return an empty list
            when none is found.
        """
        if not self._installed_path:
            return None

        _config_folders = []
        if 'IntelliJ' in self._installed_path:
            _config_folders = glob.glob(
                os.path.join(
                    os.getenv('HOME'), 'Library/Preferences/IdeaI?20*'))
            _config_folders.extend(
                glob.glob(
                    os.path.join(
                        os.getenv('HOME'),
                        'Library/Preferences/IntelliJIdea20*')))
        return _config_folders


class IdeStudio(IdeBase):
    """Class offers a set of Android Studio launching utilities.

    For example:
        1. Check if Android Studio is installed.
        2. Launch an Android Studio.
        3. Config Android Studio.
    """

    def __init__(self, installed_path=None, config_reset=False):
        super().__init__(installed_path, config_reset)
        self._ide_name = constant.IDE_ANDROID_STUDIO

    def _get_config_root_paths(self):
        """Get the config root paths from derived class.

        Returns:
            A string list of IDE config paths, return multiple paths if more
            than one path are found, return an empty list when none is found.
        """
        raise NotImplementedError()

    def _get_script_from_system(self):
        """Get correct Studio installed path from internal path.

        Returns:
            The sh full path, or None if no Studio version is installed.
        """
        found = self._get_preferred_version()
        if found:
            logging.debug('IDE internal installed path: %s.', found)
        return found

    def _get_preferred_version(self):
        """Get the user's preferred Studio version.

        Locates the Studio launch script path by following rule.

        1. If config file recorded user's preference version, load it.
        2. If config file didn't record, search them form default path if there
           are more than one version, ask user and record it.

        Returns:
            The sh full path, or None if no Studio version is installed.
        """
        versions = self._get_existent_scripts_in_system()
        if not versions:
            return None
        for version in versions:
            real_version = os.path.realpath(version)
            if config.AidegenConfig.deprecated_studio_version(real_version):
                versions.remove(version)
        return self._get_user_preference(versions)

    def apply_optional_config(self):
        """Do the configuration of Android Studio.

        Configures code style and SDK for Java project and do nothing for
        others.
        """
        if not self.project_abspath:
            return
        # TODO(b/150662865): The following workaround should be replaced.
        # Since the path of the artifact for Java is the .idea directory but
        # native is a CMakeLists.txt file using this to workaround first.
        if os.path.isfile(self.project_abspath):
            return
        if os.path.isdir(self.project_abspath):
            IdeBase.apply_optional_config(self)


class IdeLinuxStudio(IdeStudio):
    """Class offers a set of Android Studio launching utilities for OS Linux.

    For example:
        1. Check if Android Studio is installed.
        2. Launch an Android Studio.
        3. Config Android Studio.
    """

    _JDK_PATH = LINUX_JDK_PATH
    _CONFIG_DIR = CONFIG_DIR
    _IDE_JDK_TABLE_PATH = LINUX_JDK_TABLE_PATH
    _JDK_CONTENT = templates.LINUX_JDK_XML
    _DEFAULT_ANDROID_SDK_PATH = LINUX_ANDROID_SDK_PATH
    _SYMBOLIC_VERSIONS = [
        '/opt/android-studio-with-blaze-stable/bin/studio.sh',
        '/opt/android-studio-stable/bin/studio.sh',
        '/opt/android-studio-with-blaze-beta/bin/studio.sh',
        '/opt/android-studio-beta/bin/studio.sh']

    def __init__(self, installed_path=None, config_reset=False):
        super().__init__(installed_path, config_reset)
        self._bin_file_name = 'studio.sh'
        self._bin_folders = ['/opt/android-studio*/bin']
        self._bin_paths = self._get_possible_bin_paths()
        self._init_installed_path(installed_path)

    def _get_config_root_paths(self):
        """Collect the global config folder paths as a string list.

        Returns:
            A string list for IDE config root paths, and return an empty list
            when none is found.
        """
        return glob.glob(os.path.join(os.getenv('HOME'), '.AndroidStudio*'))


class IdeMacStudio(IdeStudio):
    """Class offers a set of Android Studio launching utilities for OS Mac.

    For example:
        1. Check if Android Studio is installed.
        2. Launch an Android Studio.
        3. Config Android Studio.
    """

    _JDK_PATH = MAC_JDK_PATH
    _IDE_JDK_TABLE_PATH = ALTERNAIVE_JDK_TABLE_PATH
    _JDK_CONTENT = templates.MAC_JDK_XML
    _DEFAULT_ANDROID_SDK_PATH = MAC_ANDROID_SDK_PATH

    def __init__(self, installed_path=None, config_reset=False):
        super().__init__(installed_path, config_reset)
        self._bin_file_name = 'studio'
        self._bin_folders = ['/Applications/Android Studio.app/Contents/MacOS']
        self._bin_paths = self._get_possible_bin_paths()
        self._init_installed_path(installed_path)

    def _get_config_root_paths(self):
        """Collect the global config folder paths as a string list.

        Returns:
            A string list for IDE config root paths, and return an empty list
            when none is found.
        """
        return glob.glob(
            os.path.join(
                os.getenv('HOME'), 'Library/Preferences/AndroidStudio*'))


class IdeEclipse(IdeBase):
    """Class offers a set of Eclipse launching utilities.

    Attributes:
        cmd: A list of the build command.

    For example:
        1. Check if Eclipse is installed.
        2. Launch an Eclipse.
    """

    def __init__(self, installed_path=None, config_reset=False):
        super().__init__(installed_path, config_reset)
        self._ide_name = constant.IDE_ECLIPSE
        self._bin_file_name = 'eclipse'
        self.cmd = []

    def _get_script_from_system(self):
        """Get correct IDE installed path from internal path.

        Remove any file with extension, the filename should be like, 'eclipse',
        'eclipse47' and so on, check if the file is executable and filter out
        file such as 'eclipse.ini'.

        Returns:
            The sh full path, or None if no IntelliJ version is installed.
        """
        for ide_path in self._bin_paths:
            # The binary name of Eclipse could be eclipse47, eclipse49,
            # eclipse47_testing or eclipse49_testing. So finding the matched
            # binary by /path/to/ide/eclipse*.
            ls_output = glob.glob(ide_path + '*', recursive=True)
            if ls_output:
                ls_output = sorted(ls_output)
                match_eclipses = []
                for path in ls_output:
                    if os.access(path, os.X_OK):
                        match_eclipses.append(path)
                if match_eclipses:
                    match_eclipses = sorted(match_eclipses)
                    logging.debug('Result for checking %s after sort: %s.',
                                  self._ide_name, match_eclipses[0])
                    return match_eclipses[0]
        return None

    def _get_ide_cmd(self):
        """Compose launch IDE command to run a new process and redirect output.

        AIDEGen will create a default workspace
        ~/Documents/AIDEGen_Eclipse_workspace for users if they agree to do
        that. Also, we could not import the default project through the command
        line so remove the project path argument.

        Returns:
            A string of launch IDE command.
        """
        if (os.path.exists(os.path.expanduser(constant.ECLIPSE_WS))
                or str(input(_ALERT_CREATE_WS)).lower() == 'y'):
            self.cmd.extend(['-data', constant.ECLIPSE_WS])
        self.cmd.extend([constant.IGNORE_STD_OUT_ERR_CMD, '&'])
        return ' '.join(self.cmd)

    def apply_optional_config(self):
        """Override to do nothing."""

    def _get_config_root_paths(self):
        """Override to do nothing."""


class IdeLinuxEclipse(IdeEclipse):
    """Class offers a set of Eclipse launching utilities for OS Linux.

    For example:
        1. Check if Eclipse is installed.
        2. Launch an Eclipse.
    """

    def __init__(self, installed_path=None, config_reset=False):
        super().__init__(installed_path, config_reset)
        self._bin_folders = ['/opt/eclipse*', '/usr/bin/']
        self._bin_paths = self._get_possible_bin_paths()
        self._init_installed_path(installed_path)
        self.cmd = [constant.NOHUP, self._installed_path.replace(' ', r'\ ')]


class IdeMacEclipse(IdeEclipse):
    """Class offers a set of Eclipse launching utilities for OS Mac.

    For example:
        1. Check if Eclipse is installed.
        2. Launch an Eclipse.
    """

    def __init__(self, installed_path=None, config_reset=False):
        super().__init__(installed_path, config_reset)
        self._bin_file_name = 'eclipse'
        self._bin_folders = [os.path.expanduser('~/eclipse/**')]
        self._bin_paths = self._get_possible_bin_paths()
        self._init_installed_path(installed_path)
        self.cmd = [self._installed_path.replace(' ', r'\ ')]


class IdeCLion(IdeBase):
    """Class offers a set of CLion launching utilities.

    For example:
        1. Check if CLion is installed.
        2. Launch an CLion.
    """

    def __init__(self, installed_path=None, config_reset=False):
        super().__init__(installed_path, config_reset)
        self._ide_name = constant.IDE_CLION

    def apply_optional_config(self):
        """Override to do nothing."""

    def _get_config_root_paths(self):
        """Override to do nothing."""


class IdeLinuxCLion(IdeCLion):
    """Class offers a set of CLion launching utilities for OS Linux.

    For example:
        1. Check if CLion is installed.
        2. Launch an CLion.
    """

    def __init__(self, installed_path=None, config_reset=False):
        super().__init__(installed_path, config_reset)
        self._bin_file_name = 'clion.sh'
        # TODO(b/141288011): Handle /opt/clion-*/bin to let users choose a
        # preferred version of CLion in the future.
        self._bin_folders = ['/opt/clion-stable/bin']
        self._bin_paths = self._get_possible_bin_paths()
        self._init_installed_path(installed_path)


class IdeMacCLion(IdeCLion):
    """Class offers a set of Android Studio launching utilities for OS Mac.

    For example:
        1. Check if Android Studio is installed.
        2. Launch an Android Studio.
    """

    def __init__(self, installed_path=None, config_reset=False):
        super().__init__(installed_path, config_reset)
        self._bin_file_name = 'clion'
        self._bin_folders = ['/Applications/CLion.app/Contents/MacOS/CLion']
        self._bin_paths = self._get_possible_bin_paths()
        self._init_installed_path(installed_path)


class IdeVSCode(IdeBase):
    """Class offers a set of VSCode launching utilities.

    For example:
        1. Check if VSCode is installed.
        2. Launch an VSCode.
    """

    def __init__(self, installed_path=None, config_reset=False):
        super().__init__(installed_path, config_reset)
        self._ide_name = constant.IDE_VSCODE

    def apply_optional_config(self):
        """Override to do nothing."""

    def _get_config_root_paths(self):
        """Override to do nothing."""


class IdeLinuxVSCode(IdeVSCode):
    """Class offers a set of VSCode launching utilities for OS Linux."""

    def __init__(self, installed_path=None, config_reset=False):
        super().__init__(installed_path, config_reset)
        self._bin_file_name = 'code'
        self._bin_folders = ['/usr/bin']
        self._bin_paths = self._get_possible_bin_paths()
        self._init_installed_path(installed_path)


class IdeMacVSCode(IdeVSCode):
    """Class offers a set of VSCode launching utilities for OS Mac."""

    def __init__(self, installed_path=None, config_reset=False):
        super().__init__(installed_path, config_reset)
        self._bin_file_name = 'code'
        self._bin_folders = ['/usr/local/bin']
        self._bin_paths = self._get_possible_bin_paths()
        self._init_installed_path(installed_path)


def get_ide_util_instance(ide='j'):
    """Get an IdeUtil class instance for launching IDE.

    Args:
        ide: A key character of IDE to be launched. Default ide='j' is to
            launch IntelliJ.

    Returns:
        An IdeUtil class instance.
    """
    conf = project_config.ProjectConfig.get_instance()
    if not conf.is_launch_ide:
        return None
    is_mac = (android_dev_os.AndroidDevOS.MAC == android_dev_os.AndroidDevOS.
              get_os_type())
    tool = IdeUtil(conf.ide_installed_path, ide, conf.config_reset, is_mac)
    if not tool.is_ide_installed():
        ipath = conf.ide_installed_path or tool.get_default_path()
        err = _NO_LAUNCH_IDE_CMD.format(constant.IDE_NAME_DICT[ide], ipath)
        logging.error(err)
        stack_trace = common_util.remove_user_home_path(err)
        logs = '%s exists? %s' % (common_util.remove_user_home_path(ipath),
                                  os.path.exists(ipath))
        aidegen_metrics.ends_asuite_metrics(constant.IDE_LAUNCH_FAILURE,
                                            stack_trace,
                                            logs)
        raise errors.IDENotExistError(err)
    return tool


def _get_ide(installed_path=None, ide='j', config_reset=False, is_mac=False):
    """Get IDE to be launched according to the ide input and OS type.

    Args:
        installed_path: The IDE installed path to be checked.
        ide: A key character of IDE to be launched. Default ide='j' is to
            launch IntelliJ.
        config_reset: A boolean, if true reset configuration data.

    Returns:
        A corresponding IDE instance.
    """
    if is_mac:
        return _get_mac_ide(installed_path, ide, config_reset)
    return _get_linux_ide(installed_path, ide, config_reset)


def _get_mac_ide(installed_path=None, ide='j', config_reset=False):
    """Get IDE to be launched according to the ide input for OS Mac.

    Args:
        installed_path: The IDE installed path to be checked.
        ide: A key character of IDE to be launched. Default ide='j' is to
            launch IntelliJ.
        config_reset: A boolean, if true reset configuration data.

    Returns:
        A corresponding IDE instance.
    """
    if ide == 'e':
        return IdeMacEclipse(installed_path, config_reset)
    if ide == 's':
        return IdeMacStudio(installed_path, config_reset)
    if ide == 'c':
        return IdeMacCLion(installed_path, config_reset)
    if ide == 'v':
        return IdeMacVSCode(installed_path, config_reset)
    return IdeMacIntelliJ(installed_path, config_reset)


def _get_linux_ide(installed_path=None, ide='j', config_reset=False):
    """Get IDE to be launched according to the ide input for OS Linux.

    Args:
        installed_path: The IDE installed path to be checked.
        ide: A key character of IDE to be launched. Default ide='j' is to
            launch IntelliJ.
        config_reset: A boolean, if true reset configuration data.

    Returns:
        A corresponding IDE instance.
    """
    if ide == 'e':
        return IdeLinuxEclipse(installed_path, config_reset)
    if ide == 's':
        return IdeLinuxStudio(installed_path, config_reset)
    if ide == 'c':
        return IdeLinuxCLion(installed_path, config_reset)
    if ide == 'v':
        return IdeLinuxVSCode(installed_path, config_reset)
    return IdeLinuxIntelliJ(installed_path, config_reset)