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

import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;
import java.nio.ByteBuffer;
import java.util.Arrays;

/** Used to write values. */
class OutputBuffer extends ByteArrayOutputStream {
  static final int BLOCK_SIZE = 64 * 1024;

  private int bitCount;                           // position in booleans

  public OutputBuffer() { super(BLOCK_SIZE + BLOCK_SIZE >> 2); }

  public boolean isFull() { return size() >= BLOCK_SIZE; }

  public ByteBuffer asByteBuffer() { return ByteBuffer.wrap(buf, 0, count); }

  public void writeValue(Object value, ValueType type)
    throws IOException {
    switch (type) {
    case NULL:
                                              break;
    case BOOLEAN:
      writeBoolean((Boolean)value);           break;
    case INT:
      writeInt((Integer)value);               break;
    case LONG:
      writeLong((Long)value);                 break;
    case FIXED32:
      writeFixed32((Integer)value);           break;
    case FIXED64:
      writeFixed64((Long)value);              break;
    case FLOAT:
      writeFloat((Float)value);               break;
    case DOUBLE:
      writeDouble((Double)value);             break;
    case STRING:
      writeString((String)value);             break;
    case BYTES:
      if (value instanceof ByteBuffer)
        writeBytes((ByteBuffer)value);
      else
        writeBytes((byte[])value);
      break;
    default:
      throw new TrevniRuntimeException("Unknown value type: "+type);
    }
  }

  public void writeBoolean(boolean value) {
    if (bitCount == 0) {                           // first bool in byte
      ensure(1);
      count++;
    }
    if (value)
      buf[count-1] |= (byte)(1 << bitCount);
    bitCount++;
    if (bitCount == 8)
      bitCount = 0;
  }

  public void writeLength(int length) throws IOException {
    bitCount = 0;
    writeInt(length);
  }

  private static final Charset UTF8 = Charset.forName("UTF-8");

  public void writeString(String string) throws IOException {
    byte[] bytes = string.getBytes(UTF8);
    writeInt(bytes.length);
    write(bytes, 0, bytes.length);
  }

  public void writeBytes(ByteBuffer bytes) throws IOException {
    int pos = bytes.position();
    int start = bytes.arrayOffset() + pos;
    int len = bytes.limit() - pos;
    writeBytes(bytes.array(), start, len);
  }

  public void writeBytes(byte[] bytes) throws IOException {
    writeBytes(bytes, 0, bytes.length);
  }

  public void writeBytes(byte[] bytes, int start, int len) throws IOException {
    writeInt(len);
    write(bytes, start, len);
  }

  public void writeFloat(float f) throws IOException {
    writeFixed32(Float.floatToRawIntBits(f));
  }

  public void writeDouble(double d) throws IOException {
    writeFixed64(Double.doubleToRawLongBits(d));
  }

  public void writeFixed32(int i) throws IOException {
    ensure(4);
    buf[count  ] = (byte)((i       ) & 0xFF);
    buf[count+1] = (byte)((i >>>  8) & 0xFF);
    buf[count+2] = (byte)((i >>> 16) & 0xFF);
    buf[count+3] = (byte)((i >>> 24) & 0xFF);
    count += 4;
  }

  public void writeFixed64(long l) throws IOException {
    ensure(8);
    int first = (int)(l & 0xFFFFFFFF);
    int second = (int)((l >>> 32) & 0xFFFFFFFF);
    buf[count  ] = (byte)((first        ) & 0xFF);
    buf[count+4] = (byte)((second       ) & 0xFF);
    buf[count+5] = (byte)((second >>>  8) & 0xFF);
    buf[count+1] = (byte)((first >>>   8) & 0xFF);
    buf[count+2] = (byte)((first >>>  16) & 0xFF);
    buf[count+6] = (byte)((second >>> 16) & 0xFF);
    buf[count+7] = (byte)((second >>> 24) & 0xFF);
    buf[count+3] = (byte)((first >>>  24) & 0xFF);
    count += 8;
  }

  public void writeInt(int n) throws IOException {
    ensure(5);
    n = (n << 1) ^ (n >> 31);                     // move sign to low-order bit
    if ((n & ~0x7F) != 0) {
      buf[count++] = (byte)((n | 0x80) & 0xFF);
      n >>>= 7;
      if (n > 0x7F) {
        buf[count++] = (byte)((n | 0x80) & 0xFF);
        n >>>= 7;
        if (n > 0x7F) {
          buf[count++] = (byte)((n | 0x80) & 0xFF);
          n >>>= 7;
          if (n > 0x7F) {
            buf[count++] = (byte)((n | 0x80) & 0xFF);
            n >>>= 7;
          }
        }
      }
    }
    buf[count++] = (byte) n;
  }

