/*
 * 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.hadoop.hbase.io.util;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import org.apache.hadoop.hbase.nio.ByteBuff;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.yetus.audience.InterfaceAudience;

import org.apache.hbase.thirdparty.com.google.common.base.Preconditions;

/*
 * It seems like as soon as somebody sets himself to the task of creating VInt encoding, his mind
 * blanks out for a split-second and he starts the work by wrapping it in the most convoluted
 * interface he can come up with. Custom streams that allocate memory, DataOutput that is only used
 * to write single bytes... We operate on simple streams. Thus, we are going to have a simple
 * implementation copy-pasted from protobuf Coded*Stream.
 */
@InterfaceAudience.Private
public class StreamUtils {

  public static void writeRawVInt32(OutputStream output, int value) throws IOException {
    while (true) {
      if ((value & ~0x7F) == 0) {
        output.write(value);
        return;
      } else {
        output.write((value & 0x7F) | 0x80);
        value >>>= 7;
      }
    }
  }

  public static int readRawVarint32(InputStream input) throws IOException {
    byte tmp = (byte) input.read();
    if (tmp >= 0) {
      return tmp;
    }
    int result = tmp & 0x7f;
    if ((tmp = (byte) input.read()) >= 0) {
      result |= tmp << 7;
    } else {
      result |= (tmp & 0x7f) << 7;
      if ((tmp = (byte) input.read()) >= 0) {
        result |= tmp << 14;
      } else {
        result |= (tmp & 0x7f) << 14;
        if ((tmp = (byte) input.read()) >= 0) {
          result |= tmp << 21;
        } else {
          result |= (tmp & 0x7f) << 21;
          result |= (tmp = (byte) input.read()) << 28;
          if (tmp < 0) {
            // Discard upper 32 bits.
            for (int i = 0; i < 5; i++) {
              if (input.read() >= 0) {
                return result;
              }
            }
            throw new IOException("Malformed varint");
          }
        }
      }
    }
    return result;
  }

  public static int readRawVarint32(ByteBuff input) throws IOException {
    byte tmp = input.get();
    if (tmp >= 0) {
      return tmp;
    }
    int result = tmp & 0x7f;
    if ((tmp = input.get()) >= 0) {
      result |= tmp << 7;
    } else {
      result |= (tmp & 0x7f) << 7;
      if ((tmp = input.get()) >= 0) {
        result |= tmp << 14;
      } else {
        result |= (tmp & 0x7f) << 14;
        if ((tmp = input.get()) >= 0) {
          result |= tmp << 21;
        } else {
          result |= (tmp & 0x7f) << 21;
          result |= (tmp = input.get()) << 28;
          if (tmp < 0) {
            // Discard upper 32 bits.
            for (int i = 0; i < 5; i++) {
              if (input.get() >= 0) {
                return result;
              }
            }
            throw new IOException("Malformed varint");
          }
        }
      }
    }
    return result;
  }

  /**
   * Reads a varInt value stored in an array. Input array where the varInt is available Offset in
   * the input array where varInt is available
   * @return A pair of integers in which first value is the actual decoded varInt value and second
   *         value as number of bytes taken by this varInt for it's storage in the input array.
   * @throws IOException When varint is malformed and not able to be read correctly
   */
  public static Pair<Integer, Integer> readRawVarint32(byte[] input, int offset)
    throws IOException {
    int newOffset = offset;
    byte tmp = input[newOffset++];
    if (tmp >= 0) {
      return new Pair<>((int) tmp, newOffset - offset);
    }
    int result = tmp & 0x7f;
    tmp = input[newOffset++];
    if (tmp >= 0) {
      result |= tmp << 7;
    } else {
      result |= (tmp & 0x7f) << 7;
      tmp = input[newOffset++];
      if (tmp >= 0) {
        result |= tmp << 14;
      } else {
        result |= (tmp & 0x7f) << 14;
        tmp = input[newOffset++];
        if (tmp >= 0) {
          result |= tmp << 21;
        } else {
          result |= (tmp & 0x7f) << 21;
          tmp = input[newOffset++];
          result |= tmp << 28;
          if (tmp < 0) {
            // Discard upper 32 bits.
            for (int i = 0; i < 5; i++) {
              tmp = input[newOffset++];
              if (tmp >= 0) {
                return new Pair<>(result, newOffset - offset);
              }
            }
            throw new IOException("Malformed varint");
          }
        }
      }
    }
    return new Pair<>(result, newOffset - offset);
  }

