"""
This module is an instrumentation library. It is not
used inside the FauxPy tool itself. It is used as
a library to instrument the project on which
the fault localization is applied.
"""

# TODO: To slow and risky. Find a better solution. Specially for
#  the counter. Using {pytest-xdist, --forked} seems to be a solution.
#  It also runs each test in a different session which seems to be
#  the best choice for NN programs.
import os.path
import threading
import json
from fauxpy import constants

# The predicate switching paper does not talk about situations
# in which one predicate is executed in multiple threads.
# Thus, preventing race conditions is not needed.
# However, applying fault localization on a real project
# might cause such situations due to the project being
# multi-threaded. Thus, we prevent race conditions anyway by using
# the following statement, to stay one the safe side.
_ATOMIC = threading.Lock()

_CONFIG_FILE_NAME = (
    constants.getCollectModeConfigFileName()
)  # For Run mode. Must be provided.
_EXE_PRED_SEQ_FILE_NAME = (
    constants.getCollectModeExecutedPredicateSequenceFileName()
)  # For Info mode. Generated by this library.
_EVAL_COUNT_FILE_NAME = (
    constants.getCollectModeEvaluationCounterFileName()
)  # For Run mode.
_EXCEPTION_SEEN_FILE_NAME = constants.getExceptionSeenFileName()  # For Run mode.

# This library can be used in two modes: "Run"/"Info".
# Info mode is used to count the number of times each
# predicate is executed and in which order (i.e., collecting
# the predicate instances). It is activated if
# the config file does not exist in the root directory.
# Run mode is used in the predicate switching algorithm.
# In Run mode, each time, one predicate instance is switched.
_MODE = "Info"

_PREDICATE_NAME = ""  # For Run mode.
_INSTANCE_NUMBER = -1  # For Run mode. Instance numbers start from 0.


def init():
    global _PREDICATE_NAME, _INSTANCE_NUMBER, _MODE, _CONFIG_FILE_NAME

    try:
        with open(_CONFIG_FILE_NAME) as confFile:
            confContent = confFile.read()
            confDict = json.loads(confContent)
            _PREDICATE_NAME = confDict["PredicateName"]
            _INSTANCE_NUMBER = int(confDict["InstanceNumber"])
            _MODE = "Run"
    except FileNotFoundError:
        _MODE = "Info"


def _isTargetPredicate(predicateName):
    global _PREDICATE_NAME

    return predicateName == _PREDICATE_NAME


def _isTargetInstance():
    global _ATOMIC, _INSTANCE_NUMBER

    with _ATOMIC:
        if os.path.exists(_EVAL_COUNT_FILE_NAME):
            with open(_EVAL_COUNT_FILE_NAME, "r") as fileRead:
                evaluationCounter = int(fileRead.read())
        else:
            with open(_EVAL_COUNT_FILE_NAME, "w") as fileWrite:
                fileWrite.write("0")
                evaluationCounter = 0

        areEqual = evaluationCounter == _INSTANCE_NUMBER

    return areEqual


def _increaseCounter():
    global _ATOMIC, _EVAL_COUNT_FILE_NAME

    with _ATOMIC:
        with open(_EVAL_COUNT_FILE_NAME, "r") as fileRead:
            evaluationCounter = int(fileRead.read())
            evaluationCounter = evaluationCounter + 1
        with open(_EVAL_COUNT_FILE_NAME, "w") as fileWrite:
            fileWrite.write(str(evaluationCounter))


def _addToPredicateSequence(predicateName):
    global _ATOMIC, _EXE_PRED_SEQ_FILE_NAME

    with _ATOMIC:
        with open(_EXE_PRED_SEQ_FILE_NAME, "a") as infoFile:
            infoFile.write(predicateName + ",")


def wrap_pred_to_switch(predicate, predicateName):
    global _MODE

    predReturn = predicate

    if _MODE == "Run":
        if _isTargetPredicate(predicateName):
            if _isTargetInstance():
                predReturn = not predicate
            _increaseCounter()
    elif _MODE == "Info":
        _addToPredicateSequence(predicateName)
    else:
        # This one should never happen.
        raise Exception(
            f"Problem in the instrumentation library. "
            f"Mode {_MODE} is not supported."
        )

    return predReturn


def exception_seen_at_next_line(exception_name):
    global _ATOMIC, _EXCEPTION_SEEN_FILE_NAME

    if _MODE == "Run":
        with _ATOMIC:
            with open(_EXCEPTION_SEEN_FILE_NAME, "a") as infoFile:
                infoFile.write(exception_name + ",")


init()
