import json
import os
import sys
import traceback

from .basics import Identifier, parse_identifier, create_preliminary_identifier, FileObject, parse_file_object, Tag, parse_tag, EventRequest, ProgramExecutionRequest, DisplayMessageRequest


##############################################################################################################
# constants
##############################################################################################################


PATH_PREFIX = '/collab'
INPUT_FILE_LOCATION = os.path.join(PATH_PREFIX, 'in.txt')
OUTPUT_FILE_LOCATION = os.path.join(PATH_PREFIX, 'out.txt')

_SOURCE_OF_TRUTH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'TRUTH.txt')
_truth = None
def _get_truth():
    """
    load constants from a file that is autogenerated from the outside when this module is compiled.
    This is a convenient way to share constants between different applications during development.
    Uses a cache.
    """
    global _truth
    if _truth is None:
        with open(_SOURCE_OF_TRUTH, 'r') as f:
            _truth = json.load(f)
    return _truth

def _get_valid_identifier_types():
    """
    returns a list of strings that are all the valid types of identifiers.
    """
    res = [a[0] for a in _get_truth()['valid_identifier_types']]
    return res

def _get_valid_event_request_names():
    """
    returns a list of strings that are all the valid types of event requests.
    """
    res = [a[0] for a in _get_truth()['valid_event_request_types']]
    return res


##############################################################################################################
# a context manager to take care of logging and error handling
##############################################################################################################


_the_manager = None
class manager:
    """
    Context manager for managing logging and error handling.
    """
    def __init__(self, suppress_exceptions_after_logging_them=False, redirect_stdout_to_log=True):
        global _the_manager
        if _the_manager is None:
            _the_manager = self
        else:
            warning = "only one manager of the collab library should be created!"
            print(warning)
            raise IOException(warning)
        self.suppress_exceptions_after_logging_them = suppress_exceptions_after_logging_them
        self.error_file = os.path.join(PATH_PREFIX, 'error.txt')
        self.log_file = os.path.join(PATH_PREFIX, 'log.txt')
        self.redirect_stdout_to_log = redirect_stdout_to_log
    def __enter__(self):
        self.log_file_stream = open(self.log_file, 'w')
        self.log_file_stream.__enter__()
        if self.redirect_stdout_to_log:
            self.previous_stdout = sys.stdout
            sys.stdout = self.log_file_stream
        return self
    def __exit__(self, etype, value, exception_traceback):
        # stop redirecting stdout to the log file
        if self.redirect_stdout_to_log:
            sys.stdout = self.previous_stdout
        # close the log file
        self.log_file_stream.__exit__()
        # make the output available
        make_output_available()
        # log any uncaught exceptions that may have occurred
        if exception_traceback:
            error_message = "%s\n-----\n%s\n-----\n" % (etype, value,)
            with open(self.error_file, 'w') as f:
                f.write(error_message)
                traceback.print_tb(exception_traceback, limit=None, file=f)
            # allow the exception to continue
            return self.suppress_exceptions_after_logging_them
        else:
            # no error, so nothing needs to be done
            pass
    def log(self, message):
        """log a message to the log file.
        This file is available via the collab website for inspection, but is not available to other programs running in collab."""
        self.log_file_stream.write(message)
        self.log_file_stream.write('\n')


def log(message):
    """uses the context manager for logging"""
    if _the_manager is None:
        raise IOError("the collab library's manager needs to be created in order to use logging.")
    _the_manager.log(message)


##############################################################################################################
# input
##############################################################################################################


_input = None
def _get_input():
    """
    parse the input file. This uses a cache, so it only has to be done once.
    """
    global _input
    if _input is None:
        with open(INPUT_FILE_LOCATION, 'r') as f:
            _input = json.load(f)
    return _input


def get_current_step():
    """
    returns an integer indicating the number of the current step in the execution environment.
    """
    return _get_input()['current_step']


