/*
 *  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.tomcat.util.buf;

import java.io.IOException;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.util.Locale;

import org.apache.tomcat.util.res.StringManager;

/**
 * This class is used to represent a sub array of bytes in an HTTP message. It represents all request/response elements.
 * The byte/char conversions are delayed and cached. Everything is recyclable.
 * <p>
 * The object can represent a byte[], a char[], or a (sub) String. All operations can be made in case-sensitive mode or
 * not.
 */
public final class MessageBytes implements Cloneable, Serializable {

    private static final long serialVersionUID = 1L;

    private static final StringManager sm = StringManager.getManager(MessageBytes.class);

    // primary type ( whatever is set as original value )
    /**
     * The primary data type of this MessageBytes instance.
     */
    private int type = T_NULL;

    /**
     * Type is NULL.
     */
    public static final int T_NULL = 0;
    /**
     * getType() is T_STR if the object used to create the MessageBytes was a String.
     */
    public static final int T_STR = 1;
    /**
     * getType() is T_BYTES if the object used to create the MessageBytes was a byte[].
     */
    public static final int T_BYTES = 2;
    /**
     * getType() is T_CHARS if the object used to create the MessageBytes was a char[].
     */
    public static final int T_CHARS = 3;

    /**
     * Empty character array.
     */
    public static final char[] EMPTY_CHAR_ARRAY = new char[0];

    /**
     * The cached hash code value.
     */
    private int hashCode = 0;
    // did we compute the hashcode ?
    /**
     * Whether the hash code has been computed.
     */
    private boolean hasHashCode = false;

    // Internal objects to represent array + offset, and specific methods
    /**
     * The internal byte chunk for byte array representation.
     */
    private final ByteChunk byteC = new ByteChunk();
    /**
     * The internal char chunk for character array representation.
     */
    private final CharChunk charC = new CharChunk();

    // String
    /**
     * The cached string value.
     */
    private String strValue;

    /**
     * Creates a new, uninitialized MessageBytes object. Use static newInstance() in order to allow future hooks.
     */
    private MessageBytes() {
    }

