# -*- coding: utf-8 -*-

########################################################################
#
# License: BSD
# Created: November 25, 2009
# Author: Francesc Alted - faltet@pytables.com
#
# $Id$
#
########################################################################

"""Create links in the HDF5 file.

This module implements containers for soft and external links.  Hard
links doesn't need a container as such as they are the same as regular
nodes (groups or leaves).

Classes:

    SoftLink
    ExternalLink

Functions:

Misc variables:

"""

import os
import tables
from tables import linkextension
from tables.node import Node
from tables.utils import lazyattr
from tables.attributeset import AttributeSet
import tables.file
from tables._past import previous_api, previous_api_property


def _g_get_link_class(parent_id, name):
    """Guess the link class."""

    return linkextension._get_link_class(parent_id, name)

_g_getLinkClass = previous_api(_g_get_link_class)


class Link(Node):
    """Abstract base class for all PyTables links.

    A link is a node that refers to another node.  The Link class inherits from
    Node class and the links that inherits from Link are SoftLink and
    ExternalLink.  There is not a HardLink subclass because hard links behave
    like a regular Group or Leaf.  Contrarily to other nodes, links cannot have
    HDF5 attributes.  This is an HDF5 library limitation that might be solved
    in future releases.

    See :ref:`LinksTutorial` for a small tutorial on how to work with links.

    .. rubric:: Link attributes

    .. attribute:: target

        The path string to the pointed node.

    """

    # Properties
    @lazyattr
    def _v_attrs(self):
        """
        A *NoAttrs* instance replacing the typical *AttributeSet* instance of
        other node objects.  The purpose of *NoAttrs* is to make clear that
        HDF5 attributes are not supported in link nodes.
        """
        class NoAttrs(AttributeSet):
            def __getattr__(self, name):
                raise KeyError("you cannot get attributes from this "
                               "`%s` instance" % self.__class__.__name__)

            def __setattr__(self, name, value):
                raise KeyError("you cannot set attributes to this "
                               "`%s` instance" % self.__class__.__name__)

            def _g_close(self):
                pass
        return NoAttrs(self)

    def __init__(self, parentnode, name, target=None, _log=False):
        self._v_new = target is not None
        self.target = target
        """The path string to the pointed node."""

        super(Link, self).__init__(parentnode, name, _log)

    # Public and tailored versions for copy, move, rename and remove methods
    def copy(self, newparent=None, newname=None,
             overwrite=False, createparents=False):
        """Copy this link and return the new one.

        See :meth:`Node._f_copy` for a complete explanation of the arguments.
        Please note that there is no recursive flag since links do not have
        child nodes.

        """

        newnode = self._f_copy(newparent=newparent, newname=newname,
                               overwrite=overwrite,
                               createparents=createparents)
        # Insert references to a `newnode` via `newname`
        newnode._v_parent._g_refnode(newnode, newname, True)
        return newnode

    def move(self, newparent=None, newname=None, overwrite=False):
        """Move or rename this link.

        See :meth:`Node._f_move` for a complete explanation of the arguments.

        """

        return self._f_move(newparent=newparent, newname=newname,
                            overwrite=overwrite)

    def remove(self):
        """Remove this link from the hierarchy."""

        return self._f_remove()

    def rename(self, newname=None, overwrite=False):
        """Rename this link in place.

        See :meth:`Node._f_rename` for a complete explanation of the arguments.

        """

        return self._f_rename(newname=newname, overwrite=overwrite)

    def __repr__(self):
        return str(self)


