// License: GPL. For details, see Readme.txt file.
package org.openstreetmap.gui.jmapviewer;

import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;

/**
 * {@link TileCache} implementation that stores all {@link Tile} objects in
 * memory up to a certain limit ({@link #getCacheSize()}). If the limit is
 * exceeded the least recently used {@link Tile} objects will be deleted.
 *
 * @author Jan Peter Stotz
 */
public class MemoryTileCache implements TileCache {

    protected static final Logger log = Logger.getLogger(MemoryTileCache.class.getName());

    /**
     * Default cache size
     */
    protected int cacheSize;

    protected final Map<String, CacheEntry> hash;

    /**
     * List of all tiles in their last recently used order
     */
    protected final CacheLinkedListElement lruTiles;

    /**
     * Constructs a new {@code MemoryTileCache}.
     */
    public MemoryTileCache() {
        this(200);
    }

    /**
     * Constructs a new {@code MemoryTileCache}.
     * @param cacheSize size of the cache
     */
    public MemoryTileCache(int cacheSize) {
        this.cacheSize = cacheSize;
        hash = new HashMap<>(cacheSize);
        lruTiles = new CacheLinkedListElement();
    }

    @Override
    public synchronized void addTile(Tile tile) {
        CacheEntry entry = createCacheEntry(tile);
        if (hash.put(tile.getKey(), entry) == null) {
            // only if hash hadn't had the element, add it to LRU
            lruTiles.addFirst(entry);
            if (hash.size() > cacheSize || lruTiles.getElementCount() > cacheSize) {
                removeOldEntries();
            }
        }
    }

    @Override
    public synchronized Tile getTile(TileSource source, int x, int y, int z) {
        CacheEntry entry = hash.get(Tile.getTileKey(source, x, y, z));
        if (entry == null)
            return null;
        lruTiles.moveElementToFirstPos(entry);
        return entry.tile;
    }

    /**
     * Removes the least recently used tiles
     */
    protected synchronized void removeOldEntries() {
        try {
            while (lruTiles.getElementCount() > cacheSize) {
                removeEntry(lruTiles.getLastElement());
            }
        } catch (NullPointerException e) {
            log.warning(e.getMessage());
        }
    }

    protected synchronized void removeEntry(CacheEntry entry) {
        hash.remove(entry.tile.getKey());
        lruTiles.removeEntry(entry);
    }

    protected CacheEntry createCacheEntry(Tile tile) {
        return new CacheEntry(tile);
    }

    @Override
    public synchronized void clear() {
        hash.clear();
        lruTiles.clear();
    }

    @Override
    public synchronized int getTileCount() {
        return hash.size();
    }

    @Override
    public synchronized int getCacheSize() {
        return cacheSize;
    }

    /**
     * Changes the maximum number of {@link Tile} objects that this cache holds.
     *
     * @param cacheSize
     *            new maximum number of tiles
     */
    public synchronized void setCacheSize(int cacheSize) {
        this.cacheSize = cacheSize;
        if (hash.size() > cacheSize)
            removeOldEntries();
    }

    /**
     * Linked list element holding the {@link Tile} and links to the
     * {@link #next} and {@link #prev} item in the list.
     */
    protected static class CacheEntry {
        private final Tile tile;
        private CacheEntry next;
        private CacheEntry prev;

        protected CacheEntry(Tile tile) {
            this.tile = tile;
        }

        @Override
        public String toString() {
            return tile.toString();
        }
    }

    /**
     * Special implementation of a double linked list for {@link CacheEntry}
     * elements. It supports element removal in constant time - in difference to
     * the Java implementation which needs O(n).
     *
     * @author Jan Peter Stotz
     */
    protected static class CacheLinkedListElement {
        protected CacheEntry firstElement;
        protected CacheEntry lastElement;
        protected int elementCount;

        /**
         * Constructs a new {@code CacheLinkedListElement}.
         */
        public CacheLinkedListElement() {
            clear();
        }

        public void clear() {
            elementCount = 0;
            firstElement = null;
            lastElement = null;
        }

        /**
         * Add the element to the head of the list.
         *
         * @param element new element to be added
         */
        public void addFirst(CacheEntry element) {
            if (element == null) return;
            if (elementCount == 0) {
                firstElement = element;
                lastElement = element;
                element.prev = null;
                element.next = null;
            } else {
                element.next = firstElement;
                firstElement.prev = element;
                element.prev = null;
                firstElement = element;
            }
            elementCount++;
        }

        /**
         * Removes the specified element from the list.
         *
         * @param element element to be removed
         */
        public void removeEntry(CacheEntry element) {
            if (element == null) return;
            if (element.next != null) {
                element.next.prev = element.prev;
            }
            if (element.prev != null) {
                element.prev.next = element.next;
            }
            if (element == firstElement)
                firstElement = element.next;
            if (element == lastElement)
                lastElement = element.prev;
            element.next = null;
            element.prev = null;
            elementCount--;
        }

        public void moveElementToFirstPos(CacheEntry entry) {
            if (firstElement == entry)
                return;
            removeEntry(entry);
            addFirst(entry);
        }

        public int getElementCount() {
            return elementCount;
        }

        public CacheEntry getLastElement() {
            return lastElement;
        }

        public CacheEntry getFirstElement() {
            return firstElement;
        }
    }
}
