/*
 * Copyright 2008-2009 LinkedIn, Inc
 * Copyright 2013 Big Switch Networks, Inc.
 *
 * 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.
 */

package org.sdnplatform.sync.internal.store;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.sdnplatform.sync.IClosableIterator;
import org.sdnplatform.sync.IVersion;
import org.sdnplatform.sync.Versioned;
import org.sdnplatform.sync.IVersion.Occurred;
import org.sdnplatform.sync.error.ObsoleteVersionException;
import org.sdnplatform.sync.error.SyncException;
import org.sdnplatform.sync.internal.util.Pair;


/**
 * A simple non-persistent, in-memory store.
 */
public class InMemoryStorageEngine<K, V> implements IStorageEngine<K, V> {

    private final ConcurrentMap<K, List<Versioned<V>>> map;
    private final String name;
    
    /**
     * Interval in milliseconds before tombstones will be cleared.
     */
    protected int tombstoneDeletion = 24 * 60 * 60 * 1000;

    public InMemoryStorageEngine(String name) {
        this.name = name;
        this.map = new ConcurrentHashMap<K, List<Versioned<V>>>();
    }

    public InMemoryStorageEngine(String name, 
                                 ConcurrentMap<K, List<Versioned<V>>> map) {
        this.name = name;
        this.map = map;
    }

    // ******************
    // StorageEngine<K,V>
    // ******************

    @Override
    public void close() {}

    @Override
    public List<IVersion> getVersions(K key) throws SyncException {
        return StoreUtils.getVersions(get(key));
    }

    @Override
    public List<Versioned<V>> get(K key) throws SyncException {
        StoreUtils.assertValidKey(key);
        List<Versioned<V>> results = map.get(key);
        if(results == null) {
            return new ArrayList<Versioned<V>>(0);
        }
        synchronized(results) {
            return new ArrayList<Versioned<V>>(results);
        }
    }

    @Override
    public void put(K key, Versioned<V> value) throws SyncException {
        if (!doput(key, value))
            throw new ObsoleteVersionException();
    }

    public boolean doput(K key, Versioned<V> value) throws SyncException {
        StoreUtils.assertValidKey(key);

        IVersion version = value.getVersion();

        while(true) {
            List<Versioned<V>> items = map.get(key);
            // If we have no value, optimistically try to add one
            if(items == null) {
                items = new ArrayList<Versioned<V>>();
                items.add(new Versioned<V>(value.getValue(), version));
                if (map.putIfAbsent(key, items) != null)
                    continue;
                return true;
            } else {
                synchronized(items) {
                    // if this check fails, items has been removed from the map
                    // by delete, so we try again.
                    if(map.get(key) != items)
                        continue;

                    // Check for existing versions - remember which items to
                    // remove in case of success
                    List<Versioned<V>> itemsToRemove = new ArrayList<Versioned<V>>(items.size());
                    for(Versioned<V> versioned: items) {
                        Occurred occurred = value.getVersion().compare(versioned.getVersion());
                        if(occurred == Occurred.BEFORE) {
                            return false;
                        } else if(occurred == Occurred.AFTER) {
                            itemsToRemove.add(versioned);
                        }
                    }
                    items.removeAll(itemsToRemove);
                    items.add(value);
                }
                return true;
            }
        }
    }

    @Override
    public IClosableIterator<Entry<K,List<Versioned<V>>>> entries() {
        return new InMemoryIterator<K, V>(map);
    }

    @Override
    public IClosableIterator<K> keys() {
        // TODO Implement more efficient version.
        return StoreUtils.keys(entries());
    }

    @Override
    public void truncate() {
        map.clear();
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public boolean writeSyncValue(K key, Iterable<Versioned<V>> values) {
        boolean success = false;
        for (Versioned<V> value : values) {
            try {
                put (key, value);
                success = true;
            } catch (SyncException e) {
                // ignore
            }
        }
        return success;
    }

    @Override
    public void cleanupTask() {
        // Remove tombstones that are older than the tombstone deletion
        // threshold.  If a value is deleted and the tombstone has been 
        // cleaned up before the cluster is fully synchronized, then there
        // is a chance that deleted values could be resurrected
        Iterator<Entry<K, List<Versioned<V>>>> iter = map.entrySet().iterator();
        while (iter.hasNext()) {
            Entry<K, List<Versioned<V>>> e = iter.next();
            List<Versioned<V>> items = e.getValue();

            synchronized (items) {
                if (StoreUtils.canDelete(items, tombstoneDeletion))
                    iter.remove();
            }
        }
    }

    @Override
    public boolean isPersistent() {
        return false;
    }

    @Override
    public void setTombstoneInterval(int interval) {
        this.tombstoneDeletion = interval;
    }

    // *********************
    // InMemoryStorageEngine
    // *********************

    /**
     * Get the number of keys currently in the store
     * @return
     */
    public int size() {
        return map.size();
    }
    
    /**
     * Atomically remove the key and return the value that was mapped to it,
     * if any
     * @param key the key to remove
     * @return the mapped values
     */
    public List<Versioned<V>> remove(K key) {
        while (true) {
            List<Versioned<V>> items = map.get(key);
            synchronized (items) {
                if (map.remove(key, items))
                    return items;                
            }
        }
    }

    /**
     * Check whether the given key is present in the store
     * @param key the key
     * @return <code>true</code> if the key is present
     */
    public boolean containsKey(K key) {
        return map.containsKey(key);
    }
    
    // ******
    // Object
    // ******

    @Override
    public String toString() {
        return toString(15);
    }

    // *************
    // Local methods
    // *************

    protected String toString(int size) {
        StringBuilder builder = new StringBuilder();
        builder.append("{");
        int count = 0;
        for(Entry<K, List<Versioned<V>>> entry: map.entrySet()) {
            if(count > size) {
                builder.append("...");
                break;
            }
            builder.append(entry.getKey());
            builder.append(':');
            builder.append(entry.getValue());
            builder.append(',');
        }
        builder.append('}');
        return builder.toString();
    }

    private static class InMemoryIterator<K, V> implements 
        IClosableIterator<Entry<K, List<Versioned<V>>>> {

        private final Iterator<Entry<K, List<Versioned<V>>>> iterator;

        public InMemoryIterator(ConcurrentMap<K, List<Versioned<V>>> map) {
            this.iterator = map.entrySet().iterator();
        }

        public boolean hasNext() {
            return iterator.hasNext();
        }

        public Pair<K, List<Versioned<V>>> next() {
            Entry<K, List<Versioned<V>>> entry = iterator.next();
            return new Pair<K, List<Versioned<V>>>(entry.getKey(), 
                    entry.getValue());
        }

        public void remove() {
            throw new UnsupportedOperationException("No removal y'all.");
        }

        @Override
        public void close() {
            // nothing to do
        }
    }
}