    /**
     * Construct a new MessageBytes instance.
     *
     * @return the instance
     */
    public static MessageBytes newInstance() {
        return factory.newInstance();
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    /**
     * Check if the message bytes is null.
     * @return true if null
     */
    public boolean isNull() {
        return type == T_NULL;
    }

    /**
     * Resets the message bytes to an uninitialized (NULL) state.
     */
    public void recycle() {
        type = T_NULL;
        byteC.recycle();
        charC.recycle();

        strValue = null;

        hasHashCode = false;
        hasLongValue = false;
    }


    /**
     * Sets the content to the specified sub array of bytes.
     *
     * @param b   the bytes
     * @param off the start offset of the bytes
     * @param len the length of the bytes
     */
    public void setBytes(byte[] b, int off, int len) {
        byteC.setBytes(b, off, len);
        type = T_BYTES;
        hasHashCode = false;
        hasLongValue = false;
    }

    /**
     * Sets the content to be a char[]
     *
     * @param c   the chars
     * @param off the start offset of the chars
     * @param len the length of the chars
     */
    public void setChars(char[] c, int off, int len) {
        charC.setChars(c, off, len);
        type = T_CHARS;
        hasHashCode = false;
        hasLongValue = false;
    }

    /**
     * Set the content to be a string
     *
     * @param s The string
     */
    public void setString(String s) {
        strValue = s;
        hasHashCode = false;
        hasLongValue = false;
        if (s == null) {
            type = T_NULL;
        } else {
            type = T_STR;
        }
    }

    // -------------------- Conversion and getters --------------------

    /**
     * Compute the string value.
     *
     * @return the string
     */
    @Override
    public String toString() {
        switch (type) {
            case T_NULL:
            case T_STR:
                // No conversion required
                break;
            case T_BYTES:
                strValue = byteC.toString();
                break;
            case T_CHARS:
                strValue = charC.toString();
                break;
        }

        return strValue;
    }


    /**
     * Convert to String (if not already of the String type) and then return the String value.
     *
     * @return The current value as a String
     */
    public String toStringType() {
        switch (type) {
            case T_NULL:
            case T_STR:
                // No conversion required
                break;
            case T_BYTES:
                setString(byteC.toString());
                break;
            case T_CHARS:
                setString(charC.toString());
                break;
        }

        return strValue;
    }


    // ----------------------------------------
    /**
     * Return the type of the original content. Can be T_STR, T_BYTES, T_CHARS or T_NULL
     *
     * @return the type
     */
    public int getType() {
        return type;
    }

    /**
     * Returns the byte chunk, representing the byte[] and offset/length. Valid only if T_BYTES or after a conversion
     * was made.
     *
     * @return the byte chunk
     */
    public ByteChunk getByteChunk() {
        return byteC;
    }

    /**
     * Returns the char chunk, representing the char[] and offset/length. Valid only if T_CHARS or after a conversion
     * was made.
     *
     * @return the char chunk
     */
    public CharChunk getCharChunk() {
        return charC;
    }

    /**
     * Returns the string value. Valid only if T_STR or after a conversion was made.
     *
     * @return the string
     */
    public String getString() {
        return strValue;
    }

    /**
     * Return the Charset used for string&lt;-&gt;byte conversions.
     *
     * @return the Charset used for string&lt;-&gt;byte conversions
     */
    public Charset getCharset() {
        return byteC.getCharset();
    }

    /**
     * Set the Charset used for string&lt;-&gt;byte conversions.
     *
     * @param charset The charset
     */
    public void setCharset(Charset charset) {
        byteC.setCharset(charset);
    }


    /**
     * Convert to bytes and fill the ByteChunk with the converted value.
     */
    public void toBytes() {
        if (type == T_NULL) {
            byteC.recycle();
            return;
        }

        if (type == T_BYTES) {
            // No conversion required
            return;
        }

        if (getCharset() == ByteChunk.DEFAULT_CHARSET) {
            if (type == T_CHARS) {
                toBytesSimple(charC.getChars(), charC.getStart(), charC.getLength());
            } else {
                // Must be T_STR
                char[] chars = strValue.toCharArray();
                toBytesSimple(chars, 0, chars.length);
            }
            return;
        }

        ByteBuffer bb;
        if (type == T_CHARS) {
            bb = getCharset().encode(CharBuffer.wrap(charC));
        } else {
            // Must be T_STR
            bb = getCharset().encode(strValue);
        }

        byteC.setBytes(bb.array(), bb.arrayOffset(), bb.limit());
    }


    /**
     * Simple conversion of chars to bytes.
     *
     * @throws IllegalArgumentException if any of the characters to convert are above code point 0xFF.
     */
    private void toBytesSimple(char[] chars, int start, int len) {
        byte[] bytes = new byte[len];

        for (int i = 0; i < len; i++) {
            if (chars[i + start] > 255) {
                throw new IllegalArgumentException(sm.getString("messageBytes.illegalCharacter",
                        Character.toString(chars[i + start]), Integer.valueOf(chars[i + start])));
            } else {
                bytes[i] = (byte) chars[i + start];
            }
        }

        byteC.setBytes(bytes, 0, len);
        type = T_BYTES;
    }


    /**
     * Convert to char[] and fill the CharChunk.
     * <p>
     * Note: The conversion from bytes is not optimised - it converts to String first. However, Tomcat doesn't call this
     * method to convert from bytes so there is no benefit from optimising that path.
     */
    public void toChars() {
        switch (type) {
            case T_NULL:
                charC.recycle();
                //$FALL-THROUGH$
            case T_CHARS:
                // No conversion required
                return;
            case T_BYTES:
                toString();
                //$FALL-THROUGH$
            case T_STR: {
                char[] cc = strValue.toCharArray();
                charC.setChars(cc, 0, cc.length);
            }
        }
    }


    /**
     * Returns the length of the original buffer.
     * <p>
     * Note: The length in bytes may be different from the length in chars.
     *
     * @return the length
     */
    public int getLength() {
        if (type == T_BYTES) {
            return byteC.getLength();
        }
        if (type == T_CHARS) {
            return charC.getLength();
        }
        if (type == T_STR) {
            return strValue.length();
        }
        toString();
        if (strValue == null) {
            return 0;
        }
        return strValue.length();
    }

    // -------------------- equals --------------------

    /**
     * Compares the message bytes to the specified String object.
     *
     * @param s the String to compare
     *
     * @return <code>true</code> if the comparison succeeded, <code>false</code> otherwise
     */
    public boolean equals(String s) {
        switch (type) {
            case T_STR:
                if (strValue == null) {
                    return s == null;
                }
                return strValue.equals(s);
            case T_CHARS:
                return charC.equals(s);
            case T_BYTES:
                return byteC.equals(s);
            default:
                return false;
        }
    }

    /**
     * Compares the message bytes to the specified String object.
     *
     * @param s the String to compare
     *
     * @return <code>true</code> if the comparison succeeded, <code>false</code> otherwise
     */
    public boolean equalsIgnoreCase(String s) {
        switch (type) {
            case T_STR:
                if (strValue == null) {
                    return s == null;
                }
                return strValue.equalsIgnoreCase(s);
            case T_CHARS:
                return charC.equalsIgnoreCase(s);
            case T_BYTES:
                return byteC.equalsIgnoreCase(s);
            default:
                return false;
        }
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof MessageBytes) {
            return equals((MessageBytes) obj);
        }
        return false;
    }

