/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.brooklyn.core.internal.storage.impl;

import static com.google.common.base.Preconditions.checkNotNull;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;

import javax.annotation.Nullable;

/**
 * A decorator for a ConcurrentMap that allows null values to be used.
 * 
 * However, {@link #values()} and {@link #entrySet()} return immutable snapshots
 * of the map's contents. This may be revisited in a future version.
 * 
 * @author aled
 */
public class ConcurrentMapAcceptingNullVals<K, V> implements ConcurrentMap<K, V> {

    private static enum Marker {
        NULL;
    }
    
    private final ConcurrentMap<K, V> delegate;

    public ConcurrentMapAcceptingNullVals(ConcurrentMap<K,V> delegate) {
        this.delegate = checkNotNull(delegate, "delegate");
    }
    
    @Override
    public void clear() {
        delegate.clear();
    }

    @Override
    public boolean containsKey(Object key) {
        return delegate.containsKey(key);
    }

    @Override
    public boolean containsValue(Object value) {
        return delegate.containsValue(toNonNullValue(value));
    }

    @Override
    public Set<Map.Entry<K, V>> entrySet() {
        // Note that returns an immutable snapshot
        Set<Map.Entry<K, V>> result = new LinkedHashSet<Map.Entry<K, V>>(delegate.size());
        for (Map.Entry<K, V> entry : delegate.entrySet()) {
            result.add(new AbstractMap.SimpleEntry<K,V>(entry.getKey(), (V)fromNonNullValue(entry.getValue())));
        }
        return Collections.unmodifiableSet(result);
    }

    @Override
    public Collection<V> values() {
        // Note that returns an immutable snapshot
        List<V> result = new ArrayList<V>(delegate.size());
        for (V v : delegate.values()) {
            result.add((V)fromNonNullValue(v));
        }
        return Collections.unmodifiableCollection(result);
    }

    @Override
    public Set<K> keySet() {
        return delegate.keySet();
    }

    @Override
    public V get(Object key) {
        return (V) fromNonNullValue(delegate.get(key));
    }

    @Override
    public boolean isEmpty() {
        return delegate.isEmpty();
    }

    @Override
    public V put(K key, V value) {
        return (V) fromNonNullValue(delegate.put(key, (V) toNonNullValue(value)));
    }

    @Override
    public void putAll(Map<? extends K, ? extends V> vals) {
        for (Map.Entry<? extends K, ? extends V> entry : vals.entrySet()) {
            put(entry.getKey(), entry.getValue());
        }
    }

    @Override
    public V remove(Object key) {
        return (V) fromNonNullValue(delegate.remove(key));
    }

    @Override
    public int size() {
        return delegate.size();
    }

    @Override
    public V putIfAbsent(K key, V value) {
        return (V) fromNonNullValue(delegate.putIfAbsent(key, (V) toNonNullValue(value)));
    }

    @Override
    public boolean remove(Object key, Object value) {
        return delegate.remove(key, toNonNullValue(value));
    }

    @Override
    public V replace(K key, V value) {
        return (V) fromNonNullValue(delegate.replace(key, (V) toNonNullValue(value)));
    }

    @Override
    public boolean replace(K key, V oldValue, V newValue) {
        return delegate.replace(key, (V) toNonNullValue(oldValue), (V) toNonNullValue(newValue));
    }
    
    private static class SetWithNullVals<T> implements Set<T> {

        private final Set<T> delegate;
        
        public SetWithNullVals(Set<T> delegate) {
            this.delegate = delegate;
        }
        
        @Override
        public boolean add(T e) {
            return delegate.add(e); // unsupported; let delegate give exception
        }

        @Override
        public boolean addAll(Collection<? extends T> c) {
            return delegate.addAll(c); // unsupported; let delegate give exception
        }

        @Override
        public void clear() {
            delegate.clear();
        }

        @Override
        public boolean contains(Object o) {
            return delegate.contains(toNonNullValue(o));
        }

        @Override
        public boolean containsAll(Collection<?> c) {
            for (Object e : c) {
                if (!delegate.contains(toNonNullValue(e))) return false;
            }
            return true;
        }

        @Override
        public boolean isEmpty() {
            return delegate.isEmpty();
        }

        @Override
        public Iterator<T> iterator() {
            // TODO Auto-generated method stub
            return null;
        }

        @Override
        public boolean remove(Object o) {
            return delegate.remove(toNonNullValue(o));
        }

        @Override
        public boolean removeAll(Collection<?> c) {
            boolean result = false;
            for (Object e : c) {
                result = result & delegate.remove(toNonNullValue(e));
            }
            return result;
        }

        @Override
        public boolean retainAll(Collection<?> c) {
            boolean result = false;
            for (Iterator<T> iter = delegate.iterator(); iter.hasNext();) {
                T e = iter.next();
                if (!c.contains(fromNonNullValue(e))) {
                    iter.remove();
                    result = true;
                }
            }
            return result;
        }

        @Override
        public int size() {
            return delegate.size();
        }

        @Override
        public Object[] toArray() {
            // TODO Auto-generated method stub
            return null;
        }

        @Override
        public <T> T[] toArray(T[] a) {
            // TODO Auto-generated method stub
            return null;
        }

    }
    
    private static Object toNonNullValue(Object value) {
        return (value != null) ? value : Marker.NULL;
    }

    private static Object fromNonNullValue(Object value) {
        return (value == Marker.NULL) ? null : value; 
    }
    
    @Override
    public boolean equals(@Nullable Object object) {
        // copied from guava's non-public method Maps.equalsImpl
        if (this == object) {
            return true;
        }
        if (object instanceof Map) {
            Map<?, ?> o = (Map<?, ?>) object;
            return entrySet().equals(o.entrySet());
        }
        return false;
    }

    @Override
    public int hashCode() {
        // copied from guava's ImmutableMap.hashCode
        return entrySet().hashCode();
    }

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