#  Copyright (c) 2018 Uber Technologies, Inc.
#
# 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.

from __future__ import absolute_import, print_function

import argparse
import datetime
import logging
import shutil
import subprocess
import sys
import tempfile
import time
from os.path import join

from . import commandlineutil, projectgen
from .cpulogger import CPULogger
from .moduletree import ModuleGenType
from .statemanagement import SettingsState, XcodeManager
from .util import check_dependent_commands, grab_mac_marketing_name, makedir, sudo_enabled


class CommandLineMultisuite(object):

    @staticmethod
    def parse_config(args):
        tmp_root = join(tempfile.gettempdir(), 'mock_app_gen_out')
        log_root = join(tmp_root, 'logs')

        parser = argparse.ArgumentParser(
            description='Build all mock app types and log times & traces to a log file. '
            'Useful as a benchmark suite to compare build performance of different computers.')

        parser.add_argument('--log_dir', default=log_root, help="Where logs such as build times should exist."),
        parser.add_argument(
            '--app_gen_output_dir', default=tmp_root, help="Where generated mock apps should be outputted to."),
        parser.add_argument('--buck_command', default='buck', help="The path to the buck binary.  Defaults to `buck`."),

        commandlineutil.AppGenerationConfig.add_app_gen_options(parser)

        actions = parser.add_argument_group('Extra Actions')
        actions.add_argument(
            '--trace_cpu', action='store_true', default=False,
            help="If we should add cpu utilization to build traces."),
        actions.add_argument(
            '--switch_xcode_versions',
            action='store_true',
            default=False,
            help="Switch xcode verions as part of the full multisuite test. This will search your "
            "`/Applications` directory for xcode.app bundles to build with. Requires sudo."),
        actions.add_argument(
            '--full_clean',
            action='store_true',
            default=False,
            help="Clean all default xcode cache directories to prevent cache effects changing test results."),

        testing = parser.add_argument_group('Testing Shortcuts')
        testing.add_argument(
            '--skip_xcode_build',
            action='store_true',
            default=False,
            help="Skips building the mock apps.  Useful for speeding up testing and making "
            "integration tests independent of non-python dependencies."),
        testing.add_argument(
            '--test_build_only',
            action='store_true',
            default=False,
            help="Only builds a small flat build type to create short testing loops."),

        out = parser.parse_args(args)
        commandlineutil.AppGenerationConfig.validate_app_gen_options(out)

        return out

    # noinspection PyAttributeOutsideInit
    def config_to_vars(self, config):
        self.app_gen_options = commandlineutil.AppGenerationConfig()
        self.app_gen_options.pull_from_args(config)

        self.log_dir = config.log_dir
        self.output_dir = config.app_gen_output_dir
        self.buck_binary = config.buck_command

        logging.info('Log output directory: %s', self.log_dir)
        logging.info('Mock app gen output directory: %s', self.output_dir)

        self.trace_cpu = config.trace_cpu
        self.switch_xcode_versions = config.switch_xcode_versions
        self.full_clean = config.full_clean
        self.run_xcodebuild = (not config.skip_xcode_build)
        self.test_build_only = config.test_build_only

    def main(self, args=sys.argv[1:]):
        logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(funcName)s: %(message)s')

        args = self.parse_config(args)
        self.config_to_vars(args)

        try:
            self.run_multisuite()
        except Exception as e:
            print(e)
        finally:
            self.multisuite_cleanup()
            logging.info("Done")

    # noinspection PyAttributeOutsideInit
    def make_context(self, log_dir, output_dir, test_build):
        self.cpu_logger = CPULogger()
        self.xcode_manager = XcodeManager()
        self.settings_state = SettingsState(output_dir)

        self.log_dir = log_dir
        self.output_dir = output_dir

        self.mock_output_dir = join(output_dir, 'apps', 'mockapp')
        self.mock_app_workspace = join(self.mock_output_dir, 'App', 'MockApp.xcworkspace')
        self.buckconfig_path = join(output_dir, '.buckconfig.local')

        self.sys_info_path = join(log_dir, 'system_info.txt')
        self.build_time_path = join(log_dir, 'build_times.txt')
        self.build_time_csv_path = join(log_dir, 'build_times.csv')
        self.build_trace_path = join(log_dir, 'build_traces')
        self.buck_path = '/apps/mockapp'
        self.app_buck_path = "/{}/App:MockApp".format(self.buck_path)

        has_dot = self.app_gen_options.dot_file_path and self.app_gen_options.dot_root_node_name
        if test_build:
            logging.info("Using test build settings")
            self.app_gen_options.lines_of_code = 100000
            self.wmo_modes = [False]
            self.type_list = [ModuleGenType.dot] if has_dot else [ModuleGenType.flat]
        else:
            self.wmo_modes = [True, False]
            self.type_list = ModuleGenType.enum_list()
            if not has_dot:
                logging.warning("Removing dot mock app type due to lack of dot file to read. "
                                "Specify one in the command line options.")
                self.type_list.remove(ModuleGenType.dot)

    def build_app_type(self, gen_type, wmo_enabled):
        xcode_version, xcode_build_id = XcodeManager.get_current_xcode_version()
        xcode_name = '{}_'.format(xcode_version.replace('.', '_'))
        build_log_path = join(self.log_dir, '{}{}_mockapp_build_log.txt'.format(xcode_name, gen_type))

        gen_info = '{} (wmo_enabled: {}, xcode_version: {} {})'.format(gen_type, wmo_enabled, xcode_version,
                                                                       xcode_build_id)
        logging.info('##### Generating %s', gen_info)

        self.project_generator.use_wmo = wmo_enabled
        commandlineutil.del_old_output_dir(self.mock_output_dir)

        logging.info('Generating mock app')
        app_node, node_list = commandlineutil.gen_graph(gen_type, self.app_gen_options)
        self.project_generator.gen_app(app_node, node_list, self.app_gen_options.lines_of_code)

        swift_loc = commandlineutil.count_loc(self.mock_output_dir)
        logging.info('App type "%s" generated %d loc', gen_type, swift_loc)

        # Build App
        total_time = 0
        if self.run_xcodebuild:
            logging.info('Generate xcode workspace & clean')

            derived_data_path = join(tempfile.gettempdir(), 'ub_mockapp_derived_data')
            shutil.rmtree(derived_data_path, ignore_errors=True)
            makedir(derived_data_path)

            subprocess.check_call([self.buck_binary, 'project', self.app_buck_path, '-d'])
            if self.full_clean:
                self.xcode_manager.clean_caches()

            logging.info('Start xcodebuild')
            start = time.time()
            with open(build_log_path, 'w') as build_log_file:
                subprocess.check_call([
                    'xcodebuild', 'build', '-scheme', 'MockApp', '-sdk', 'iphonesimulator', '-workspace',
                    self.mock_app_workspace, '-derivedDataPath', derived_data_path
                ],
                                      stdout=build_log_file,
                                      stderr=build_log_file)
            end = time.time()
            total_time = int(end - start)
        else:
            logging.info('Skipping xcodebuild & buck project')

        # Log Results
        build_end = str(datetime.datetime.now())
        log_statement = '{} w/ {} (loc: {}) modules took {} s\n'.format(gen_info, len(node_list), swift_loc, total_time)
        logging.info(log_statement)
        self.build_time_file.write(log_statement)
        self.build_time_file.flush()
        full_xcode_version = xcode_version + " " + xcode_build_id
        self.build_time_csv_file.write('{}, {}, {}, {}, {}, {}, {}\n'.format(
            build_end, gen_type, full_xcode_version, wmo_enabled, total_time, len(node_list), swift_loc))
        self.build_time_csv_file.flush()

    def verify_dependencies(self):
        if not self.run_xcodebuild:
            return  # We don't need these binaries if we are not going to use them.

        xcode = ['xcodebuild', 'xcode-select']
        local = [self.buck_binary]
        missing = check_dependent_commands(xcode + local)

        if missing == xcode:
            logging.error("Xcode command line tools do not seem to be installed / are missing in your path.")
        elif missing == local:
            logging.error("Specified buck command not available.  Did you install it in your path?")

        if missing:
            logging.error("Missing required binaries: %s", str(missing))
            raise OSError("Missing required binaries: {}".format(missing))

    def dump_system_info(self):
        logging.info('Recording device info')
        with open(self.sys_info_path, 'w') as info_file:
            info_file.write(grab_mac_marketing_name())
            info_file.write('\n')
            info_file.flush()
            subprocess.check_call(['system_profiler', 'SPHardwareDataType', '-detailLevel', 'mini'], stdout=info_file)
            subprocess.check_call(['sw_vers'], stdout=info_file)

    # noinspection PyAttributeOutsideInit
    def multisuite_setup(self):
        self.make_context(self.log_dir, self.output_dir, self.test_build_only)
        self.settings_state.save_buckconfig_local()
        self.settings_state.save_xcode_select()

        if self.switch_xcode_versions:
            self.sudo_warning()
            self.xcode_paths = self.xcode_manager.discover_xcode_versions()
            self.xcode_versions = self.xcode_paths.keys()
            logging.info("Discovered xcode verions: %s", str(self.xcode_versions))
        else:
            self.xcode_paths = {}
            self.xcode_versions = [None]

        for path in [self.log_dir, self.build_trace_path, self.output_dir]:
            makedir(path)

        logging.info('Starting build session')
        self.build_time_file = open(self.build_time_path, 'a')
        self.build_time_csv_file = open(self.build_time_csv_path, 'a')

        now = str(datetime.datetime.now())
        self.build_time_file.write('Build session started at {}\n'.format(now))
        self.build_time_file.flush()

        self.dump_system_info()

        self.project_generator = projectgen.BuckProjectGenerator(self.mock_output_dir, self.buck_path)

        self.verify_dependencies()

    def multisuite_cleanup(self):
        logging.info("Cleaning up multisuite build test")
        self.settings_state.restore_buckconfig_local()

        if self.switch_xcode_versions:
            self.settings_state.restore_xcode_select()

        if self.build_time_file:
            self.build_time_file.close()
        if self.build_time_csv_file:
            self.build_time_csv_file.close()
        if self.trace_cpu:
            self.cpu_logger.kill()

    @staticmethod
    def sudo_warning():
        if not sudo_enabled():
            logging.warning('I would suggest executing this in a bash script and adding a sudo keep alive so you')
            logging.warning('can run this fully unattended: https://gist.github.com/cowboy/3118588')
            logging.warning("If you don't, then half way through the build suite, it will stall asking for a password.")
            logging.warning("I don't know how to make xcode-select -s not require sudo unfortunately.")

    def switch_xcode_version(self, xcode_version):
        logging.warning('Switching to xcode version %s', str(xcode_version))
        self.xcode_manager.switch_xcode_version(self.xcode_paths[xcode_version])

    def run_multisuite(self):
        self.multisuite_setup()

        start_time = time.time()
        if self.trace_cpu:
            self.cpu_logger.start()

        commandlineutil.make_custom_buckconfig_local(self.buckconfig_path)

        for xcode_version in self.xcode_versions:
            if self.switch_xcode_versions:
                self.switch_xcode_version(xcode_version)

            for wmo_enabled in self.wmo_modes:
                logging.info('Swift WMO Enabled: {}'.format(wmo_enabled))

                for gen_type in self.type_list:
                    self.build_app_type(gen_type, wmo_enabled)

        if self.trace_cpu:
            self.cpu_logger.stop()
            commandlineutil.apply_cpu_to_traces(self.build_trace_path, self.cpu_logger, start_time)


def main():
    CommandLineMultisuite().main()


if __name__ == '__main__':
    main()