    /**
     * Compare this message bytes to another.
     * @param mb the other message bytes
     * @return true if equal
     */
    public boolean equals(MessageBytes mb) {
        // MessageBytes can be one of four types so there are 4 * 4 = 16 possible combinations.

        // If either instance is a String, use equals(String)
        if (type == T_STR) {
            return mb.equals(strValue);
        }
        if (mb.type == T_STR) {
            return equals(mb.strValue);
        }

        // If either instance is null, use isNull
        if (type == T_NULL) {
            return mb.isNull();
        }
        if (mb.type == T_NULL) {
            return isNull();
        }

        // At this point both instances are either T_BYTES or T_CHARS
        if (mb.type == T_CHARS && type == T_CHARS) {
            return charC.equals(mb.charC);
        }
        if (mb.type == T_BYTES && type == T_BYTES) {
            return byteC.equals(mb.byteC);
        }
        if (mb.type == T_CHARS && type == T_BYTES) {
            return byteC.equals(mb.charC);
        }
        if (mb.type == T_BYTES && type == T_CHARS) {
            return mb.byteC.equals(charC);
        }

        // Impossible to reach this point
        return false;
    }


    /**
     * Check if the message bytes starts with the specified string, ignoring case.
     *
     * @return <code>true</code> if the message bytes starts with the specified string
     * @param s   the string
     * @param pos The start position
     */
    public boolean startsWithIgnoreCase(String s, int pos) {
        switch (type) {
            case T_STR:
                if (strValue == null) {
                    return false;
                }
                if (strValue.length() < pos + s.length()) {
                    return false;
                }

                for (int i = 0; i < s.length(); i++) {
                    char c1 = s.charAt(i);
                    char c2 = strValue.charAt(pos + i);
                    // Use ASCII short-cut if possible
                    if (c1 > 0xFF || c2 > 0xFF) {
                        if (Character.toLowerCase(c1) != Character.toLowerCase(c2)) {
                            return false;
                        }
                    } else if (Ascii.toLower(c1) != Ascii.toLower(c2)) {
                        return false;
                    }

                }
                return true;
            case T_CHARS:
                return charC.startsWithIgnoreCase(s, pos);
            case T_BYTES:
                return byteC.startsWithIgnoreCase(s, pos);
            default:
                return false;
        }
    }


    // -------------------- Hash code --------------------
    @Override
    public int hashCode() {
        if (hasHashCode) {
            return hashCode;
        }
        int code = hash();
        hashCode = code;
        hasHashCode = true;
        return code;
    }

