/****************************************************************
 * 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.james.mime4j.util;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;

import org.apache.james.mime4j.message.Header;
import org.apache.james.mime4j.parser.ContentHandler;
import org.apache.james.mime4j.parser.Field;

/**
 * An object, which may be used to implement header, or parameter
 * maps. The maps keys are the header or parameter names. The
 * maps values are strings (single value), lists, or arrays.
 * <p>
 * Note that this class is not directly used anywhere in Mime4j.
 * Instead a user might choose to use it instead of {@link Header}
 * and {@link Field} in a custom {@link ContentHandler} implementation.
 * See also MIME4j-24.
 */
public class StringArrayMap implements Serializable {
    private static final long serialVersionUID = -5833051164281786907L;
    private final Map<String, Object> map = new HashMap<String, Object>();

    /**
     * <p>Converts the given object into a string. The object may be either of:
     * <ul>
     *   <li>a string, which is returned without conversion</li>
     *   <li>a list of strings, in which case the first element is returned</li>
     *   <li>an array of strings, in which case the first element is returned</li>
     * </ul>
     */
    public static String asString(Object pValue) {
        if (pValue == null) {
            return null;
        }
        if (pValue instanceof String) {
            return (String) pValue;
        }
        if (pValue instanceof String[]) {
            return ((String[]) pValue)[0];
        }
        if (pValue instanceof List) {
            return (String) ((List<?>) pValue).get(0);
        }
        throw new IllegalStateException("Invalid parameter class: " + pValue.getClass().getName());
    }

    /**
     * <p>Converts the given object into a string array. The object may be either of:
     * <ul>
     *   <li>a string, which is returned as an array with one element</li>
     *   <li>a list of strings, which is being converted into a string array</li>
     *   <li>an array of strings, which is returned without conversion</li>
     * </ul>
     */
    public static String[] asStringArray(Object pValue) {
        if (pValue == null) {
            return null;
        }
        if (pValue instanceof String) {
            return new String[]{(String) pValue};
        }
        if (pValue instanceof String[]) {
            return (String[]) pValue;
        }
        if (pValue instanceof List) {
            final List<?> l = (List<?>) pValue;
            return l.toArray(new String[l.size()]);
        }
        throw new IllegalStateException("Invalid parameter class: " + pValue.getClass().getName());
    }

    /**
     * <p>Converts the given object into a string enumeration. The object may be either of:
     * <ul>
     *   <li>a string, which is returned as an enumeration with one element</li>
     *   <li>a list of strings, which is being converted into a string enumeration</li>
     *   <li>an array of strings, which is being converted into a string enumeration</li>
     * </ul>
     */
    public static Enumeration<String> asStringEnum(final Object pValue) {
        if (pValue == null) {
            return null;
        }
        if (pValue instanceof String) {
            return new Enumeration<String>(){
                private Object value = pValue;
                public boolean hasMoreElements() {
                    return value != null;
                }
                public String nextElement() {
                    if (value == null) {
                        throw new NoSuchElementException();
                    }
                    final String s = (String) value;
                    value = null;
                    return s;
                }
            };
        }
        if (pValue instanceof String[]) {
            final String[] values = (String[]) pValue;
            return new Enumeration<String>() {
                private int offset;
                public boolean hasMoreElements() {
                    return offset < values.length;
                }
                public String nextElement() {
                    if (offset >= values.length) {
                        throw new NoSuchElementException();
                    }
                    return values[offset++];
                }
            };
        }
        if (pValue instanceof List) {
            @SuppressWarnings("unchecked")
            final List<String> stringList = (List<String>) pValue; 
            return Collections.enumeration(stringList);
        }
        throw new IllegalStateException("Invalid parameter class: " + pValue.getClass().getName());
    }

    /**
     * Converts the given map into a string array map: The map values
     * are string arrays.
     */
    public static Map<String, String[]> asMap(final Map<String, Object> pMap) {
        Map<String, String[]> result = new HashMap<String, String[]>(pMap.size());
        for (Map.Entry<String, Object> entry : pMap.entrySet()) {
            final String[] value = asStringArray(entry.getValue());
            result.put(entry.getKey(), value);
        }
        return Collections.unmodifiableMap(result);
    }

    /**
     * Adds a value to the given map.
     */
    protected void addMapValue(Map<String, Object> pMap, String pName, String pValue) {
        Object o = pMap.get(pName);
        if (o == null) {
            o = pValue;
        } else if (o instanceof String) {
            final List<Object> list = new ArrayList<Object>();
            list.add(o);
            list.add(pValue);
            o = list;
        } else if (o instanceof List) {
            @SuppressWarnings("unchecked")
            final List<String> stringList = (List<String>) o; 
            stringList.add(pValue);
        } else if (o instanceof String[]) {
            final List<String> list = new ArrayList<String>();
            final String[] arr = (String[]) o;
            for (String str : arr) {
                list.add(str);
            }
            list.add(pValue);
            o = list;
        } else {
            throw new IllegalStateException("Invalid object type: " + o.getClass().getName());
        }
        pMap.put(pName, o);
    }

    /**
     * Lower cases the given name.
     */
    protected String convertName(String pName) {
        return pName.toLowerCase();
    }

    /**
     * Returns the requested value.
     */
    public String getValue(String pName) {
        return asString(map.get(convertName(pName)));
    }

    /**
     * Returns the requested values as a string array.
     */
    public String[] getValues(String pName) {
        return asStringArray(map.get(convertName(pName)));
    }

    /**
     * Returns the requested values as an enumeration.
     */
    public Enumeration<String> getValueEnum(String pName) {
        return asStringEnum(map.get(convertName(pName)));
    }

    /**
     * Returns the set of registered names as an enumeration.
     * @see #getNameArray()
     */
    public Enumeration<String> getNames() {
        return Collections.enumeration(map.keySet());
    }

    /**
     * Returns an unmodifiable map of name/value pairs. The map keys
     * are the lower cased parameter/header names. The map values are
     * string arrays.
     */
    public Map<String, String[]> getMap() {
        return asMap(map);
    }

    /**
     * Adds a new name/value pair.
     */
    public void addValue(String pName, String pValue) {
        addMapValue(map, convertName(pName), pValue);
    }

    /**
     * Returns the set of registered names.
     * @see #getNames()
     */
    public String[] getNameArray() {
        final Collection<String> c = map.keySet();
        return c.toArray(new String[c.size()]);
    }
}
