/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2015 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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.wildfly.security.authz;

import java.util.AbstractCollection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.wildfly.common.Assert;

/**
 * A map-backed attributes collection.
 *
 * @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
 */
public class MapAttributes implements Attributes {
    // backing map

    private final Map<String, EntriesList> map;

    // cache fields

    private HashMap<String, Entry> entryCache;
    private Collection<Entry> entries;
    private Collection<String> values;

    /**
     * Construct an instance using a hash map for backing store.
     */
    public MapAttributes() {
        this.map = new HashMap<>();
    }

    /**
     * Construct a new instance copying mappings from an original map.
     *
     * @param original the original map
     */
    public MapAttributes(Map<String, ? extends Collection<String>> original) {
        Assert.checkNotNullParam("original", original);
        Map<String, EntriesList> map = new HashMap<>(original.size());
        for (Map.Entry<String, ? extends Collection<String>> entry : original.entrySet()) {
            map.put(entry.getKey(), new EntriesList(entry.getValue()));
        }
        this.map = map;
    }

    /**
     * Construct a new instance copying mappings from an original attributes collection.
     *
     * @param original the original collection
     */
    public MapAttributes(Attributes original) {
        Assert.checkNotNullParam("original", original);
        Map<String, EntriesList> map = new HashMap<>(original.size());
        for (Entry entry : original.entries()) {
            final EntriesList entriesList = new EntriesList(entry);
            if (! entriesList.isEmpty()) map.put(entry.getKey(), entriesList);
        }
        this.map = map;
    }

    public Set<String> keySet() {
        return map.keySet();
    }

    public Collection<String> values() {
        final Collection<String> values = this.values;
        if (values != null) {
            return values;
        }
        return this.values = Attributes.super.values();
    }

    public Collection<Entry> entries() {
        final Collection<Entry> entries = this.entries;
        if (entries != null) {
            return entries;
        }
        return this.entries = new AbstractCollection<Entry>() {
            public Iterator<Entry> iterator() {
                final Iterator<String> iterator = map.keySet().iterator();
                return new Iterator<Entry>() {

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

                    public Entry next() {
                        return get(iterator.next());
                    }

                    public void remove() {
                        iterator.remove();
                    }
                };
            }

            public int size() {
                return MapAttributes.this.size();
            }
        };
    }

    public int size(final String key) {
        final EntriesList list = map.get(key);
        return list == null ? 0 : list.size();
    }

    public boolean remove(final String key) {
        return map.remove(key) != null;
    }

    public void add(final String key, final int idx, final String value) {
        EntriesList list = map.get(key);
        if (list == null) {
            map.put(key, list = new EntriesList());
        }
        list.add(idx, value);
    }

    public String get(final String key, final int idx) {
        EntriesList list = map.get(key);
        return list == null ? null : list.get(idx);
    }

    public String set(final String key, final int idx, final String value) {
        EntriesList list = map.get(key);
        if (list == null) {
            throw new IndexOutOfBoundsException();
        }
        return list.set(idx, value);
    }

    public String remove(final String key, final int idx) {
        EntriesList list = map.get(key);
        if (list == null) {
            throw new IndexOutOfBoundsException();
        }
        final String result = list.remove(idx);
        if (list.isEmpty()) {
            map.remove(key);
        }
        return result;
    }

    public List<String> copyAndRemove(final String key) {
        final EntriesList old = map.remove(key);
        return old == null ? new ArrayList<>(0) : old;
    }

    public List<String> copyAndReplace(final String key, final Collection<String> values) {
        final EntriesList old = map.replace(key, new EntriesList(values));
        return old == null ? new ArrayList<>(0) : old;
    }

    public boolean containsKey(final String key) {
        return map.containsKey(key);
    }

    public boolean containsValue(final String key, final String value) {
        final EntriesList list = map.get(key);
        return list != null && list.contains(value);
    }

