# This file is part of the MapProxy project.
# Copyright (C) 2010 Omniscale <http://omniscale.de>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Tile caching (creation, caching and retrieval of tiles).

.. digraph:: Schematic Call Graph

    ranksep = 0.1;
    node [shape="box", height="0", width="0"]

    cl  [label="CacheMapLayer" href="<mapproxy.core.layer.CacheMapLayer>"]
    tm  [label="TileManager",  href="<TileManager>"];
    fc      [label="FileCache", href="<FileCache>"];
    s       [label="Source", href="<mapproxy.core.source.Source>"];

    {
        cl -> tm [label="load_tile_coords"];
        tm -> fc [label="load\\nstore\\nis_cached"];
        tm -> s  [label="get_map"]
    }

"""
from typing import Optional, Union, Sequence

from mapproxy.grid import TileCoord
from mapproxy.image import BlankImageResult, BaseImageResult

import logging

log = logging.getLogger('mapproxy.cache.tile')


class Tile:
    """
    Internal data object for all tiles. Stores the tile-``coord`` and the tile data.
    """

    def __init__(self, coord: Optional[TileCoord], result: Optional[BaseImageResult] = None, cacheable: bool = True):
        self.coord = coord
        self.image_result = result
        self.location = None
        self.stored = False
        self._cacheable = cacheable
        self.size: Optional[int] = None
        self.timestamp: Optional[float] = None

    @property
    def cacheable(self) -> 'CacheInfo':
        return CacheInfo(cacheable=self._cacheable, timestamp=self.timestamp,
                         size=self.size)

    @cacheable.setter
    def cacheable(self, cacheable: Union['CacheInfo', bool]):
        if isinstance(cacheable, bool):
            self._cacheable = cacheable
        else:  # assume cacheable is CacheInfo
            self._cacheable = cacheable.cacheable
            self.timestamp = cacheable.timestamp
            self.size = cacheable.size

    def image_result_buffer(self, *args, **kw):
        if self.image_result is not None:
            return self.image_result.as_buffer(*args, **kw)
        else:
            return None

    def image_result_image(self):
        if self.image_result is not None:
            return self.image_result.as_image()
        else:
            return None

    def is_missing(self) -> bool:
        """
        Returns ``True`` when the tile has no ``data``, except when the ``coord``
        is ``None``. It doesn't check if the tile exists.

        >>> Tile((1, 2, 3)).is_missing()
        True
        >>> Tile((1, 2, 3), './tmp/foo').is_missing()
        False
        >>> Tile(None).is_missing()
        False
        """
        if self.coord is None:
            return False
        return self.image_result is None

    def __eq__(self, other):
        """
        >>> Tile((0, 0, 1)) == Tile((0, 0, 1))
        True
        >>> Tile((0, 0, 1)) == Tile((1, 0, 1))
        False
        >>> Tile((0, 0, 1)) is None
        False
        """
        if isinstance(other, Tile):
            return (self.coord == other.coord and
                    self.image_result == other.image_result)
        else:
            return NotImplemented

    def __ne__(self, other):
        """
        >>> Tile((0, 0, 1)) != Tile((0, 0, 1))
        False
        >>> Tile((0, 0, 1)) != Tile((1, 0, 1))
        True
        >>> Tile((0, 0, 1)) != None
        True
        """
        equal_result = self.__eq__(other)
        if equal_result is NotImplemented:
            return NotImplemented
        else:
            return not equal_result

    def __repr__(self):
        return 'Tile(%r, image_result=%r)' % (self.coord, self.image_result)


class CacheInfo(object):
    def __init__(self, cacheable=True, timestamp=None, size=None):
        self.cacheable = cacheable
        self.timestamp = timestamp
        self.size = size

    def __bool__(self):
        return self.cacheable


class TileCollection(object):
    def __init__(self, tile_coords: Sequence[Optional[TileCoord]]):
        self.tiles = [Tile(coord) for coord in tile_coords]
        self.tiles_dict = {}
        for tile in self.tiles:
            self.tiles_dict[tile.coord] = tile

    def __getitem__(self, idx_or_coord):
        if isinstance(idx_or_coord, int):
            return self.tiles[idx_or_coord]
        if idx_or_coord in self.tiles_dict:
            return self.tiles_dict[idx_or_coord]
        return Tile(idx_or_coord)

    def __contains__(self, tile_or_coord):
        if isinstance(tile_or_coord, tuple):
            return tile_or_coord in self.tiles_dict
        if hasattr(tile_or_coord, 'coord'):
            return tile_or_coord.coord in self.tiles_dict
        return False

    def __len__(self):
        return len(self.tiles)

    def __iter__(self):
        return iter(self.tiles)

    @property
    def empty(self):
        """
        Returns True if no tile in this collection contains an image_result.
        """
        return all((t.image_result is None for t in self.tiles))

    @property
    def blank(self):
        """
        Returns True if all image_results for the tiles are BlankImageResults or have no image result at all.
        """
        return all((t.image_result is None or isinstance(t.image_result, BlankImageResult) for t in self.tiles))

    def __repr__(self):
        return 'TileCollection(%r)' % self.tiles