def get_inputs():
    """
    returns a list of inputs that were given to this program.
    """
    inputs = _get_input()['inputs']
    res = []
    for c,a in enumerate(inputs):
        obj = parse_file_object(a, c)
        obj.file = os.path.join(PATH_PREFIX, obj.file_name)
        res.append(obj)
    return res


class ProgramTrigger:
    """
    describes why this program was triggered
    """
    def __init__(self, event_type, reason):
        self.event_type = event_type
        self.reason = reason


def get_trigger():
    """
    returns a ProgramTrigger that describes why this program was called
    """
    trigger = _get_input()['trigger']
    res = ProgramTrigger(trigger['event_type'], trigger['reason'])
    return res


_all_earlier_tags = None
def get_tags():
    """
    returns a list of tags that were created earlier and given to this program as input.
    """
    global _all_earlier_tags
    if _all_earlier_tags is None:
        tags = _get_input()['tags']
        _all_earlier_tags = [parse_tag(a) for a in tags]
    return _all_earlier_tags


##############################################################################################################
# output
##############################################################################################################


_output_counter = 0
_output_files = []
def add_output_file():
    """
    create an output, to make the results of this program available to other programs and to the Execution Environment.
    you can create multiple outputs, and they will be added in the order in which they were created with this function.
    """
    global _output_counter
    global _output_files
    identifier = create_preliminary_identifier('file')
    file_name = "out_%d" % _output_counter
    creation_index = _output_counter
    creation_step = get_current_step()
    res = FileObject(identifier, file_name, creation_step, creation_index)
    res.file = os.path.join(PATH_PREFIX, res.file_name)
    _output_counter += 1
    _output_files.append(res)
    return res


_output_tags = []
def create_tag(symbol, *arguments):
    """
    creates a new Tag using a preexisting Collab Symbol and an arbitrary number of identifiers.
    Note that this Tag is preliminary and will be rejected by the Execution Environment if the Symbol does not actually exist,
    or is not accessible for this program, or if anything else is wrong.
    For any arguments, you may use either an Identifier or any class that has an identifier as a field of the same name,
    which is all of the classes used here.
    """
    global _output_tags
    identifier = create_preliminary_identifier('tag')
    res = Tag(identifier, symbol, *arguments)
    _output_tags.append(res)
    return res


_output_event_requests = []
def _add_event_request(request):
    """
    add a request to the Execution Environment.
    Note that this function works differently depending on the type of the request.
    """
    global _output_event_requests
    request_type = request.request_type
    if not request_type in _get_valid_event_request_names():
        raise ValueError("'%s' is not a valid type of event request" % (request_type,))
    _output_event_requests.append(request)


def request_program_execution(program_identifier, *argument_identifiers):
    """
    a request to execute a Program.
    This expects a program as its first argument, and files to use as parameters as the rest.
    The program may be identified as a String displaying the name of the program (in which case the latest version is picked),
    as a String of <name>#<version> (which identifies the version directly),
    or as an integer that is the program's ID (which is unambiguous and includes the version)
    """
    request = ProgramExecutionRequest().program(program_identifier).arguments(argument_identifiers)
    _add_event_request(request)
    return request


def request_to_display_message(message_text):
    """
    returns a DisplayMessageRequest, which is used for building a message to display to the user,
    with several options that can be added via method chaining.
    """
    request = DisplayMessageRequest().text(message_text)
    _add_event_request(request)
    return request


def make_output_available():
    """
    create the output file and write all previously added messages to the execution environemnt into it.
    This function is called automatically when the context manager closes, but it is possible to call it earlier, too,
    if you are unsure if the program might time out later, so that you can at least output some preliminary results.
    """
    res = {
        'output_files' : [a.to_json() for a in _output_files],
        'output_tags' : [a.to_json() for a in _output_tags],
        'event_requests' : [a.to_json() for a in _output_event_requests],
    }
    with open(OUTPUT_FILE_LOCATION, 'w') as f:
        json.dump(res, f, indent=4)



