#-------------------------------------------------------------------------------
#
# Project: EOxServer <http://eoxserver.org>
# Authors: Fabian Schindler <fabian.schindler@eox.at>
#
#-------------------------------------------------------------------------------
# Copyright (C) 2013 EOX IT Services GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies of this Software or works derived from this Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#-------------------------------------------------------------------------------


""" This module provides Python file-object like access to VSI files.
"""


import os
from uuid import uuid4
from functools import wraps

if os.environ.get('READTHEDOCS', None) != 'True':
    import numpy

    from eoxserver.contrib.gdal import (
        VSIFOpenL, VSIFCloseL, VSIFReadL, VSIFWriteL, VSIFSeekL, VSIFTellL,
        VSIStatL, VSIFTruncateL, Unlink, Rename, FileFromMemBuffer
    )

    UINT32_MAX = numpy.iinfo('uint32').max

    rename = Rename

    unlink = remove = Unlink


def open(filename, mode="r"):
    """ A function mimicking the builtin function
    :func:`open <__builtins__.open>` but returning a :class:`VSIFile` instead.

    :param filename: the path to the file; this might also be any VSI special
                     path like "/vsicurl/..." or "/vsizip/...". See the `GDAL
                     documentation
                     <http://trac.osgeo.org/gdal/wiki/UserDocs/ReadInZip>`_
                     for reference.
    :param mode: the file opening mode
    :returns: a :class:`VSIFile`
    """
    return VSIFile(filename, mode)


def _ensure_open(func):
    @wraps(func)
    def wrapper(self, *args, **kwargs):
        if self._handle is None:
            raise ValueError('I/O operation on closed file')
        return func(self, *args, **kwargs)
    return wrapper


class VSIFile(object):
    """ File-like object interface for VSI file API.

    :param filename: the path to the file; this might also be any VSI special
                     path like "/vsicurl/..." or "/vsizip/...". See the `GDAL
                     documentation
                     <http://trac.osgeo.org/gdal/wiki/UserDocs/ReadInZip>`_
                     and `manuals
                     <http://www.gdal.org/gdal_virtual_file_systems.html>`_
                     for reference.
    :param mode: the file opening mode
    """

    def __init__(self, filename, mode="r"):
        self._handle = VSIFOpenL(filename, mode)
        self._filename = filename

        if self._handle is None:
            raise IOError("Failed to open file '%s'." % self._filename)

    @property
    def name(self):
        """ Returns the filename referenced by this file
        """
        return self._filename

    @_ensure_open
    def read(self, size=UINT32_MAX):
        """ Read from the file. If no ``size`` is specified, read until the end
        of the file.

        :param size: the number of bytes to be read
        :returns: the bytes read as a string
        """

        value = VSIFReadL(1, size, self._handle)
        return value if value is not None else ''

    @_ensure_open
    def write(self, data):
        """ Write the buffer ``data`` to the file.

        :param data: the string buffer to be written
        """
        VSIFWriteL(data, 1, len(data), self._handle)

    @_ensure_open
    def tell(self):
        """ Return the current read/write offset of the file.

        :returns: an integer offset
        """
        return VSIFTellL(self._handle)

    @_ensure_open
    def seek(self, offset, whence=os.SEEK_SET):
        """ Set the new read/write offset in the file.

        :param offset: the new offset
        :param whence: how the offset shall be interpreted; possible options are
                       :const:`os.SEEK_SET`, :const:`os.SEEK_CUR` and
                       :const:`os.SEEK_END`
        """
        VSIFSeekL(self._handle, offset, whence)

    def close(self):
        """ Close the file.
        """
        if self._handle is not None:
            VSIFCloseL(self._handle)
        self._handle = None

    @property
    def closed(self):
        """ Return a boolean value to indicate whether or not the file is
        already closed.
        """
        return (self._handle is None)

    def __iter__(self):
        """ Iterate over the lines within the file.
        """
        return self

    @_ensure_open
    def next(self):
        """ Satisfaction of the iterator protocol. Return the next line in the
            file or raise `StopIteration`.
        """
        line = self.readline()
        if not line:
            raise StopIteration
        return line

    @_ensure_open
    def readline(self, length=None, windowsize=1024):
        """ Read a single line from the file and return it.

        :param length: the maximum number of bytes to read to look for a whole
                       line.
        :param windowsize: the windowsize to search for a newline character.
        """
        line = ""
        while True:
            # read amount and detect for EOF
            string = self.read(length or windowsize)
            if not string:
                break

            try:
                position = string.index('\n')
                line += string[:position]

                # retun the cursor for the remainder of the string
                self.seek(-(len(string) - (position + 1)), os.SEEK_CUR)
                break
            except ValueError:
                line += string

            # also break when a specific size was requested but no newline was
            # found
            if length:
                break

        return line

    def readlines(self, sizehint=0):
        """ Read the remainder of the file (or up to `sizehint` bytes) and return
            the lines.

        :param sizehint: the number of bytes to scan for lines.
        :return: the lines
        :rtype: list of strings
        """
        # TODO: take sizehint into account
        lines = [line for line in self]
        return lines

    @property
    @_ensure_open
    def size(self):
        """ Return the size of the file in bytes
        """
        stat = VSIStatL(self.name)
        return stat.size

    @_ensure_open
    def flush(self):
        pass
        # VSIFlushL(self._handle)  # TODO: not available?

    @_ensure_open
    def truncate(self, size=None):
        """ Truncates the file to the given size or to the size until the current
            position.

        :param size: the new size of the file.
        """
        size = size or self.tell()
        VSIFTruncateL(self._handle, size)

    def isatty(self):
        """ Never a TTY """
        return False

    def __enter__(self):
        return self

    def __exit__(self, etype=None, evalue=None, tb=None):
        self.close()

    def __del__(self):
        self.close()


class TemporaryVSIFile(VSIFile):
    """ Subclass of VSIFile, that automatically deletes the physical file upon
        deletion.
    """

    @classmethod
    def from_buffer(cls, buf, mode="wb", filename=None):
        """ Creates a :class:`TemporaryVSIFile` from a string.

        :param buf: the supplied string
        :param mode: the file opening mode
        :param filename: the optional filename the file shall be stored under;
                         by default this is an in-memory location
        """
        if not filename:
            filename = "/vsimem/%s" % uuid4().hex
        f = cls(filename, mode)
        f.write(buf)
        f.seek(0)
        return f

    def close(self):
        """ Close the file. This also deletes it.
        """
        if not self.closed:
            super(TemporaryVSIFile, self).close()
            remove(self.name)


def join(first, *paths):
    """ Joins the given VSI path specifiers. Similar to :func:`os.path.join` but
        takes care of the VSI-specific handles such as `vsicurl`, `vsizip`, etc.
    """
    parts = first.split('/')
    for path in paths:
        new = path.split('/')
        if path.startswith('/vsi'):
            parts = new[0:2] + (parts if parts[0] else parts[1:]) + new[2:]
        else:
            parts.extend(new)
    return '/'.join(parts)
