/*
 * 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.openjpa.util.proxy;

import java.io.InputStream;
import java.io.ObjectStreamException;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.openjpa.util.MapChangeTracker;
import org.apache.openjpa.util.Proxies;

/**
 * Utility methods used by map proxies.
 *
 * @author Abe White
 */
public class ProxyMaps
    extends Proxies {

    /**
     * Call before invoking {@link Map#clear} on super.
     */
    public static void beforeClear(ProxyMap map) {
        dirty(map, true);
        Map.Entry entry;
        for (Object o : map.entrySet()) {
            entry = (Map.Entry) o;
            removed(map, entry.getKey(), true);
            removed(map, entry.getValue(), false);
        }
    }

    /**
     * Override for {@link Map#keySet}.
     */
    public static Set keySet(ProxyMap map) {
        ProxyEntrySet entries = (ProxyEntrySet) map.entrySet();
        entries.setView(ProxyEntrySet.VIEW_KEYS);
        return entries;
    }

    /**
     * Override for {@link Map#values}.
     */
    public static Collection values(ProxyMap map) {
        ProxyEntrySet entries = (ProxyEntrySet) map.entrySet();
        entries.setView(ProxyEntrySet.VIEW_VALUES);
        return entries;
    }

    /**
     * Wrap the given entry set in a proxy.
     */
    public static Set afterEntrySet(ProxyMap map, Set entries) {
        return new ProxyEntrySetImpl(map, entries);
    }

    /**
     * Call before invoking {@link Map#get} on super.
     */
    public static boolean beforeGet(ProxyMap map, Object key) {
        assertAllowedType(key, map.getKeyType());
        // Java 8 solution/workaround due to containsKey() calling get!=null, which could cause infinite loop
        return map.keySet().contains(key);
    }

    /**
     * Call after invoking {@link Map#get} on super.
     * The potential change is tracked when the get method is called. This change
     * will not translated to an update statement if the retrieved value
     * is not dirty.
     *
     * @param ret the return value from the super's method
     * @param before the return value from {@link #beforeGet}
     * @return the value to return from {@link Map#get}
     */
    public static Object afterGet(ProxyMap map, Object key,
        Object ret, boolean before) {
        if (before) {
            if (map.getChangeTracker() != null)
                ((MapChangeTracker) map.getChangeTracker()).changed(key, ret,
                    ret);
        }
        return ret;
    }


    /**
     * Call before invoking {@link Map#put} on super.
     */
    public static boolean beforePut(ProxyMap map, Object key, Object value) {
        assertAllowedType(key, map.getKeyType());
        assertAllowedType(value, map.getValueType());
        dirty(map, false);
        return map.containsKey(key);
    }

    /**
     * Call after invoking {@link Map#put} on super.
     *
     * @param ret the return value from the super's method
     * @param before the return value from {@link #beforePut}
     * @return the value to return from {@link Map#put}
     */
    public static Object afterPut(ProxyMap map, Object key, Object value,
        Object ret, boolean before) {
        if (before) {
            if (map.getChangeTracker() != null)
                ((MapChangeTracker) map.getChangeTracker()).changed(key, ret,
                    value);
            removed(map, ret, false);
        } else if (map.getChangeTracker() != null)
            ((MapChangeTracker) map.getChangeTracker()).added(key, value);
        return ret;
    }

    /**
     * Call before invoking {@link Properties#setProperty} on super.
     */
    public static boolean beforeSetProperty(ProxyMap map, String key,
        String value) {
        return beforePut(map, key, value);
    }

    /**
     * Call after invoking {@link Properties#setProperty} on super.
     *
     * @param ret the return value from the super's method
     * @param before the return value from {@link #beforeSetProperty}
     * @return the value to return from {@link Properties#setProperty}
     */
    public static Object afterSetProperty(ProxyMap map, String key,
        String value, Object ret, boolean before) {
        return afterPut(map, key, value, ret, before);
    }

    /**
     * Call before invoking {@link Properties#load} on super.
     */
    public static void beforeLoad(ProxyMap map, InputStream in) {
        dirty(map, true);
    }

    /**
     * Call before invoking {@link Properties#loadXML} on super.
     */
    public static void beforeLoadFromXML(ProxyMap map, InputStream in) {
        dirty(map, true);
    }

    /**
     * Overload for {@link Map#putAll}.
     */
    public static void putAll(ProxyMap map, Map values) {
        Map.Entry entry;
        for (Object o : values.entrySet()) {
            entry = (Map.Entry) o;
            map.put(entry.getKey(), entry.getValue());
        }
    }

    /**
     * Call before invoking {@link Map#remove} on super.
     */
    public static boolean beforeRemove(ProxyMap map, Object key) {
        dirty(map, false);
        return map.containsKey(key);
    }

    /**
     * Call after invoking {@link Map#remove} on super.
     *
     * @param ret the return value from the super's method
     * @param before the return value from {@link #beforeRemove}
     * @return the value to return from {@link Map#remove}
     */
    public static Object afterRemove(ProxyMap map, Object key, Object ret,
        boolean before) {
        if (before) {
            if (map.getChangeTracker() != null)
                ((MapChangeTracker) map.getChangeTracker()).removed(key, ret);
            removed(map, key, true);
            removed(map, ret, false);
        }
        return ret;
    }

    /**
     * See OPENJPA-2442.
     * Call before invoking {@link Map#remove(key, value) } on super.
     */
    public static boolean beforeRemove(ProxyMap map, Object key, Object value) {
        Proxies.dirty(map, false);
        return map.containsKey(key);
    }

    /**
     * See OPENJPA-2442.
     * Call after invoking {@link Map#remove(key, value) } on super.
     *
     * @param ret the return value from the super's method
     * @param before the return value from {@link #beforeRemove}
     * @return the value to return from {@link Map#remove}
     */
    public static boolean afterRemove(ProxyMap map, Object key, Object value, boolean ret,
                                      boolean before) {
        if (before) {
            if (map.getChangeTracker() != null) {
                ((MapChangeTracker) map.getChangeTracker()).removed(key, ret);
            }
            Proxies.removed(map, key, true);
            Proxies.removed(map, ret, false);
        }
        return ret;
    }

    /**
     * Marker interface for a proxy entry set.
     */
    public interface ProxyEntrySet
        extends Set {

        int VIEW_KEYS = 0;
        int VIEW_VALUES = 1;
        int VIEW_ENTRIES = 2;

        /**
         * Set what entry view this set exposes.
         */
        void setView(int view);
    }

    /**
     * Dirtying proxy for map entry set.
     */
    private static class ProxyEntrySetImpl
        extends AbstractSet
        implements ProxyEntrySet {

        private final ProxyMap _map;
        private final Set _entries;
        private int _view = VIEW_ENTRIES;

        /**
         * Supply owning map and delegate entry set on construction.
         */
        public ProxyEntrySetImpl(ProxyMap map, Set entries) {
            _map = map;
            _entries = entries;
        }

        /**
         * View mode.
         */
        public int getView() {
            return _view;
        }

        /**
         * View mode.
         */
        @Override
        public void setView(int view) {
            _view = view;
        }

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

        @Override
        public boolean remove(Object o) {
            if (_view != VIEW_KEYS)
                throw new UnsupportedOperationException();

            if (!_map.containsKey(o))
                return false;
            _map.remove(o);
            return true;
        }

        @Override
        public Iterator iterator() {
            final Iterator itr = _entries.iterator();
            return new Iterator() {
                private Map.Entry _last = null;

                @Override
                public boolean hasNext() {
                    return itr.hasNext();
                }

                @Override
                public Object next() {
                    _last = (Map.Entry) itr.next();
                    switch (_view) {
                        case VIEW_KEYS:
                            return _last.getKey();
                        case VIEW_VALUES:
                            return _last.getValue();
                        default:
                            return _last;
                    }
                }

                @Override
                public void remove() {
                    dirty(_map, false);
                    itr.remove();
                    if (_map.getChangeTracker() != null)
                        ((MapChangeTracker) _map.getChangeTracker()).
                            removed(_last.getKey(), _last.getValue());
                    Proxies.removed(_map, _last.getKey(), true);
                    Proxies.removed(_map, _last.getValue(), false);
                }
            };
        }

        protected Object writeReplace()
            throws ObjectStreamException {
            switch (_view) {
                case VIEW_KEYS:
                    return ((Map) _map).keySet();
                case VIEW_VALUES:
                    return ((Map) _map).values();
                default:
                    return ((Map) _map).entrySet();
            }
        }
    }
}
