/*
 * Copyright 2010 Ning, Inc.
 *
 * Ning 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 com.ning.http.client;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * An implementation of a {@code String -> List<String>} map that adds a fluent interface, i.e. methods that
 * return this instance.
 */
public class FluentStringsMap implements Map<String, List<String>>, Iterable<Map.Entry<String, List<String>>> {
    private final Map<String, List<String>> values = new LinkedHashMap<String, List<String>>();

    public FluentStringsMap() {
    }

    public FluentStringsMap(FluentStringsMap src) {
        if (src != null) {
            for (Map.Entry<String, List<String>> header : src) {
                add(header.getKey(), header.getValue());
            }
        }
    }

    public FluentStringsMap(Map<String, Collection<String>> src) {
        if (src != null) {
            for (Map.Entry<String, Collection<String>> header : src.entrySet()) {
                add(header.getKey(), header.getValue());
            }
        }
    }

    /**
     * Adds the specified values and returns this object.
     *
     * @param key    The key
     * @param values The value(s); if null then this method has no effect. Use the empty string to
     *               generate an empty value
     * @return This object
     */
    public FluentStringsMap add(String key, String... values) {
        if ((values != null) && (values.length > 0)) {
            add(key, Arrays.asList(values));
        }
        return this;
    }

    private List<String> fetchValues( Collection<String> values ) {
        List<String> result = null;

        if (values != null) {
            for (String value : values) {
                if (value == null) {
                    value = "";
                }
                if (result == null) {
                    // lazy initialization
                    result = new ArrayList<String>();
                }
                result.add(value);
            }
        }
        return result;
    }

    /**
     * Adds the specified values and returns this object.
     *
     * @param key   The key
     * @param values The value(s); if null then this method has no effect. Use an empty collection
     *               to generate an empty value
     * @return This object
     */
    public FluentStringsMap add(String key, Collection<String> values) {
        if (key != null) {
            List<String> nonNullValues = fetchValues( values );
    
            if (nonNullValues != null) {
                List<String> curValues = this.values.get(key);
        
                if (curValues == null) {
                    curValues = new ArrayList<String>();
                    this.values.put(key, curValues);
                }
                curValues.addAll(nonNullValues);
            }
        }
        return this;
    }

    /**
     * Adds all key-values pairs from the given object to this object and returns this object.
     *
     * @param src The source object
     * @return This object
     */
    public FluentStringsMap addAll(FluentStringsMap src) {
        if (src != null) {
            for (Map.Entry<String, List<String>> header : src) {
                add(header.getKey(), header.getValue());
            }
        }
        return this;
    }

    /**
     * Adds all key-values pairs from the given map to this object and returns this object.
     *
     * @param src The source map
     * @return This object
     */
    public FluentStringsMap addAll(Map<String, Collection<String>> src) {
        if (src != null) {
            for (Map.Entry<String, Collection<String>> header : src.entrySet()) {
                add(header.getKey(), header.getValue());
            }
        }
        return this;
    }

    /**
     * Replaces the values for the given key with the given values.
     *
     * @param key    The key
     * @param values The new values
     * @return This object
     */
    public FluentStringsMap replace(final String key, final String... values) {
        return replace(key, Arrays.asList(values));
    }

    /**
     * Replaces the values for the given key with the given values.
     *
     * @param key  The key
     * @param values The new values
     * @return This object
     */
    public FluentStringsMap replace(final String key, final Collection<String> values) {
        if (key != null) {
            List<String> nonNullValues = fetchValues( values );
    
            if (nonNullValues == null) {
                this.values.remove(key);
            }
            else {
                this.values.put(key, nonNullValues);
            }
        }
        return this;
    }

    /**
     * Replace the values for all keys from the given map that are also present in this object, with the values from the given map.
     * All key-values from the given object that are not present in this object, will be added to it.
     *
     * @param src The source object
     * @return This object
     */
    public FluentStringsMap replaceAll(FluentStringsMap src) {
        if (src != null) {
            for (Map.Entry<String, List<String>> header : src) {
                replace(header.getKey(), header.getValue());
            }
        }
        return this;
    }

    /**
     * Replace the values for all keys from the given map that are also present in this object, with the values from the given map.
     * All key-values from the given object that are not present in this object, will be added to it.
     *
     * @param src The source map
     * @return This object
     */
    public FluentStringsMap replaceAll(Map<? extends String, ? extends Collection<String>> src) {
        if (src != null) {
            for (Map.Entry<? extends String, ? extends Collection<String>> header : src.entrySet()) {
                replace(header.getKey(), header.getValue());
            }
        }
        return this;
    }