  public static Pair<Integer, Integer> readRawVarint32(ByteBuffer input, int offset)
    throws IOException {
    int newOffset = offset;
    byte tmp = input.get(newOffset++);
    if (tmp >= 0) {
      return new Pair<>((int) tmp, newOffset - offset);
    }
    int result = tmp & 0x7f;
    tmp = input.get(newOffset++);
    if (tmp >= 0) {
      result |= tmp << 7;
    } else {
      result |= (tmp & 0x7f) << 7;
      tmp = input.get(newOffset++);
      if (tmp >= 0) {
        result |= tmp << 14;
      } else {
        result |= (tmp & 0x7f) << 14;
        tmp = input.get(newOffset++);
        if (tmp >= 0) {
          result |= tmp << 21;
        } else {
          result |= (tmp & 0x7f) << 21;
          tmp = input.get(newOffset++);
          result |= tmp << 28;
          if (tmp < 0) {
            // Discard upper 32 bits.
            for (int i = 0; i < 5; i++) {
              tmp = input.get(newOffset++);
              if (tmp >= 0) {
                return new Pair<>(result, newOffset - offset);
              }
            }
            throw new IOException("Malformed varint");
          }
        }
      }
    }
    return new Pair<>(result, newOffset - offset);
  }

  /**
   * Read a byte from the given stream using the read method, and throw EOFException if it returns
   * -1, like the implementation in {@code DataInputStream}.
   * <p/>
   * This is useful because casting the return value of read method into byte directly will make us
   * lose the ability to check whether there is a byte and its value is -1 or we reach EOF, as
   * casting int -1 to byte also returns -1.
   */
  public static byte readByte(InputStream in) throws IOException {
    int r = in.read();
    if (r < 0) {
      throw new EOFException();
    }
    return (byte) r;
  }

  public static short toShort(byte hi, byte lo) {
    short s = (short) (((hi & 0xFF) << 8) | (lo & 0xFF));
    Preconditions.checkArgument(s >= 0);
    return s;
  }

  public static void writeShort(OutputStream out, short v) throws IOException {
    Preconditions.checkArgument(v >= 0);
    out.write((byte) (0xff & (v >> 8)));
    out.write((byte) (0xff & v));
  }

  public static void writeInt(OutputStream out, int v) throws IOException {
    out.write((byte) (0xff & (v >> 24)));
    out.write((byte) (0xff & (v >> 16)));
    out.write((byte) (0xff & (v >> 8)));
    out.write((byte) (0xff & v));
  }

  public static void writeLong(OutputStream out, long v) throws IOException {
    out.write((byte) (0xff & (v >> 56)));
    out.write((byte) (0xff & (v >> 48)));
    out.write((byte) (0xff & (v >> 40)));
    out.write((byte) (0xff & (v >> 32)));
    out.write((byte) (0xff & (v >> 24)));
    out.write((byte) (0xff & (v >> 16)));
    out.write((byte) (0xff & (v >> 8)));
    out.write((byte) (0xff & v));
  }

  public static long readLong(InputStream in) throws IOException {
    long result = 0;
    for (int shift = 56; shift >= 0; shift -= 8) {
      long x = in.read();
      if (x < 0) throw new IOException("EOF");
      result |= (x << shift);
    }
    return result;
  }
}
