//
//  ========================================================================
//  Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.util;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * A multi valued Map.
 *
 * @param <V> the entry type for multimap values
 */
@SuppressWarnings("serial")
public class MultiMap<V> extends LinkedHashMap<String, List<V>>
{
    public MultiMap()
    {
        super();
    }

    public MultiMap(Map<String, List<V>> map)
    {
        super(map);
    }

    public MultiMap(MultiMap<V> map)
    {
        super(map);
    }

    /**
     * Get multiple values.
     * Single valued entries are converted to singleton lists.
     *
     * @param name The entry key.
     * @return Unmodifieable List of values.
     */
    public List<V> getValues(String name)
    {
        List<V> vals = super.get(name);
        if ((vals == null) || vals.isEmpty())
        {
            return null;
        }
        return vals;
    }

    /**
     * Get a value from a multiple value.
     * If the value is not a multivalue, then index 0 retrieves the
     * value or null.
     *
     * @param name The entry key.
     * @param i Index of element to get.
     * @return Unmodifieable List of values.
     */
    public V getValue(String name, int i)
    {
        List<V> vals = getValues(name);
        if (vals == null)
        {
            return null;
        }
        if (i == 0 && vals.isEmpty())
        {
            return null;
        }
        return vals.get(i);
    }

    /**
     * Get value as String.
     * Single valued items are converted to a String with the toString()
     * Object method. Multi valued entries are converted to a comma separated
     * List.  No quoting of commas within values is performed.
     *
     * @param name The entry key.
     * @return String value.
     */
    public String getString(String name)
    {
        List<V> vals = get(name);
        if ((vals == null) || (vals.isEmpty()))
        {
            return null;
        }

        if (vals.size() == 1)
        {
            // simple form.
            return vals.get(0).toString();
        }

        // delimited form
        StringBuilder values = new StringBuilder(128);
        for (V e : vals)
        {
            if (e != null)
            {
                if (values.length() > 0)
                    values.append(',');
                values.append(e.toString());
            }
        }
        return values.toString();
    }

    /**
     * Put multi valued entry.
     *
     * @param name The entry key.
     * @param value The simple value
     * @return The previous value or null.
     */
    public List<V> put(String name, V value)
    {
        if (value == null)
        {
            return super.put(name, null);
        }
        List<V> vals = new ArrayList<>();
        vals.add(value);
        return put(name, vals);
    }

    /**
     * Shorthand version of putAll
     *
     * @param input the input map
     */
    public void putAllValues(Map<String, V> input)
    {
        for (Map.Entry<String, V> entry : input.entrySet())
        {
            put(entry.getKey(), entry.getValue());
        }
    }

    /**
     * Put multi valued entry.
     *
     * @param name The entry key.
     * @param values The List of multiple values.
     * @return The previous value or null.
     */
    public List<V> putValues(String name, List<V> values)
    {
        return super.put(name, values);
    }

    /**
     * Put multi valued entry.
     *
     * @param name The entry key.
     * @param values The array of multiple values.
     * @return The previous value or null.
     */
    @SafeVarargs
    public final List<V> putValues(String name, V... values)
    {
        List<V> list = new ArrayList<>();
        list.addAll(Arrays.asList(values));
        return super.put(name, list);
    }

    /**
     * Add value to multi valued entry.
     * If the entry is single valued, it is converted to the first
     * value of a multi valued entry.
     *
     * @param name The entry key.
     * @param value The entry value.
     */
    public void add(String name, V value)
    {
        List<V> lo = get(name);
        if (lo == null)
        {
            lo = new ArrayList<>();
        }
        lo.add(value);
        super.put(name, lo);
    }

    /**
     * Add values to multi valued entry.
     * If the entry is single valued, it is converted to the first
     * value of a multi valued entry.
     *
     * @param name The entry key.
     * @param values The List of multiple values.
     */
    public void addValues(String name, List<V> values)
    {
        List<V> lo = get(name);
        if (lo == null)
        {
            lo = new ArrayList<>();
        }
        lo.addAll(values);
        put(name, lo);
    }

    /**
     * Add values to multi valued entry.
     * If the entry is single valued, it is converted to the first
     * value of a multi valued entry.
     *
     * @param name The entry key.
     * @param values The String array of multiple values.
     */
    public void addValues(String name, V[] values)
    {
        List<V> lo = get(name);
        if (lo == null)
        {
            lo = new ArrayList<>();
        }
        lo.addAll(Arrays.asList(values));
        put(name, lo);
    }

    /**
     * Merge values.
     *
     * @param map the map to overlay on top of this one, merging together values if needed.
     * @return true if an existing key was merged with potentially new values, false if either no change was made, or there were only new keys.
     */
    public boolean addAllValues(MultiMap<V> map)
    {
        boolean merged = false;

        if ((map == null) || (map.isEmpty()))
        {
            // done
            return merged;
        }

        for (Map.Entry<String, List<V>> entry : map.entrySet())
        {
            String name = entry.getKey();
            List<V> values = entry.getValue();

            if (this.containsKey(name))
            {
                merged = true;
            }

            this.addValues(name, values);
        }

        return merged;
    }

    /**
     * Remove value.
     *
     * @param name The entry key.
     * @param value The entry value.
     * @return true if it was removed.
     */
    public boolean removeValue(String name, V value)
    {
        List<V> lo = get(name);
        if ((lo == null) || (lo.isEmpty()))
        {
            return false;
        }
        boolean ret = lo.remove(value);
        if (lo.isEmpty())
        {
            remove(name);
        }
        else
        {
            put(name, lo);
        }
        return ret;
    }

    /**
     * Test for a specific single value in the map.
     * <p>
     * NOTE: This is a SLOW operation, and is actively discouraged.
     *
     * @param value the value to search for
     * @return true if contains simple value
     */
    public boolean containsSimpleValue(V value)
    {
        for (List<V> vals : values())
        {
            if ((vals.size() == 1) && vals.contains(value))
            {
                return true;
            }
        }
        return false;
    }

    @Override
    public String toString()
    {
        Iterator<Map.Entry<String, List<V>>> iter = entrySet().iterator();
        StringBuilder sb = new StringBuilder();
        sb.append('{');
        boolean delim = false;
        while (iter.hasNext())
        {
            Map.Entry<String, List<V>> e = iter.next();
            if (delim)
            {
                sb.append(", ");
            }
            String key = e.getKey();
            List<V> vals = e.getValue();
            sb.append(key);
            sb.append('=');
            if (vals.size() == 1)
            {
                sb.append(vals.get(0));
            }
            else
            {
                sb.append(vals);
            }
            delim = true;
        }
        sb.append('}');
        return sb.toString();
    }

    /**
     * @return Map of String arrays
     */
    public Map<String, String[]> toStringArrayMap()
    {
        Map<String, String[]> map = new LinkedHashMap<String, String[]>(size() * 3 / 2)
        {
            @Override
            public String toString()
            {
                StringBuilder b = new StringBuilder();
                b.append('{');
                for (String k : super.keySet())
                {
                    if (b.length() > 1)
                        b.append(',');
                    b.append(k);
                    b.append('=');
                    b.append(Arrays.asList(super.get(k)));
                }

                b.append('}');
                return b.toString();
            }
        };

        for (Map.Entry<String, List<V>> entry : entrySet())
        {
            String[] a = null;
            if (entry.getValue() != null)
            {
                a = new String[entry.getValue().size()];
                a = entry.getValue().toArray(a);
            }
            map.put(entry.getKey(), a);
        }
        return map;
    }
}