class SoftLink(linkextension.SoftLink, Link):
    """Represents a soft link (aka symbolic link).

    A soft link is a reference to another node in the *same* file hierarchy.
    Getting access to the pointed node (this action is called *dereferrencing*)
    is done via the __call__ special method (see below).

    """

    # Class identifier.
    _c_classid = 'SOFTLINK'

    _c_classId = previous_api_property('_c_classid')

    def __call__(self):
        """Dereference `self.target` and return the object.

        Examples
        --------

        ::

            >>> f=tables.open_file('data/test.h5')
            >>> print(f.root.link0)
            /link0 (SoftLink) -> /another/path
            >>> print(f.root.link0())
            /another/path (Group) ''

        """

        target = self.target
        # Check for relative pathnames
        if not self.target.startswith('/'):
            target = self._v_parent._g_join(self.target)
        return self._v_file._get_node(target)

    def __str__(self):
        """Return a short string representation of the link.

        Examples
        --------

        ::

            >>> f=tables.open_file('data/test.h5')
            >>> print(f.root.link0)
            /link0 (SoftLink) -> /path/to/node

        """

        classname = self.__class__.__name__
        target = self.target
        # Check for relative pathnames
        if not self.target.startswith('/'):
            target = self._v_parent._g_join(self.target)
        if target in self._v_file:
            dangling = ""
        else:
            dangling = " (dangling)"
        return "%s (%s) -> %s%s" % (self._v_pathname, classname,
                                    self.target, dangling)


class ExternalLink(linkextension.ExternalLink, Link):
    """Represents an external link.

    An external link is a reference to a node in *another* file.
    Getting access to the pointed node (this action is called
    *dereferencing*) is done via the :meth:`__call__` special method
    (see below).

    .. rubric:: ExternalLink attributes

    .. attribute:: extfile

        The external file handler, if the link has been dereferenced.
        In case the link has not been dereferenced yet, its value is
        None.

    """

    # Class identifier.
    _c_classid = 'EXTERNALLINK'

    _c_classId = previous_api_property('_c_classid')

    def __init__(self, parentnode, name, target=None, _log=False):
        self.extfile = None
        """The external file handler, if the link has been dereferenced.
        In case the link has not been dereferenced yet, its value is
        None."""
        super(ExternalLink, self).__init__(parentnode, name, target, _log)

    def _get_filename_node(self):
        """Return the external filename and nodepath from `self.target`."""

        # This is needed for avoiding the 'C:\\file.h5' filepath notation
        filename, target = self.target.split(':/')
        return filename, '/' + target

    def __call__(self, **kwargs):
        """Dereference self.target and return the object.

        You can pass all the arguments supported by the :func:`open_file`
        function (except filename, of course) so as to open the referenced
        external file.

        Examples
        --------

        ::

            >>> f=tables.open_file('data1/test1.h5')
            >>> print(f.root.link2)
            /link2 (ExternalLink) -> data2/test2.h5:/path/to/node
            >>> plink2 = f.root.link2('a')  # open in 'a'ppend mode
            >>> print(plink2)
            /path/to/node (Group) ''
            >>> print(plink2._v_filename)
            'data2/test2.h5'        # belongs to referenced file

        """

        filename, target = self._get_filename_node()

        if not os.path.isabs(filename):
            # Resolve the external link with respect to the this
            # file's directory.  See #306.
            base_directory = os.path.dirname(self._v_file.filename)
            filename = os.path.join(base_directory, filename)

        if self.extfile is None or not self.extfile.isopen:
            self.extfile = tables.open_file(filename, **kwargs)
        else:
            # XXX: implement better consistency checks
            assert self.extfile.filename == filename
            assert self.extfile.mode == kwargs.get('mode', 'r')

        return self.extfile._get_node(target)

    def umount(self):
        """Safely unmount self.extfile, if opened."""

        extfile = self.extfile
        # Close external file, if open
        if extfile is not None and extfile.isopen:
            extfile.close()
            self.extfile = None

    def _f_close(self):
        """Especific close for external links."""

        self.umount()
        super(ExternalLink, self)._f_close()

    def __str__(self):
        """Return a short string representation of the link.

        Examples
        --------

        ::

            >>> f=tables.open_file('data1/test1.h5')
            >>> print(f.root.link2)
            /link2 (ExternalLink) -> data2/test2.h5:/path/to/node

        """

        classname = self.__class__.__name__
        return "%s (%s) -> %s" % (self._v_pathname, classname, self.target)


## Local Variables:
## mode: python
## py-indent-offset: 4
## tab-width: 4
## fill-column: 72
## End:
