# Copyright (c) 2015 - present Facebook, Inc.
# All rights reserved.
#
# This source code is licensed under the BSD style license found in the
# LICENSE file in the root directory of this source tree. An additional grant
# of patent rights can be found in the PATENTS file in the same directory.

import argparse
import os
import subprocess
import traceback
import util
import logging

from inferlib import utils

MODULE_NAME = __name__
MODULE_DESCRIPTION = '''Run analysis of code built with a command like:
buck [options] [target]

Analysis examples:
infer -- buck build HelloWorld'''


def gen_instance(*args):
    return BuckAnalyzer(*args)


# This creates an empty argparser for the module, which provides only
# description/usage information and no arguments.
def create_argparser(group_name=MODULE_NAME):
    """This defines the set of arguments that get added by this module to the
    set of global args defined in the infer top-level module
    Do not use this function directly, it should be invoked by the infer
    top-level module"""
    parser = argparse.ArgumentParser(add_help=False)
    group = parser.add_argument_group(
        "{grp} module".format(grp=MODULE_NAME),
        description=MODULE_DESCRIPTION,
    )
    group.add_argument('--verbose', action='store_true',
                       help='Print buck compilation steps')
    group.add_argument('--no-cache', action='store_true',
                       help='Do not use buck distributed cache')
    group.add_argument('--print-harness', action='store_true',
                       help='Print generated harness code (Android only)')
    group.add_argument('--use-flavors', action='store_true',
                       help='Run Infer analysis through the use of flavors. '
                            'Currently this is supported only for the cxx_* '
                            'targets of Buck - e.g. cxx_library, cxx_binary - '
                            'and not for Java. Note: this flag should be used '
                            'in combination with passing the #infer flavor '
                            'to the Buck target.')
    group.add_argument('--xcode-developer-dir',
                       help='Specify the path to Xcode developer directory '
                            '(requires --use-flavors to work)')
    return parser


class BuckAnalyzer:
    def __init__(self, args, cmd):
        self.args = args
        self.cmd = cmd
        util.log_java_version()
        logging.info(util.run_cmd_ignore_fail(['buck', '--version']))

    def capture(self):
        try:
            if self.args.use_flavors:
                return self.capture_with_flavors()
            else:
                return self.capture_without_flavors()
        except subprocess.CalledProcessError as exc:
            if self.args.debug:
                traceback.print_exc()
            return exc.returncode

    def create_cxx_buck_configuration_args(self):
        # return a string that can be passed in input to buck
        # and configures the paths to infer/clang/plugin/xcode
        facebook_clang_plugins_root = utils.FCP_DIRECTORY
        clang_path = os.path.join(
            facebook_clang_plugins_root,
            'clang',
            'bin',
            'clang',
        )
        plugin_path = os.path.join(
            facebook_clang_plugins_root,
            'libtooling',
            'build',
            'FacebookClangPlugin.dylib',
        )
        args = [
            '--config',
            'infer.infer_bin={bin}'.format(bin=utils.get_infer_bin()),
            '--config',
            'infer.clang_compiler={clang}'.format(clang=clang_path),
            '--config',
            'infer.clang_plugin={plugin}'.format(plugin=plugin_path),
        ]
        if self.args.xcode_developer_dir is not None:
            args.append('--config')
            args.append('apple.xcode_developer_dir={devdir}'.format(
                devdir=self.args.xcode_developer_dir))
        return args

    def _get_analysis_result_files(self):
        # TODO(8610738): Make targets extraction smarter
        buck_results_cmd = [
            self.cmd[0],
            'targets',
            '--show-output'
        ] + self.cmd[2:] + self.create_cxx_buck_configuration_args()
        proc = subprocess.Popen(buck_results_cmd, stdout=subprocess.PIPE)
        (buck_output, _) = proc.communicate()
        # remove target name prefixes from each line and split them into a list
        out = [x.split(None, 1)[1] for x in buck_output.strip().split('\n')]
        # from the resulting list, get only what ends in json
        return [x for x in out if x.endswith('.json')]

    def _run_buck_with_flavors(self):
        # TODO: Use buck to identify the project's root folder
        if not os.path.isfile('.buckconfig'):
            print('Please run this command from the folder where .buckconfig '
                  'is located')
            return os.EX_USAGE
        subprocess.check_call(
            self.cmd + self.create_cxx_buck_configuration_args())
        return os.EX_OK

    def capture_with_flavors(self):
        ret = self._run_buck_with_flavors()
        if not ret == os.EX_OK:
            return ret
        result_files = self._get_analysis_result_files()
        merged_results_path = \
            os.path.join(self.args.infer_out, utils.JSON_REPORT_FILENAME)
        utils.merge_json_reports(
            result_files,
            merged_results_path)
        # TODO: adapt infer.print_errors to support json and print on screen
        print('Results saved in {results_path}'.format(
            results_path=merged_results_path))
        return os.EX_OK

    def capture_without_flavors(self):
        # BuckAnalyze is a special case, and we run the analysis from here
        capture_cmd = [utils.get_cmd_in_bin_dir('BuckAnalyze')]
        if self.args.infer_out is not None:
            capture_cmd += ['--out', self.args.infer_out]
        if self.args.debug:
            capture_cmd.append('-g')
        if self.args.no_filtering:
            capture_cmd.append('--no-filtering')
        if self.args.verbose:
            capture_cmd.append('--verbose')
        if self.args.no_cache:
            capture_cmd.append('--no-cache')
        if self.args.print_harness:
            capture_cmd.append('--print-harness')
        capture_cmd += self.cmd[2:]  # TODO: make extraction of targets smarter
        capture_cmd += ['--analyzer', self.args.analyzer]
        subprocess.check_call(capture_cmd)
        return os.EX_OK