  public void writeLong(long n) throws IOException {
    ensure(10);
    n = (n << 1) ^ (n >> 63);                     // move sign to low-order bit
    if ((n & ~0x7FL) != 0) {
      buf[count++] = (byte)((n | 0x80) & 0xFF);
      n >>>= 7;
      if (n > 0x7F) {
        buf[count++] = (byte)((n | 0x80) & 0xFF);
        n >>>= 7;
        if (n > 0x7F) {
          buf[count++] = (byte)((n | 0x80) & 0xFF);
          n >>>= 7;
          if (n > 0x7F) {
            buf[count++] = (byte)((n | 0x80) & 0xFF);
            n >>>= 7;
            if (n > 0x7F) {
              buf[count++] = (byte)((n | 0x80) & 0xFF);
              n >>>= 7;
              if (n > 0x7F) {
                buf[count++] = (byte)((n | 0x80) & 0xFF);
                n >>>= 7;
                if (n > 0x7F) {
                  buf[count++] = (byte)((n | 0x80) & 0xFF);
                  n >>>= 7;
                  if (n > 0x7F) {
                    buf[count++] = (byte)((n | 0x80) & 0xFF);
                    n >>>= 7;
                    if (n > 0x7F) {
                      buf[count++] = (byte)((n | 0x80) & 0xFF);
                      n >>>= 7;
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
    buf[count++] = (byte) n;
  }

  private void ensure(int n) {
    if (count + n > buf.length)
      buf = Arrays.copyOf(buf, Math.max(buf.length << 1, count + n));
  }

  public static int size(Object value, ValueType type) {
    switch (type) {
    case NULL:
      return 0;
    case INT:
      return size((Integer)value);
    case LONG:
      return size((Long)value);
    case FIXED32:
    case FLOAT:
      return 4;
    case FIXED64:
    case DOUBLE:
      return 8;
    case STRING:
      return size((String)value);
    case BYTES:
      if (value instanceof ByteBuffer)
        return size((ByteBuffer)value);
      return size((byte[])value);
    default:
      throw new TrevniRuntimeException("Unknown value type: "+type);
    }
  }

  public static int size(int n) {
    n = (n << 1) ^ (n >> 31);                     // move sign to low-order bit
    if (n <= (1<<7*1)-1)
      return 1;
    if (n <= (1<<7*2)-1)
      return 2;
    if (n <= (1<<7*3)-1)
      return 3;
    if (n <= (1<<7*4)-1)
      return 4;
    return 5;
  }

  public static int size(long n) {
    n = (n << 1) ^ (n >> 63);                     // move sign to low-order bit
    if (n <= (1<<7*1)-1)
      return 1;
    if (n <= (1<<7*2)-1)
      return 2;
    if (n <= (1<<7*3)-1)
      return 3;
    if (n <= (1<<7*4)-1)
      return 4;
    if (n <= (1<<7*5)-1)
      return 5;
    if (n <= (1<<7*6)-1)
      return 6;
    if (n <= (1<<7*7)-1)
      return 7;
    if (n <= (1<<7*8)-1)
      return 8;
    if (n <= (1<<7*9)-1)
      return 9;
    return 10;
  }

  public static int size(ByteBuffer bytes) {
    int length = bytes.remaining();
    return size(length) + length;
  }

  public static int size(byte[] bytes) {
    int length = bytes.length;
    return size(length) + length;
  }

  public static int size(String string) {
    int length = utf8Length(string);
    return size(length) + length;
  }

  private static int utf8Length(String string) {
    int stringLength = string.length();
    int utf8Length = 0;
    for (int i = 0; i < stringLength; i++) {
      char c = string.charAt(i);
      int p = c;                                  // code point
      if (Character.isHighSurrogate(c)            // surrogate pair
          && i != stringLength-1
          && Character.isLowSurrogate(string.charAt(i+1))) {
        p = string.codePointAt(i);
        i++;
      }
      if (p <= 0x007F) {
        utf8Length += 1;
      } else if (p <= 0x07FF) {
        utf8Length += 2;
      } else if (p <= 0x0FFFF) {
        utf8Length += 3;
      } else if (p <= 0x01FFFFF) {
        utf8Length += 4;
      } else if (p <= 0x03FFFFFF) {
        utf8Length += 5;
      } else {
        utf8Length += 6;
      }
    }
    return utf8Length;
  }

}
