/*
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License).  You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the license at
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Copyright (c) Ericsson AB, 2004-2007. All rights reserved.
 */
package com.ericsson.ssa.sip;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.sip.ServletParseException;


/**
 * A map for parameter list with name/value pairs. Minimizing the memory
 * footprint compared to normal HashMap. An arbitrary separator could be
 * inserted between the name/value pairs.
 * <p>
 * E.g ;key1=value1;key2;key3=value3 or &key1=value1&key2&key3=value3
 * <p>
 * The capacity of this map may differ from the size. The capacity is allocated
 * with an extra BUFFER_EXTEND_SIZE and aligned in chunks of
 * BUFFER_ALIGNMENT_SIZE. The capacity is never decreased.
 * <p>
 * Internally the parameters are stored directly as being received from network
 * since it is assumed to be the most compact format. When a parameter is
 * accessed via SSA interface a conversion takes place and vice versa.
 *
 * @author ehsroha
 * @since 2006-jan-30
 */
public class ParameterByteMap implements Serializable {
    private static SipURIDecoder decoder = new SipURIDecoder();
    private static SipURIEncoder encoder = new SipURIEncoder();
    public static int BUFFER_ALIGNMENT_SIZE = 8;
    public static int BUFFER_EXTEND_SIZE = 2 * BUFFER_ALIGNMENT_SIZE;
    private byte[] buffer = null;
    private int size = 0;
    private byte separator = ';';

    private ParameterByteMap() {
    }

    /**
     * Default internal byte array size is set to zero.
     *
     * @param separator
     */
    public ParameterByteMap(char separator) {
        this.separator = (byte) separator;
    }

    /**
     * Copies the array starting from offset until length into an internal byte
     * array with internal size length. If the array is null the internal byte
     * array size is set to zero.
     */
    public ParameterByteMap(byte[] array, int offset, int length, char separator)
        throws ServletParseException {
        this(separator);

        if (array != null) {
            trim(array, offset, length);

            if (separator == '&') {
                // special handling for uri-headers,
                // replace ? with &
                array[offset] = '&';
            }

            buffer = increaseBufferIfNeeded(array, offset, size, 0, true);
        }
    }

    /**
     * Copies the array into an internal byte array with internal size set to
     * length of array. If the array is null the internal byte array size is set
     * to zero.
     */
    public ParameterByteMap(byte[] array, char separator)
        throws ServletParseException {
        this(array, 0, (array == null) ? 0 : array.length, separator);
    }

    /**
     * Remove whitespace except inside quoted string Adjusts the size member.
     *
     * @param array
     * @param offset
     * @param separator
     * @return trimmed byte array
     */
    private void trim(byte[] array, int offset, int length)
        throws ServletParseException {
        length += offset;

        if (length > array.length) {
            throw new ServletParseException("Illegal length");
        }

        byte whitespace = ' ';
        byte quote = '"';
        int slowPos = offset;
        int fastPos = offset;

        while (fastPos < length) {
            // eat whitespace except inside quoted string
            while ((fastPos < (length - 1)) && (array[fastPos] == whitespace)) {
                fastPos++;
            }

            // skip quoted string...
            if (array[fastPos] == quote) {
                if (slowPos < fastPos) {
                    array[slowPos] = array[fastPos];
                }

                slowPos++;
                fastPos++;

                while ((fastPos < (length - 1)) && (array[fastPos] != quote)) {
                    if (slowPos < fastPos) {
                        array[slowPos] = array[fastPos];
                    }

                    slowPos++;
                    fastPos++;
                }
            }

            // adjust for eaten spaces...
            if (slowPos < fastPos) {
                array[slowPos] = array[fastPos];
            }

            slowPos++;
            fastPos++;
        }

        size = slowPos - offset;
    }

    private int size() {
        return size;
    }

    private static int adjustedLength(int length, boolean initial) {
        // add an extra capacity...
        if (!initial) {
            length += BUFFER_EXTEND_SIZE;
        }

        // should be even chunks of BUFFER_ALIGNMENT_SIZE
        int rest = length % BUFFER_ALIGNMENT_SIZE;

        return (rest == 0) ? length : ((length + BUFFER_ALIGNMENT_SIZE) - rest);
    }

    private static byte[] increaseBufferIfNeeded(byte[] src, int size,
        int requestedExtraBytes) {
        return increaseBufferIfNeeded(src, 0, size, requestedExtraBytes, false);
    }

