"""This module implements DataFutures.

We have two basic types of futures:
    1. DataFutures which represent data objects
    2. AppFutures which represent the futures on App/Leaf tasks.
"""
import os
import logging
from concurrent.futures import Future

from parsl.dataflow.futures import AppFuture, _STATE_TO_DESCRIPTION_MAP, FINISHED
from parsl.app.errors import *
from parsl.data_provider.files import File

logger = logging.getLogger(__name__)


class DataFuture(Future):
    """A datafuture points at an AppFuture.

    We are simply wrapping a AppFuture, and adding the specific case where, if
    the future is resolved i.e file exists, then the DataFuture is assumed to be
    resolved.
    """

    def parent_callback(self, parent_fu):
        """Callback from executor future to update the parent.

        Args:
            - parent_fu (Future): Future returned by the executor along with callback

        Returns:
            - None

        Updates the super() with the result() or exception()
        """
        if parent_fu.done() is True:
            e = parent_fu._exception
            if e:
                super().set_exception(e)
            else:
                super().set_result(self.file_obj)
        return

    def __init__(self, fut, file_obj, tid=None):
        """Construct the DataFuture object.

        If the file_obj is a string convert to a File.

        Args:
            - fut (AppFuture) : AppFuture that this DataFuture will track
            - file_obj (string/File obj) : Something representing file(s)

        Kwargs:
            - tid (task_id) : Task id that this DataFuture tracks
        """
        super().__init__()
        self._tid = tid
        if isinstance(file_obj, str):
            self.file_obj = File(file_obj)
        elif isinstance(file_obj, File):
            self.file_obj = file_obj
        else:
            raise ValueError("DataFuture must be initialized with a str or File")
        self.parent = fut
        self._exception = None

        if fut is None:
            logger.debug("Setting result to filepath since no future was passed")
            self.set_result(self.file_obj)

        else:
            if isinstance(fut, Future):
                self.parent.add_done_callback(self.parent_callback)
            else:
                raise NotFutureError("DataFuture can be created only with a FunctionFuture on None")

        logger.debug("Creating DataFuture with parent: %s", self.parent)
        logger.debug("Filepath: %s", self.filepath)

    @property
    def tid(self):
        """Returns the task_id of the task that will resolve this DataFuture."""
        return self._tid

    @property
    def filepath(self):
        """Filepath of the File object this datafuture represents."""
        return self.file_obj.filepath

    @property
    def filename(self):
        """Filepath of the File object this datafuture represents."""
        return self.filepath

    def cancel(self):
        raise NotImplementedError("Cancel not implemented")

    def cancelled(self):
        return False

    def running(self):
        if self.parent:
            return self.parent.running()
        else:
            return False

    def __repr__(self):

        # The DataFuture could be wrapping an AppFuture whose parent is a Future
        # check to find the top level parent
        if isinstance(self.parent, AppFuture):
            parent = self.parent.parent
        else:
            parent = self.parent

        if parent:
            with parent._condition:
                if parent._state == FINISHED:
                    if parent._exception:
                        return '<%s at %#x state=%s raised %s>' % (
                            self.__class__.__name__,
                            id(self),
                            _STATE_TO_DESCRIPTION_MAP[parent._state],
                            parent._exception.__class__.__name__)
                    else:
                        return '<%s at %#x state=%s returned %s>' % (
                            self.__class__.__name__,
                            id(self),
                            _STATE_TO_DESCRIPTION_MAP[parent._state],
                            self.filepath)
                return '<%s at %#x state=%s>' % (
                    self.__class__.__name__,
                    id(self),
                    _STATE_TO_DESCRIPTION_MAP[parent._state])

        else:
            return '<%s at %#x state=%s>' % (
                self.__class__.__name__,
                id(self),
                _STATE_TO_DESCRIPTION_MAP[self._state])


def testing_nonfuture():
    fpath = '~/shuffled.txt'
    df = DataFuture(None, fpath)
    print(df)
    print("Result: ", df.filepath)
    assert df.filepath == os.path.abspath(os.path.expanduser(fpath))


if __name__ == "__main__":
    # logging.basicConfig(filename='futures.testing.log',level=logging.DEBUG)
    import sys
    import random
    logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
    logger.debug("Begin Testing")

    with open('shuffled.txt', 'w') as testfile:
        nums = list(range(0, 10000))
        random.shuffle(nums)
        for item in nums:
            testfile.write("{0}\n".format(item))

    foo = Future()  # type: Future[str]
    df = DataFuture(foo, './shuffled.txt')
    dx = DataFuture(foo, '~/shuffled.txt')

    print(foo.done())
    print(df.done())

    testing_nonfuture()

@@ -1976,39 +1976,8 @@
  fut
-%0A        self._exception = None
 %0A%0A  

Remove unused _exception member (#939)