#!/usr/bin/env python
from __future__ import unicode_literals
import sys
import os
import contextlib
from os.path import dirname, abspath
import signal
import traceback


# Local imports
from .settings import SERVER_TIMEOUT
from .language_registry import create_evaluator_instance
from .error_messages import prettify_exceptions

MY_DIR = abspath(dirname(__file__))
registry = None


# Raised when the code times-out.
# c.f. http://pguides.net/python/timeout-a-function
class TimeoutException(Exception):
    pass


@contextlib.contextmanager
def change_dir(path):
    cur_dir = abspath(dirname(MY_DIR))
    os.chdir(path)
    try:
        yield
    finally:
        os.chdir(cur_dir)


def timeout_handler(signum, frame):
    """A handler for the ALARM signal."""
    raise TimeoutException('Code took too long to run.')


def create_signal_handler():
    """Add a new signal handler for the execution of this code."""
    prev_handler = signal.signal(signal.SIGALRM, timeout_handler)
    signal.alarm(SERVER_TIMEOUT)
    return prev_handler


def set_original_signal_handler(old_handler=None):
    """Set back any original signal handler."""
    if old_handler is not None:
        signal.signal(signal.SIGALRM, old_handler)
        return
    else:
        raise Exception("Signal Handler: object cannot be NoneType")


def delete_signal_handler():
    signal.alarm(0)
    return


class Grader(object):
    """Tests the code obtained from Code Server"""
    def __init__(self, in_dir=None):
        msg = 'Code took more than %s seconds to run. You probably '\
              'have an infinite loop in your code.' % SERVER_TIMEOUT
        self.timeout_msg = msg
        self.in_dir = in_dir if in_dir else MY_DIR

    def evaluate(self, kwargs):
        """Evaluates given code with the test cases based on
        given arguments in test_case_data.

        The ref_code_path is a path to the reference code.
        The reference code will call the function submitted by the student.
        The reference code will check for the expected output.

        If the path's start with a "/" then we assume they are absolute paths.
        If not, we assume they are relative paths w.r.t. the location of this
        code_server script.

        If the optional `in_dir` keyword argument is supplied it changes the
        directory to that directory (it does not change it back to the original
        when done).

        Returns
        -------

        A tuple: (success, error, weight).
        """
        self.setup()
        test_case_instances = self.get_evaluator_objects(kwargs)
        with change_dir(self.in_dir):
            success, error, weight = self.safe_evaluate(test_case_instances)
        self.teardown()

        result = {'success': success, 'error': error, 'weight': weight}
        return result

    # Private Protocol ##########
    def setup(self):
        if self.in_dir:
            if not os.path.exists(self.in_dir):
                os.makedirs(self.in_dir)

    def get_evaluator_objects(self, kwargs):
        metadata = kwargs.get('metadata')
        test_case_data = kwargs.get('test_case_data')
        test_case_instances = []

        for test_case in test_case_data:
            test_case_instance = create_evaluator_instance(metadata, test_case)
            test_case_instances.append(test_case_instance)
        return test_case_instances

    def safe_evaluate(self, test_case_instances):
        """
        Handles code evaluation along with compilation, signal handling
        and Exception handling
        """
        # Add a new signal handler for the execution of this code.
        prev_handler = create_signal_handler()
        success = False
        test_case_success_status = [False]
        if len(test_case_instances) != 0:
            test_case_success_status = [False] * len(test_case_instances)
        error = []
        weight = 0.0

        # Do whatever testing needed.
        try:
            # Run evaluator selection registry here
            for idx, test_case_instance in enumerate(test_case_instances):
                test_case_success = False
                test_case_instance.compile_code()
                eval_result = test_case_instance.check_code()
                test_case_success, err, mark_fraction = eval_result
                if test_case_success:
                    weight += mark_fraction * test_case_instance.weight
                else:
                    error.append(err)
                test_case_success_status[idx] = test_case_success

            success = all(test_case_success_status)

            for test_case_instance in test_case_instances:
                test_case_instance.teardown()

        except TimeoutException:
            error.append(
                prettify_exceptions("TimeoutException", self.timeout_msg)
                )
        except Exception as e:
            exc_type, exc_value, exc_tb = sys.exc_info()
            tb_list = traceback.format_exception(exc_type, exc_value, exc_tb)
            try:
                line_no = e.lineno
            except AttributeError:
                line_no = traceback.extract_tb(exc_tb)[-1][1]
            if len(tb_list) > 2:
                del tb_list[1:3]
            error.append(
                prettify_exceptions(
                    exc_type.__name__, str(exc_value), "".join(tb_list),
                    line_no=line_no
                    )
                )
        finally:
            # Set back any original signal handler.
            set_original_signal_handler(prev_handler)

        return success, error, weight

    def teardown(self):
        # Cancel the signal
        delete_signal_handler()