    // normal hash.
    private int hash() {
        int code = 0;
        switch (type) {
            case T_STR:
                // We need to use the same hash function
                for (int i = 0; i < strValue.length(); i++) {
                    code = code * 37 + strValue.charAt(i);
                }
                return code;
            case T_CHARS:
                return charC.hash();
            case T_BYTES:
                return byteC.hash();
            default:
                return 0;
        }
    }

    // Inefficient initial implementation. Will be replaced on the next
    // round of tune-up
    /**
     * Find the index of a string.
     * @param s the string
     * @param starting the starting position
     * @return the index
     */
    public int indexOf(String s, int starting) {
        toString();
        return strValue.indexOf(s, starting);
    }

    // Inefficient initial implementation. Will be replaced on the next
    // round of tune-up
    /**
     * Find the index of a string.
     * @param s the string
     * @return the index
     */
    public int indexOf(String s) {
        return indexOf(s, 0);
    }

    /**
     * Find the index of a string, ignoring case.
     * @param s the string
     * @param starting the starting position
     * @return the index
     */
    public int indexOfIgnoreCase(String s, int starting) {
        toString();
        String upper = strValue.toUpperCase(Locale.ENGLISH);
        String sU = s.toUpperCase(Locale.ENGLISH);
        return upper.indexOf(sU, starting);
    }

    /**
     * Copy the src into this MessageBytes, allocating more space if needed.
     *
     * @param src The source
     *
     * @throws IOException Writing overflow data to the output channel failed
     */
    public void duplicate(MessageBytes src) throws IOException {
        switch (src.getType()) {
            case T_BYTES:
                type = T_BYTES;
                ByteChunk bc = src.getByteChunk();
                byteC.allocate(2 * bc.getLength(), -1);
                byteC.append(bc);
                break;
            case T_CHARS:
                type = T_CHARS;
                CharChunk cc = src.getCharChunk();
                charC.allocate(2 * cc.getLength(), -1);
                charC.append(cc);
                break;
            case T_STR:
                type = T_STR;
                String sc = src.getString();
                this.setString(sc);
                break;
            case T_NULL:
                recycle();
        }
        setCharset(src.getCharset());
    }

    // efficient long
    /**
     * The cached long value.
     */
    private long longValue;
    /**
     * Whether the long value has been computed.
     */
    private boolean hasLongValue = false;

    /**
     * Set the buffer to the representation of a long.
     *
     * @param l The long
     */
    public void setLong(long l) {
        byteC.allocate(32, 64);
        long current = l;
        byte[] buf = byteC.getBuffer();
        int start = 0;
        int end = 0;
        if (l == 0) {
            buf[end++] = (byte) '0';
        }
        if (l < 0) {
            buf[end++] = (byte) '-';
        } else {
            current = -l;
        }
        while (current < 0) {
            int digit = (int) (current % 10);
            current = current / 10;
            buf[end++] = HexUtils.getHex(-digit);
        }
        byteC.setStart(0);
        byteC.setEnd(end);
        // Inverting buffer
        end--;
        if (l < 0) {
            start++;
        }
        while (end > start) {
            byte temp = buf[start];
            buf[start] = buf[end];
            buf[end] = temp;
            start++;
            end--;
        }
        longValue = l;
        hasHashCode = false;
        hasLongValue = true;
        type = T_BYTES;
    }

    /**
     * Convert the buffer to a long, cache the value. Used for headers conversion.
     *
     * @return the long value
     */
    public long getLong() {
        if (hasLongValue) {
            return longValue;
        }

        if (type == T_BYTES) {
            longValue = byteC.getLong();
        } else {
            longValue = Long.parseLong(toString());
        }

        hasLongValue = true;
        return longValue;

    }

    // -------------------- Future may be different --------------------

    private static final MessageBytesFactory factory = new MessageBytesFactory();

    private static class MessageBytesFactory {
        protected MessageBytesFactory() {
        }

        public MessageBytes newInstance() {
            return new MessageBytes();
        }
    }
}
