/*
 *
 * 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.qpid.framing;

import org.apache.mina.common.ByteBuffer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.Charset;

public class EncodingUtils
{
    private static final Logger _logger = LoggerFactory.getLogger(EncodingUtils.class);

    private static final String STRING_ENCODING = "iso8859-15";

    private static final Charset _charset = Charset.forName("iso8859-15");

    public static final int SIZEOF_UNSIGNED_SHORT = 2;
    public static final int SIZEOF_UNSIGNED_INT = 4;
    private static final boolean[] ALL_FALSE_ARRAY = new boolean[8];

    public static int encodedShortStringLength(String s)
    {
        if (s == null)
        {
            return 1;
        }
        else
        {
            return (short) (1 + s.length());
        }
    }

    public static int encodedShortStringLength(short s)
    {
        if (s == 0)
        {
            return 1 + 1;
        }

        int len = 0;
        if (s < 0)
        {
            len = 1;
            // sloppy - doesn't work of Integer.MIN_VALUE
            s = (short) -s;
        }

        if (s > 9999)
        {
            return 1 + 5;
        }
        else if (s > 999)
        {
            return 1 + 4;
        }
        else if (s > 99)
        {
            return 1 + 3;
        }
        else if (s > 9)
        {
            return 1 + 2;
        }
        else
        {
            return 1 + 1;
        }

    }

    public static int encodedShortStringLength(int i)
    {
        if (i == 0)
        {
            return 1 + 1;
        }

        int len = 0;
        if (i < 0)
        {
            len = 1;
            // sloppy - doesn't work of Integer.MIN_VALUE
            i = -i;
        }

        // range is now 1 - 2147483647
        if (i < Short.MAX_VALUE)
        {
            return len + encodedShortStringLength((short) i);
        }
        else if (i > 999999)
        {
            return len + 6 + encodedShortStringLength((short) (i / 1000000));
        }
        else // if (i > 99999)
        {
            return len + 5 + encodedShortStringLength((short) (i / 100000));
        }

    }

    public static int encodedShortStringLength(long l)
    {
        if (l == 0)
        {
            return 1 + 1;
        }

        int len = 0;
        if (l < 0)
        {
            len = 1;
            // sloppy - doesn't work of Long.MIN_VALUE
            l = -l;
        }

        if (l < Integer.MAX_VALUE)
        {
            return len + encodedShortStringLength((int) l);
        }
        else if (l > 9999999999L)
        {
            return len + 10 + encodedShortStringLength((int) (l / 10000000000L));
        }
        else
        {
            return len + 1 + encodedShortStringLength((int) (l / 10L));
        }

    }

    public static int encodedShortStringLength(AMQShortString s)
    {
        if (s == null)
        {
            return 1;
        }
        else
        {
            return (1 + s.length());
        }
    }

    public static int encodedLongStringLength(String s)
    {
        if (s == null)
        {
            return 4;
        }
        else
        {
            return 4 + s.length();
        }
    }

    public static int encodedLongStringLength(char[] s)
    {
        if (s == null)
        {
            return 4;
        }
        else
        {
            return 4 + s.length;
        }
    }

    public static int encodedLongstrLength(byte[] bytes)
    {
        if (bytes == null)
        {
            return 4;
        }
        else
        {
            return 4 + bytes.length;
        }
    }

    public static int encodedFieldTableLength(FieldTable table)
    {
        if (table == null)
        {
            // length is encoded as 4 octets
            return 4;
        }
        else
        {
            // length of the table plus 4 octets for the length
            return (int) table.getEncodedSize() + 4;
        }
    }

    public static int encodedContentLength(Content table)
    {
        // TODO: New Content class required for AMQP 0-9.
        return 0;
    }

    public static void writeShortStringBytes(ByteBuffer buffer, String s)
    {
        if (s != null)
        {
            byte[] encodedString = new byte[s.length()];
            char[] cha = s.toCharArray();
            for (int i = 0; i < cha.length; i++)
            {
                encodedString[i] = (byte) cha[i];
            }

            writeBytes(buffer, encodedString);
        }
        else
        {
            // really writing out unsigned byte
            buffer.put((byte) 0);
        }
    }

    public static void writeShortStringBytes(ByteBuffer buffer, AMQShortString s)
    {
        if (s != null)
        {

            s.writeToBuffer(buffer);
        }
        else
        {
            // really writing out unsigned byte
            buffer.put((byte) 0);
        }
    }

    public static void writeLongStringBytes(ByteBuffer buffer, String s)
    {
        assert (s == null) || (s.length() <= 0xFFFE);
        if (s != null)
        {
            int len = s.length();
            writeUnsignedInteger(buffer, s.length());
            byte[] encodedString = new byte[len];
            char[] cha = s.toCharArray();
            for (int i = 0; i < cha.length; i++)
            {
                encodedString[i] = (byte) cha[i];
            }

            buffer.put(encodedString);
        }
        else
        {
            writeUnsignedInteger(buffer, 0);
        }
    }

    public static void writeLongStringBytes(ByteBuffer buffer, char[] s)
    {
        assert (s == null) || (s.length <= 0xFFFE);
        if (s != null)
        {
            int len = s.length;
            writeUnsignedInteger(buffer, s.length);
            byte[] encodedString = new byte[len];
            for (int i = 0; i < s.length; i++)
            {
                encodedString[i] = (byte) s[i];
            }

            buffer.put(encodedString);
        }
        else
        {
            writeUnsignedInteger(buffer, 0);
        }
    }

    public static void writeLongStringBytes(ByteBuffer buffer, byte[] bytes)
    {
        assert (bytes == null) || (bytes.length <= 0xFFFE);
        if (bytes != null)
        {
            writeUnsignedInteger(buffer, bytes.length);
            buffer.put(bytes);
        }
        else
        {
            writeUnsignedInteger(buffer, 0);
        }
    }

    public static void writeUnsignedByte(ByteBuffer buffer, short b)
    {
        byte bv = (byte) b;
        buffer.put(bv);
    }

    public static void writeUnsignedShort(ByteBuffer buffer, int s)
    {
        // TODO: Is this comparison safe? Do I need to cast RHS to long?
        if (s < Short.MAX_VALUE)
        {
            buffer.putShort((short) s);
        }
        else
        {
            short sv = (short) s;
            buffer.put((byte) (0xFF & (sv >> 8)));
            buffer.put((byte) (0xFF & sv));
        }
    }

    public static int unsignedIntegerLength()
    {
        return 4;
    }

    public static void writeUnsignedInteger(ByteBuffer buffer, long l)
    {
        // TODO: Is this comparison safe? Do I need to cast RHS to long?
        if (l < Integer.MAX_VALUE)
        {
            buffer.putInt((int) l);
        }
        else
        {
            int iv = (int) l;

            // FIXME: This *may* go faster if we build this into a local 4-byte array and then
            // put the array in a single call.
            buffer.put((byte) (0xFF & (iv >> 24)));
            buffer.put((byte) (0xFF & (iv >> 16)));
            buffer.put((byte) (0xFF & (iv >> 8)));
            buffer.put((byte) (0xFF & iv));
        }
    }

    public static void writeFieldTableBytes(ByteBuffer buffer, FieldTable table)
    {
        if (table != null)
        {
            table.writeToBuffer(buffer);
        }
        else
        {
            EncodingUtils.writeUnsignedInteger(buffer, 0);
        }
    }

    public static void writeContentBytes(ByteBuffer buffer, Content content)
    {
        // TODO: New Content class required for AMQP 0-9.
    }

    public static void writeBooleans(ByteBuffer buffer, boolean[] values)
    {
        byte packedValue = 0;
        for (int i = 0; i < values.length; i++)
        {
            if (values[i])
            {
                packedValue = (byte) (packedValue | (1 << i));
            }
        }

        buffer.put(packedValue);
    }

    public static void writeBooleans(ByteBuffer buffer, boolean value)
    {

        buffer.put(value ? (byte) 1 : (byte) 0);
    }

    public static void writeBooleans(ByteBuffer buffer, boolean value0, boolean value1)
    {
        byte packedValue = value0 ? (byte) 1 : (byte) 0;

        if (value1)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 1));
        }

        buffer.put(packedValue);
    }

    public static void writeBooleans(ByteBuffer buffer, boolean value0, boolean value1, boolean value2)
    {
        byte packedValue = value0 ? (byte) 1 : (byte) 0;

        if (value1)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 1));
        }

        if (value2)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 2));
        }

        buffer.put(packedValue);
    }

    public static void writeBooleans(ByteBuffer buffer, boolean value0, boolean value1, boolean value2, boolean value3)
    {
        byte packedValue = value0 ? (byte) 1 : (byte) 0;

        if (value1)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 1));
        }

        if (value2)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 2));
        }

        if (value3)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 3));
        }

        buffer.put(packedValue);
    }

    public static void writeBooleans(ByteBuffer buffer, boolean value0, boolean value1, boolean value2, boolean value3,
        boolean value4)
    {
        byte packedValue = value0 ? (byte) 1 : (byte) 0;

        if (value1)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 1));
        }

        if (value2)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 2));
        }

        if (value3)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 3));
        }

        if (value4)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 4));
        }

        buffer.put(packedValue);
    }

    public static void writeBooleans(ByteBuffer buffer, boolean value0, boolean value1, boolean value2, boolean value3,
        boolean value4, boolean value5)
    {
        byte packedValue = value0 ? (byte) 1 : (byte) 0;

        if (value1)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 1));
        }

        if (value2)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 2));
        }

        if (value3)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 3));
        }

        if (value4)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 4));
        }

        if (value5)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 5));
        }

        buffer.put(packedValue);
    }

    public static void writeBooleans(ByteBuffer buffer, boolean value0, boolean value1, boolean value2, boolean value3,
        boolean value4, boolean value5, boolean value6)
    {
        byte packedValue = value0 ? (byte) 1 : (byte) 0;

        if (value1)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 1));
        }

        if (value2)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 2));
        }

        if (value3)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 3));
        }

        if (value4)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 4));
        }

        if (value5)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 5));
        }

        if (value6)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 6));
        }

        buffer.put(packedValue);
    }

    public static void writeBooleans(ByteBuffer buffer, boolean value0, boolean value1, boolean value2, boolean value3,
        boolean value4, boolean value5, boolean value6, boolean value7)
    {
        byte packedValue = value0 ? (byte) 1 : (byte) 0;

        if (value1)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 1));
        }

        if (value2)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 2));
        }

        if (value3)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 3));
        }

        if (value4)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 4));
        }

        if (value5)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 5));
        }

        if (value6)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 6));
        }

        if (value7)
        {
            packedValue = (byte) (packedValue | (byte) (1 << 7));
        }

        buffer.put(packedValue);
    }

    /**
     * This is used for writing longstrs.
     *
     * @param buffer
     * @param data
     */
    public static void writeLongstr(ByteBuffer buffer, byte[] data)
    {
        if (data != null)
        {
            writeUnsignedInteger(buffer, data.length);
            buffer.put(data);
        }
        else
        {
            writeUnsignedInteger(buffer, 0);
        }
    }

    public static void writeTimestamp(ByteBuffer buffer, long timestamp)
    {
        writeLong(buffer, timestamp);
    }

    public static boolean[] readBooleans(ByteBuffer buffer)
    {
        final byte packedValue = buffer.get();
        if (packedValue == 0)
        {
            return ALL_FALSE_ARRAY;
        }

        final boolean[] result = new boolean[8];

        result[0] = ((packedValue & 1) != 0);
        result[1] = ((packedValue & (1 << 1)) != 0);
        result[2] = ((packedValue & (1 << 2)) != 0);
        result[3] = ((packedValue & (1 << 3)) != 0);
        if ((packedValue & 0xF0) == 0)
        {
            result[0] = ((packedValue & 1) != 0);
        }

        result[4] = ((packedValue & (1 << 4)) != 0);
        result[5] = ((packedValue & (1 << 5)) != 0);
        result[6] = ((packedValue & (1 << 6)) != 0);
        result[7] = ((packedValue & (1 << 7)) != 0);

        return result;
    }

    public static FieldTable readFieldTable(ByteBuffer buffer) throws AMQFrameDecodingException
    {
        long length = buffer.getUnsignedInt();
        if (length == 0)
        {
            return null;
        }
        else
        {
            return FieldTableFactory.newFieldTable(buffer, length);
        }
    }

    public static Content readContent(ByteBuffer buffer) throws AMQFrameDecodingException
    {
        // TODO: New Content class required for AMQP 0-9.
        return null;
    }

    public static AMQShortString readAMQShortString(ByteBuffer buffer)
    {
        return AMQShortString.readFromBuffer(buffer);

    }

    public static String readShortString(ByteBuffer buffer)
    {
        short length = buffer.getUnsigned();
        if (length == 0)
        {
            return null;
        }
        else
        {
            // this may seem rather odd to declare two array but testing has shown
            // that constructing a string from a byte array is 5 (five) times slower
            // than constructing one from a char array.
            // this approach here is valid since we know that all the chars are
            // ASCII (0-127)
            byte[] stringBytes = new byte[length];
            buffer.get(stringBytes, 0, length);
            char[] stringChars = new char[length];
            for (int i = 0; i < stringChars.length; i++)
            {
                stringChars[i] = (char) stringBytes[i];
            }

            return new String(stringChars);
        }
    }

    public static String readLongString(ByteBuffer buffer)
    {
        long length = buffer.getUnsignedInt();
        if (length == 0)
        {
            return "";
        }
        else
        {
            // this may seem rather odd to declare two array but testing has shown
            // that constructing a string from a byte array is 5 (five) times slower
            // than constructing one from a char array.
            // this approach here is valid since we know that all the chars are
            // ASCII (0-127)
            byte[] stringBytes = new byte[(int) length];
            buffer.get(stringBytes, 0, (int) length);
            char[] stringChars = new char[(int) length];
            for (int i = 0; i < stringChars.length; i++)
            {
                stringChars[i] = (char) stringBytes[i];
            }

            return new String(stringChars);
        }
    }

    public static byte[] readLongstr(ByteBuffer buffer)
    {
        long length = buffer.getUnsignedInt();
        if (length == 0)
        {
            return null;
        }
        else
        {
            byte[] result = new byte[(int) length];
            buffer.get(result);

            return result;
        }
    }

    public static long readTimestamp(ByteBuffer buffer)
    {
        // Discard msb from AMQ timestamp
        // buffer.getUnsignedInt();
        return buffer.getLong();
    }

    static byte[] hexToByteArray(String id)
    {
        // Should check param for null, long enough for this check, upper-case and trailing char
        String s = (id.charAt(1) == 'x') ? id.substring(2) : id; // strip 0x

        int len = s.length();
        int byte_len = len / 2;
        byte[] b = new byte[byte_len];

        for (int i = 0; i < byte_len; i++)
        {
            // fixme: refine these repetitive subscript calcs.
            int ch = i * 2;

            byte b1 = Byte.parseByte(s.substring(ch, ch + 1), 16);
            byte b2 = Byte.parseByte(s.substring(ch + 1, ch + 2), 16);

            b[i] = (byte) ((b1 * 16) + b2);
        }

        return (b);
    }

    public static char[] convertToHexCharArray(byte[] from)
    {
        int length = from.length;
        char[] result_buff = new char[(length * 2) + 2];

        result_buff[0] = '0';
        result_buff[1] = 'x';

        int bite;
        int dest = 2;

        for (int i = 0; i < length; i++)
        {
            bite = from[i];

            if (bite < 0)
            {
                bite += 256;
            }

            result_buff[dest++] = hex_chars[bite >> 4];
            result_buff[dest++] = hex_chars[bite & 0x0f];
        }

        return (result_buff);
    }

    public static String convertToHexString(byte[] from)
    {
        return (new String(convertToHexCharArray(from)));
    }

    public static String convertToHexString(ByteBuffer bb)
    {
        int size = bb.limit();

        byte[] from = new byte[size];

        // Is this not the same.
        // bb.get(from, 0, length);
        for (int i = 0; i < size; i++)
        {
            from[i] = bb.get(i);
        }

        return (new String(convertToHexCharArray(from)));
    }

    private static char[] hex_chars = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

    // **** new methods

    // AMQP_BOOLEAN_PROPERTY_PREFIX

    public static void writeBoolean(ByteBuffer buffer, Boolean aBoolean)
    {
        buffer.put((byte) (aBoolean ? 1 : 0));
    }

    public static boolean readBoolean(ByteBuffer buffer)
    {
        byte packedValue = buffer.get();

        return (packedValue == 1);
    }

    public static int encodedBooleanLength()
    {
        return 1;
    }

    // AMQP_BYTE_PROPERTY_PREFIX
    public static void writeByte(ByteBuffer buffer, Byte aByte)
    {
        buffer.put(aByte);
    }

    public static byte readByte(ByteBuffer buffer)
    {
        return buffer.get();
    }

    public static int encodedByteLength()
    {
        return 1;
    }

    // AMQP_SHORT_PROPERTY_PREFIX
    public static void writeShort(ByteBuffer buffer, Short aShort)
    {
        buffer.putShort(aShort);
    }

    public static short readShort(ByteBuffer buffer)
    {
        return buffer.getShort();
    }

    public static int encodedShortLength()
    {
        return 2;
    }

    // INTEGER_PROPERTY_PREFIX
    public static void writeInteger(ByteBuffer buffer, Integer aInteger)
    {
        buffer.putInt(aInteger);
    }

    public static int readInteger(ByteBuffer buffer)
    {
        return buffer.getInt();
    }

    public static int encodedIntegerLength()
    {
        return 4;
    }

    // AMQP_LONG_PROPERTY_PREFIX
    public static void writeLong(ByteBuffer buffer, Long aLong)
    {
        buffer.putLong(aLong);
    }

    public static long readLong(ByteBuffer buffer)
    {
        return buffer.getLong();
    }

    public static int encodedLongLength()
    {
        return 8;
    }

    // Float_PROPERTY_PREFIX
    public static void writeFloat(ByteBuffer buffer, Float aFloat)
    {
        buffer.putFloat(aFloat);
    }

    public static float readFloat(ByteBuffer buffer)
    {
        return buffer.getFloat();
    }

    public static int encodedFloatLength()
    {
        return 4;
    }

    // Double_PROPERTY_PREFIX
    public static void writeDouble(ByteBuffer buffer, Double aDouble)
    {
        buffer.putDouble(aDouble);
    }

    public static double readDouble(ByteBuffer buffer)
    {
        return buffer.getDouble();
    }

    public static int encodedDoubleLength()
    {
        return 8;
    }

    public static byte[] readBytes(ByteBuffer buffer)
    {
        short length = buffer.getUnsigned();
        if (length == 0)
        {
            return null;
        }
        else
        {
            byte[] dataBytes = new byte[length];
            buffer.get(dataBytes, 0, length);

            return dataBytes;
        }
    }

    public static void writeBytes(ByteBuffer buffer, byte[] data)
    {
        if (data != null)
        {
            // TODO: check length fits in an unsigned byte
            writeUnsignedByte(buffer, (short) data.length);
            buffer.put(data);
        }
        else
        {
            // really writing out unsigned byte
            buffer.put((byte) 0);
        }
    }

    // CHAR_PROPERTY
    public static int encodedCharLength()
    {
        return encodedByteLength();
    }

    public static char readChar(ByteBuffer buffer)
    {
        // This is valid as we know that the Character is ASCII 0..127
        return (char) buffer.get();
    }

    public static void writeChar(ByteBuffer buffer, char character)
    {
        // This is valid as we know that the Character is ASCII 0..127
        writeByte(buffer, (byte) character);
    }

    public static long readLongAsShortString(ByteBuffer buffer)
    {
        short length = buffer.getUnsigned();
        short pos = 0;
        if (length == 0)
        {
            return 0L;
        }

        byte digit = buffer.get();
        boolean isNegative;
        long result = 0;
        if (digit == (byte) '-')
        {
            isNegative = true;
            pos++;
            digit = buffer.get();
        }
        else
        {
            isNegative = false;
        }

        result = digit - (byte) '0';
        pos++;

        while (pos < length)
        {
            pos++;
            digit = buffer.get();
            result = (result << 3) + (result << 1);
            result += digit - (byte) '0';
        }

        return result;
    }

    public static long readUnsignedInteger(ByteBuffer buffer)
    {
        long l = 0xFF & buffer.get();
        l <<= 8;
        l = l | (0xFF & buffer.get());
        l <<= 8;
        l = l | (0xFF & buffer.get());
        l <<= 8;
        l = l | (0xFF & buffer.get());

        return l;
    }

}