    /**
     * {@inheritDoc}
     */
    /* @Override */
    public List<String> put(String key, List<String> value)
    {
        if (key == null) {
            throw new NullPointerException("Null keys are not allowed");
        }

        List<String> oldValue = get(key);

        replace(key, value);
        return oldValue;
    }

    /**
     * {@inheritDoc}
     */
    /* @Override */
    public void putAll(Map<? extends String, ? extends List<String>> values)
    {
        replaceAll(values);
    }

    /**
     * Removes the values for the given key if present and returns this object.
     *
     * @param key The key
     * @return This object
     */
    public FluentStringsMap delete(String key) {
        values.remove(key);
        return this;
    }

    /**
     * Removes the values for the given keys if present and returns this object.
     *
     * @param keys The keys
     * @return This object
     */
    public FluentStringsMap deleteAll(String... keys) {
        if (keys != null) {
            for (String key : keys) {
                remove(key);
            }
        }
        return this;
    }

    /**
     * Removes the values for the given keys if present and returns this object.
     *
     * @param keys The keys
     * @return This object
     */
    public FluentStringsMap deleteAll(Collection<String> keys) {
        if (keys != null) {
            for (String key : keys) {
                remove(key);
            }
        }
        return this;
    }

    /**
     * {@inheritDoc}
     */
    /* @Override */
    public List<String> remove(Object key)
    {
        if (key == null) {
            return null;
        }
        else {
            List<String> oldValues = get(key.toString());

            delete(key.toString());
            return oldValues;
        }
    }

    /**
     * {@inheritDoc}
     */
    /* @Override */
    public void clear()
    {
        values.clear();
    }

    /**
     * {@inheritDoc}
     */
    /* @Override */
    public Iterator<Map.Entry<String, List<String>>> iterator() {
        return Collections.unmodifiableSet(values.entrySet()).iterator();
    }

    /**
     * {@inheritDoc}
     */
    /* @Override */
    public Set<String> keySet()
    {
        return Collections.unmodifiableSet(values.keySet());
    }

    /**
     * {@inheritDoc}
     */
    /* @Override */
    public Set<Entry<String, List<String>>> entrySet()
    {
        return values.entrySet();
    }

    /**
     * {@inheritDoc}
     */
    /* @Override */
    public int size()
    {
        return values.size();
    }

    /**
     * {@inheritDoc}
     */
    /* @Override */
    public boolean isEmpty()
    {
        return values.isEmpty();
    }

    /**
     * {@inheritDoc}
     */
    /* @Override */
    public boolean containsKey(Object key)
    {
        return key == null ? false : values.containsKey(key.toString());
    }

    /**
     * {@inheritDoc}
     */
    /* @Override */
    public boolean containsValue(Object value)
    {
        return values.containsValue(value);
    }

    /**
     * Returns the value for the given key. If there are multiple values for this key,
     * then only the first one will be returned.
     *
     * @param key The key
     * @return The first value
     */
    public String getFirstValue(String key) {
        List<String> values = get(key);

        if (values == null) {
            return null;
        }
        else if (values.isEmpty()) {
            return "";
        }
        else {
            return values.get(0);
        }
    }

    /**
     * Returns the values for the given key joined into a single string using the given delimiter.
     *
     * @param key The key
     * @return The value as a single string
     */
    public String getJoinedValue(String key, String delimiter) {
        List<String> values = get(key);
        
        if (values == null) {
            return null;
        }
        else if (values.size() == 1) {
            return values.get(0);
        }
        else {
            StringBuilder result = new StringBuilder();

            for (String value : values) {
                if (result.length() > 0) {
                    result.append(delimiter);
                }
                result.append(value);
            }
            return result.toString();
        }
    }

    /**
     * {@inheritDoc}
     */
    /* @Override */
    public List<String> get(Object key)
    {
        if (key == null) {
            return null;
        }

        return values.get(key.toString());
    }

    /**
     * {@inheritDoc}
     */
    /* @Override */
    public Collection<List<String>> values()
    {
        return values.values();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }

        final FluentStringsMap other = (FluentStringsMap) obj;

        if (values == null) {
            if (other.values != null) {
                return false;
            }
        }
        else if (!values.equals(other.values)) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        return values == null ? 0 : values.hashCode();
    }

    @Override
    public String toString()
    {
        StringBuilder result = new StringBuilder();

        for (Map.Entry<String, List<String>> entry : values.entrySet()) {
            if (result.length() > 0) {
                result.append("; ");
            }
            result.append("\"");
            result.append(entry.getKey());
            result.append("=");

            boolean needsComma = false;

            for (String value : entry.getValue()) {
                if (needsComma) {
                    result.append(", ");
                }
                else {
                    needsComma = true;
                }
                result.append(value);
            }
            result.append("\"");
        }
        return result.toString();
    }
}
