#!/usr/bin/env python

import json
import logging
import shutil
import signal
import subprocess
import sys
import tempfile
import time
import types
import os
import urlparse

from benchmark_builder import BenchmarkBuilder
from benchmark_results import BenchmarkResults
from browser_driver.browser_driver_factory import BrowserDriverFactory
from http_server_driver.http_server_driver_factory import HTTPServerDriverFactory
from utils import timeout


_log = logging.getLogger(__name__)


class BenchmarkRunner(object):

    def __init__(self, plan_file, local_copy, count_override, build_dir, output_file, platform, browser, scale_unit=True, device_id=None):
        try:
            plan_file = self._find_plan_file(plan_file)
            with open(plan_file, 'r') as fp:
                self._plan_name = os.path.split(os.path.splitext(plan_file)[0])[1]
                self._plan = json.load(fp)
                if not 'options' in self._plan:
                    self._plan['options'] = {}
                if local_copy:
                    self._plan['local_copy'] = local_copy
                if count_override:
                    self._plan['count'] = count_override
                self._browser_driver = BrowserDriverFactory.create(platform, browser)
                self._http_server_driver = HTTPServerDriverFactory.create(platform)
                self._http_server_driver.set_device_id(device_id)
                self._build_dir = os.path.abspath(build_dir) if build_dir else None
                self._output_file = output_file
                self._scale_unit = scale_unit
                self._device_id = device_id
        except IOError as error:
            _log.error('Can not open plan file: {plan_file} - Error {error}'.format(plan_file=plan_file, error=error))
            raise error
        except ValueError as error:
            _log.error('Plan file: {plan_file} may not follow JSON format - Error {error}'.format(plan_file=plan_file, error=error))
            raise error

    def _find_plan_file(self, plan_file):
        if not os.path.exists(plan_file):
            absPath = os.path.join(os.path.dirname(__file__), 'data/plans', plan_file)
            if os.path.exists(absPath):
                return absPath
            if not absPath.endswith('.plan'):
                absPath += '.plan'
            if os.path.exists(absPath):
                return absPath
        return plan_file

    def _get_result(self, test_url):
        result = self._browser_driver.add_additional_results(test_url, self._http_server_driver.fetch_result())
        assert(not self._http_server_driver.get_return_code())
        return result

    def _run_one_test(self, web_root, test_file):
        result = None
        try:
            self._http_server_driver.serve(web_root)
            url = urlparse.urljoin(self._http_server_driver.base_url(), self._plan_name + '/' + test_file)
            self._browser_driver.launch_url(url, self._plan['options'], self._build_dir)
            with timeout(self._plan['timeout']):
                result = self._get_result(url)
        finally:
            self._browser_driver.close_browsers()
            self._http_server_driver.kill_server()

        return json.loads(result)

    def _run_benchmark(self, count, web_root):
        results = []
        debug_outputs = []
        for iteration in xrange(1, count + 1):
            _log.info('Start the iteration {current_iteration} of {iterations} for current benchmark'.format(current_iteration=iteration, iterations=count))
            try:
                self._browser_driver.prepare_env(self._device_id)

                if 'entry_point' in self._plan:
                    result = self._run_one_test(web_root, self._plan['entry_point'])
                    debug_outputs.append(result.pop('debugOutput', None))
                    assert(result)
                    results.append(result)
                elif 'test_files' in self._plan:
                    run_result = {}
                    for test in self._plan['test_files']:
                        result = self._run_one_test(web_root, test)
                        assert(result)
                        run_result = self._merge(run_result, result)
                        debug_outputs.append(result.pop('debugOutput', None))

                    results.append(run_result)
                else:
                    raise Exception('Plan does not contain entry_point or test_files')

            finally:
                self._browser_driver.restore_env()
                self._browser_driver.close_browsers()

            _log.info('End the iteration {current_iteration} of {iterations} for current benchmark'.format(current_iteration=iteration, iterations=count))

        results = self._wrap(results)
        output_file = self._output_file if self._output_file else self._plan['output_file']
        self._dump(self._merge({'debugOutput': debug_outputs}, results), output_file)
        self.show_results(results, self._scale_unit)

    def execute(self):
        with BenchmarkBuilder(self._plan_name, self._plan) as web_root:
            self._run_benchmark(int(self._plan['count']), web_root)

    @classmethod
    def _dump(cls, results, output_file):
        _log.info('Dumping the results to file {output_file}'.format(output_file=output_file))
        try:
            with open(output_file, 'w') as fp:
                json.dump(results, fp)
        except IOError as error:
            _log.error('Cannot open output file: {output_file} - Error: {error}'.format(output_file=output_file, error=error))
            _log.error('Results are:\n {result}'.format(result=json.dumps(results)))

    @classmethod
    def _wrap(cls, dicts):
        _log.debug('Merging following results:\n{results}'.format(results=json.dumps(dicts)))
        if not dicts:
            return None
        ret = {}
        for dic in dicts:
            ret = cls._merge(ret, dic)
        _log.debug('Results after merging:\n{result}'.format(result=json.dumps(ret)))
        return ret

    @classmethod
    def _merge(cls, a, b):
        assert(isinstance(a, type(b)))
        arg_type = type(a)
        # special handle for list type, and should be handle before equal check
        if arg_type == types.ListType and len(a) and (type(a[0]) == types.StringType or type(a[0]) == types.UnicodeType):
            return a
        if arg_type == types.DictType:
            result = {}
            for key, value in a.items():
                if key in b:
                    result[key] = cls._merge(value, b[key])
                else:
                    result[key] = value
            for key, value in b.items():
                if key not in result:
                    result[key] = value
            return result
        # for other types
        return a + b

    @classmethod
    def show_results(cls, results, scale_unit=True):
        results = BenchmarkResults(results)
        print results.format(scale_unit)