    public void removeRange(final String key, final int from, final int to) {
        final EntriesList list = map.get(key);
        if (list == null) {
            throw new IndexOutOfBoundsException();
        }
        list.removeRange(from, to);
    }

    public int indexOf(final String key, final String value) {
        EntriesList list = map.get(key);
        return list == null ? -1 : list.indexOf(value);
    }

    public int lastIndexOf(final String key, final String value) {
        EntriesList list = map.get(key);
        return list == null ? -1 : list.lastIndexOf(value);
    }

    public boolean set(final String key, final int idx, final String expect, final String update) {
        EntriesList list = map.get(key);
        if (list == null || !list.get(idx).equals(expect)) {
            return false;
        }
        list.set(idx, update);
        return true;
    }

    public String getFirst(final String key) {
        EntriesList list = map.get(key);
        return list == null ? null : list.get(0);
    }

    public String getLast(final String key) {
        EntriesList list = map.get(key);
        return list == null ? null : list.get(list.size() - 1);
    }

    public void addFirst(final String key, final String value) {
        EntriesList list = map.get(key);
        if (list == null) {
            map.put(key, list = new EntriesList());
        }
        list.add(0, value);
    }

    public void addLast(final String key, final String value) {
        EntriesList list = map.get(key);
        if (list == null) {
            map.put(key, list = new EntriesList());
        }
        list.add(value);
    }

    public boolean removeFirst(final String key, final String value) {
        EntriesList list = map.get(key);
        if (list == null) {
            return false;
        }
        int idx = list.indexOf(value);
        if (idx == -1) {
            return false;
        }
        list.remove(idx);
        if (list.isEmpty()) {
            map.remove(key);
        }
        return true;
    }

    public boolean removeLast(final String key, final String value) {
        EntriesList list = map.get(key);
        if (list == null) {
            return false;
        }
        int idx = list.lastIndexOf(value);
        if (idx == -1) {
            return false;
        }
        list.remove(idx);
        if (list.isEmpty()) {
            map.remove(key);
        }
        return true;
    }

    public String removeFirst(final String key) {
        EntriesList list = map.get(key);
        if (list == null) {
            return null;
        }
        final String removed = list.remove(0);
        if (list.isEmpty()) {
            map.remove(key);
        }
        return removed;
    }

    public String removeLast(final String key) {
        EntriesList list = map.get(key);
        if (list == null) {
            return null;
        }
        final String removed = list.remove(list.size() - 1);
        if (list.isEmpty()) {
            map.remove(key);
        }
        return removed;
    }

    public boolean remove(final String key, final int idx, final String value) {
        EntriesList list = map.get(key);
        if (list == null) {
            return false;
        }
        if (! list.get(idx).equals(value)) {
            return false;
        }
        list.remove(idx);
        if (list.isEmpty()) {
            map.remove(key);
        }
        return true;
    }

    public boolean removeAll(final String key, final String value) {
        EntriesList list = map.get(key);
        return list != null && list.removeAll(Collections.singleton(value));
    }

    public Entry get(final String key) {
        HashMap<String, Entry> entryCache = this.entryCache;
        if (entryCache == null) {
            entryCache = this.entryCache = new HashMap<>();
        }
        return entryCache.computeIfAbsent(key, s -> new SimpleAttributesEntry(this, s));
    }

    public int size() {
        return map.size();
    }

    public boolean isEmpty() {
        return map.isEmpty();
    }

    public void clear() {
        map.clear();
    }

    static final class EntriesList extends ArrayList<String> implements Set<String> {

        private static final long serialVersionUID = 8113580421577650775L;

        EntriesList(final int initialCapacity) {
            super(initialCapacity);
        }

        EntriesList() {
        }

        EntriesList(final Collection<? extends String> c) {
            super(c);
        }

        public void removeRange(final int fromIndex, final int toIndex) {
            super.removeRange(fromIndex, toIndex);
        }
    }
}