    private static byte[] increaseBufferIfNeeded(byte[] src, int offset,
        int size, int requestedExtraBytes, boolean forceCreation) {
        if (((src != null) && (forceCreation == false)) &&
                ((size + requestedExtraBytes) <= src.length)) {
            return src;
        } else {
            int capacity = adjustedLength(size + requestedExtraBytes, true);
            byte[] newBuffer = new byte[capacity];

            if (src != null) {
                System.arraycopy(src, offset, newBuffer, 0, size);
            }

            return newBuffer;
        }
    }

    private static byte[] escape(String str) {
        return encoder.encodeParameter(str).getBytes();
    }

    private static String unescape(String str) {
        try {
            return decoder.decode(str);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private String getValue(byte[] key) throws UnsupportedEncodingException {
        int pos = indexOf(key);

        while ((pos != -1) && (buffer[pos - 1] != separator)) {
            // what if key is same as data inside value:
            // e.g. key=hej and buffer: ;key1=hej;hej=value2
            pos = indexOf(key, pos + 1);
        }

        if (pos >= 0) {
            if ((pos + key.length) == size()) {
                // key exist without value at end of buffer...
                return "";
            } else if (buffer[pos + key.length] == separator) {
                // key exist without value inside buffer...
                return "";
            }

            while ((pos < size()) && (buffer[pos] != separator)) {
                pos++;

                if (buffer[pos] == '=') {
                    pos++;

                    int mark = pos;

                    while (pos < size()) {
                        if (buffer[pos] == separator) {
                            break;
                        }

                        pos++;
                    }

                    return new String(buffer, mark, pos - mark,
                        SipFactoryImpl.SIP_CHARSET);
                }
            }
        }

        // key is not found...
        return null;
    }

    private int indexOf(byte[] token) {
        return indexOf(token, 0);
    }

    private int indexOf(byte[] token, int fromIndex) {
        return indexOf(buffer, 0, size(), token, 0, token.length, fromIndex);
    }

    private static int indexOf(byte[] source, int sourceOffset,
        int sourceCount, byte[] target, int targetOffset, int targetCount,
        int fromIndex) {
        if (fromIndex >= sourceCount) {
            return ((targetCount == 0) ? sourceCount : (-1));
        }

        if (fromIndex < 0) {
            fromIndex = 0;
        }

        if (targetCount == 0) {
            return fromIndex;
        }

        byte first = target[targetOffset];
        int max = sourceOffset + (sourceCount - targetCount);

        for (int i = sourceOffset + fromIndex; i <= max; i++) {
            /* Look for first character. */
            if (source[i] != first) {
                while ((++i <= max) && (source[i] != first))
                    ;
            }

            /* Found first character, now look at the rest of v2 */
            if (i <= max) {
                int j = i + 1;
                int end = (j + targetCount) - 1;

                for (int k = targetOffset + 1;
                        (j < end) && (source[j] == target[k]); j++, k++)
                    ;

                if (j == end) {
                    /* Found whole string. */
                    return i - sourceOffset;
                }
            }
        }

        return -1;
    }

    private static int append(byte[] src, byte[] dest, int destPos) {
        System.arraycopy(src, 0, dest, destPos, src.length);

        return destPos + src.length;
    }

    private byte[] append(byte[] src, int length, byte[] key, byte[] value,
        byte separator) {
        int appendLength = 0;

        if (key != null) {
            // key
            appendLength += (key.length + 1);
        }

        if ((value != null) && (value.length > 0)) {
            // '=' + value
            appendLength += (value.length + 1);
        }

        byte[] newBuffer = increaseBufferIfNeeded(src, length, appendLength);

        // append dest
        int newPos = length;

        if (key != null) {
            // append separator
            newBuffer[newPos] = separator;
            newPos += 1;
            // append key
            newPos = append(key, newBuffer, newPos);
        }

        if ((value != null) && (value.length > 0)) {
            // append =
            newBuffer[newPos] = '=';
            newPos += 1;
            // append value
            newPos = append(value, newBuffer, newPos);
        }

        size = newPos;

        return newBuffer;
    }

    private byte[] replace(byte[] src, int length, int start, int end,
        byte[] value) {
        int equalLength = 1;

        // don't need equal char if value is empty...
        if (value.length == 0) {
            equalLength = 0;
        }

        // could be negative if new value is
        // shorter then previous value..
        int extendLength = (value.length + equalLength) - (end - start);
        byte[] newBuffer = increaseBufferIfNeeded(src, length, extendLength);
        size = length + extendLength;

        // append until start
        if (src != newBuffer) {
            System.arraycopy(src, 0, newBuffer, 0, start);
        }

        // move last section first, same buffer could be used & risk of
        // overwrite useful data...
        if ((length - end) > 0) {
            int destPos = (value.length == 0) ? start : (start + 1 +
                value.length);
            System.arraycopy(src, end, newBuffer, destPos, length - end);
        }

        if (value.length > 0) {
            // insert '='
            newBuffer[start] = '=';
            // insert value
            System.arraycopy(value, 0, newBuffer, start + 1, value.length);
        }

        return newBuffer;
    }

    private byte[] remove(byte[] src, int length, int start, int end) {
        size = length - (end - start);

        // append from end to length of dest
        if ((length - end) > 0) {
            System.arraycopy(src, end, src, start, length - end);
        }

        return src;
    }

    private void removeKey(byte[] key) {
        int pos = indexOf(key);

        while ((pos != -1) && (buffer[pos - 1] != separator)) {
            // what if key is same as data inside value:
            // e.g. key=hej and buffer: ;key1=hej;hej=value2
            pos = indexOf(key, pos + 1);
        }

        if (pos >= 0) {
            if ((pos + key.length) == size()) {
                // key exist without value at end of buffer...
                buffer = remove(buffer, size(), pos - 1, size());
            } else if (buffer[pos + key.length] == separator) {
                // key exist without value inside buffer...
                buffer = remove(buffer, size(), pos - 1, pos + key.length);
            } else {
                int mark = pos - 1;

                while ((pos < size()) && (buffer[pos] != separator)) {
                    pos++;
                }

                // key and value exist...
                buffer = remove(buffer, size(), mark, pos);
            }
        }
    }

    private void putValue(byte[] key, byte[] value) {
        int pos = indexOf(key);

        while ((pos != -1) && (buffer[pos - 1] != separator)) {
            // what if key is same as data inside value:
            // e.g. key=hej and buffer: ;key1=hej;hej=value2
            pos = indexOf(key, pos + 1);
        }

        if (pos == -1) {
            // key was not found, lets append new key and value
            buffer = append(buffer, size(), key, value, separator);
        } else if (value != null) {
            // key is found, replace value if present
            //
            // find next separator or end of buffer
            while ((pos < (size() - 1)) && (buffer[pos] != separator)) {
                if (buffer[pos] == '=') {
                    int mark = pos;
                    pos++;

                    while (pos < size()) {
                        if (buffer[pos] == separator) {
                            break;
                        }

                        pos++;
                    }

                    // previous value should now be replaced...
                    buffer = replace(buffer, size(), mark, pos, value);

                    return;
                }

                pos++;
            }

            if (buffer[pos] == separator) {
                // only key before, no previous value...
                buffer = replace(buffer, size(), pos, pos, value);
            } else {
                // last key, no previous value...
                // lets append the value to the end of the buffer
                buffer = append(buffer, size(), null, value, separator);
            }
        }
    }

    @Override
    public Object clone() {
        ParameterByteMap newMap = new ParameterByteMap();

        if (buffer != null) {
            byte[] newBuffer = new byte[buffer.length];
            System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
            newMap.buffer = newBuffer;
            newMap.size = size;
        }

        newMap.separator = separator;

        return newMap;
    }

    private boolean isQuotedString(String str) {
        return (str != null) && !str.equals("") && (str.charAt(0) == '"') &&
        (str.charAt(str.length() - 1) == '"');
    }

    /**
     * Returns the value to which the specified key is mapped in this identity
     * map, or null if the map contains no mapping for this key.
     *
     * @param key
     *        the key whose associated value is to be returned.
     * @return the value to which this map maps the specified key, or null if the
     *         map contains no mapping for this key.
     */
    public String get(String key) {
        byte[] escapedKey = Ascii7String.getBytes(key);
        String value;

        try {
            value = getValue(escapedKey);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }

        String unescapedValue = isQuotedString(value) ? value : unescape(value);

        return unescapedValue;
    }

    /**
     * Associates the specified value with the specified key in this map. If the
     * map previously contained a mapping for this key, the old value is
     * replaced.
     *
     * @param key
     *        key with which the specified value is to be associated.
     * @param value
     *        value to be associated with the specified key.
     */
    public void put(String key, String value) {
        byte[] encodedKey = Ascii7String.getBytes(key);

        try {
            byte[] encodedValue = isQuotedString(value)
                ? value.getBytes(SipFactoryImpl.SIP_CHARSET) : escape(value);
            putValue(encodedKey, encodedValue);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Removes the mapping for this key from this map if present.
     *
     * @param key
     *        key whose mapping is to be removed from the map.
     */
    public void remove(String key) {
        byte[] encodedKey = Ascii7String.getBytes(key);
        removeKey(encodedKey);
    }

    /**
     * </p>
     * Returns a iterator view of the keys contained in this map.
     *
     * @return a iterator view of the keys contained in this map.
     */
    public Iterator<String> getKeys() {
        return keyList().iterator();
    }

    private List<String> keyList() {
        LinkedList<String> l = new LinkedList<String>();
        int pos = 0;
        int mark = 0;

        while (pos < size()) {
            if (buffer[pos] == separator) {
                // found separator..
                pos++;
                mark = pos; // mark start of key

                while ((pos < size()) && (buffer[pos] != separator)) {
                    if (buffer[pos] == '=') {
                        break;
                    }

                    pos++;
                }

                // intentionally no character conversion since
                // no other than US-ASCII is supported
                l.add(new String(buffer, mark, pos - mark));
            } else {
                pos++;
            }
        }

        return l;
    }

    public Set<Map.Entry<String, String>> entrySet() {
        HashMap<String, String> entryMap = new HashMap<String, String>();

        for (String s : keyList()) {
            entryMap.put(s, get(s));
        }

        return entryMap.entrySet();
    }

    /**
     * Returns the map as a byte array.
     *
     * @return the map as a byte array.
     */
    public byte[] toArray() {
        if (buffer == null) {
            return null;
        }

        byte[] newArray = new byte[size];
        System.arraycopy(buffer, 0, newArray, 0, size);

        if (separator == '&') {
            // special handling for uri-headers,
            // replace & with ?
            newArray[0] = '?';
        }

        return newArray;
    }

    @Override
    public String toString() {
        if (buffer == null) {
            return "";
        }

        try {
            String str = null;

            if (separator == '&') {
                // special handling for uri-headers,
                // replace & with ?
                buffer[0] = '?';
                str = new String(buffer, 0, size(), SipFactoryImpl.SIP_CHARSET);
                buffer[0] = '&';
            } else {
                str = new String(buffer, 0, size(), SipFactoryImpl.SIP_CHARSET);
            }

            return str;
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * A user, ttl, or method uri-parameter appearing in only one URI never
     * matches, even if it contains the default value.
     * </p>
     * A URI that includes an maddr parameter will not match a URI that contains
     * no maddr parameter.
     * </p>
     * URI header components are never ignored
     *
     * @return
     */
    private boolean isHeaderOrSpecialParameter(String parameter) {
        return ((separator == '&') || isSpecialParameter(parameter));
    }

    private boolean isSpecialParameter(String parameter) {
        return ((separator == '&') ||
        ((separator == ';') &&
        (parameter.equalsIgnoreCase("user") ||
        parameter.equalsIgnoreCase("method") ||
        parameter.equalsIgnoreCase("ttl") ||
        parameter.equalsIgnoreCase("transport") ||
        parameter.equalsIgnoreCase("maddr"))));
    }

    /**
     * Any parameter that is: user, ttl, or method uri-parameter or maddr
     * parameter.
     *
     * @return true or false
     */
    public boolean isOnlySpecialParameters() {
        for (String key : keyList()) {
            if (!isSpecialParameter(key)) {
                return false;
            }
        }

        return true;
    }

    private String findAndRemoveKey(String key, List<String> remoteKeys) {
        int i = -1;

        for (String remotekey : remoteKeys) {
            i++;

            if (key.equalsIgnoreCase(remotekey)) {
                return remoteKeys.remove(i);
            }
        }

        return null;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (obj instanceof ParameterByteMap) {
            ParameterByteMap remote = (ParameterByteMap) obj;
            List<String> keys = keyList();
            List<String> remoteKeys = remote.keyList();

            // if the maps don't have the same number
            // of headers they are not equal.
            if ((separator == '&') && (keys.size() != remoteKeys.size())) {
                return false;
            }

            String value = null;
            String remoteValue = null;
            String remoteKey = null;

            for (String key : keys) {
                remoteKey = findAndRemoveKey(key, remoteKeys);

                if (remoteKey != null) {
                    value = get(key);
                    remoteValue = remote.get(remoteKey);

                    // if one parameter is set in one map but
                    // not in the other then they are not equal
                    if (((value != null) && (remoteValue == null)) ||
                            ((value == null) && (remoteValue != null))) {
                        return false;
                    } else {
                        // if one parameter is not equal then the whole map is not
                        // equal
                        if (((value != null) && (remoteValue != null)) &&
                                !value.equalsIgnoreCase(remoteValue)) {
                            return false;
                        }
                    }
                } else {
                    // key only available in one list, it is ignored
                    // if it's not a header or special one...
                    if (isHeaderOrSpecialParameter(key)) {
                        return false;
                    }
                }
            }

            // the remote list might still have parameters, they are ignored
            // if they are not headers or special once...
            for (String leftKey : remoteKeys) {
                if (isHeaderOrSpecialParameter(leftKey)) {
                    return false;
                }
            }

            return true;
        }

        return false;
    }
}
