/*
 * Copyright The OpenTelemetry Authors
 * SPDX-License-Identifier: Apache-2.0
 */

// Includes work from:
// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc.  All rights reserved.
// https://developers.google.com/protocol-buffers/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package io.opentelemetry.exporter.internal.marshal;

import static io.opentelemetry.exporter.internal.marshal.WireFormat.FIXED32_SIZE;
import static io.opentelemetry.exporter.internal.marshal.WireFormat.FIXED64_SIZE;
import static io.opentelemetry.exporter.internal.marshal.WireFormat.MAX_VARINT_SIZE;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * Minimal copy of protobuf-java's CodedInputStream, currently only used in GrpcStatusUtil.
 *
 * <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
 * at any time.
 */
@SuppressWarnings("UnnecessaryFinal")
public final class CodedInputStream {

  private final byte[] buffer;
  private final int limit;
  private int pos;
  private int lastTag;

  /** Returns a new {@link CodedInputStream}. */
  public static CodedInputStream newInstance(byte[] serialized) {
    return new CodedInputStream(serialized);
  }

  private CodedInputStream(byte[] buffer) {
    this.buffer = buffer;
    limit = buffer.length;
    pos = 0;
  }

  /** Reads the next tag. */
  public int readTag() throws IOException {
    if (isAtEnd()) {
      lastTag = 0;
      return 0;
    }

    lastTag = readRawVarint32();
    if (WireFormat.getTagFieldNumber(lastTag) == 0) {
      // If we actually read zero (or any tag number corresponding to field
      // number zero), that's not a valid tag.
      throw new IOException("Invalid tag: " + lastTag);
    }
    return lastTag;
  }

  /** Reads a string field. */
  public String readStringRequireUtf8() throws IOException {
    final int size = readRawVarint32();
    if (size > 0 && size <= (limit - pos)) {
      String result = new String(buffer, pos, size, StandardCharsets.UTF_8);
      pos += size;
      return result;
    }

    if (size == 0) {
      return "";
    }
    if (size <= 0) {
      throw newNegativeException();
    }
    throw newTruncatedException();
  }

  /** Skips a field. */
  public boolean skipField(final int tag) throws IOException {
    switch (WireFormat.getTagWireType(tag)) {
      case WireFormat.WIRETYPE_VARINT:
        skipRawVarint();
        return true;
      case WireFormat.WIRETYPE_FIXED64:
        skipRawBytes(FIXED64_SIZE);
        return true;
      case WireFormat.WIRETYPE_LENGTH_DELIMITED:
        skipRawBytes(readRawVarint32());
        return true;
      case WireFormat.WIRETYPE_FIXED32:
        skipRawBytes(FIXED32_SIZE);
        return true;
      default:
        throw new IOException("Invalid wire type: " + tag);
    }
  }

  private boolean isAtEnd() {
    return pos == limit;
  }

  /** Read varint32. */
  @SuppressWarnings("LabelledBreakTarget")
  public int readRawVarint32() throws IOException {
    // See implementation notes for readRawVarint64
    fastpath:
    {
      int tempPos = pos;

      if (limit == tempPos) {
        break fastpath;
      }

      final byte[] buffer = this.buffer;
      int x;
      if ((x = buffer[tempPos++]) >= 0) {
        pos = tempPos;
        return x;
      } else if (limit - tempPos < 9) {
        break fastpath;
      } else if ((x ^= (buffer[tempPos++] << 7)) < 0) {
        x ^= (~0 << 7);
      } else if ((x ^= (buffer[tempPos++] << 14)) >= 0) {
        x ^= (~0 << 7) ^ (~0 << 14);
      } else if ((x ^= (buffer[tempPos++] << 21)) < 0) {
        x ^= (~0 << 7) ^ (~0 << 14) ^ (~0 << 21);
      } else {
        int y = buffer[tempPos++];
        x ^= y << 28;
        x ^= (~0 << 7) ^ (~0 << 14) ^ (~0 << 21) ^ (~0 << 28);
        if (y < 0
            && buffer[tempPos++] < 0
            && buffer[tempPos++] < 0
            && buffer[tempPos++] < 0
            && buffer[tempPos++] < 0
            && buffer[tempPos++] < 0) {
          break fastpath; // Will throw malformedVarint()
        }
      }
      pos = tempPos;
      return x;
    }
    return (int) readRawVarint64SlowPath();
  }

  private long readRawVarint64SlowPath() throws IOException {
    long result = 0;
    for (int shift = 0; shift < 64; shift += 7) {
      final byte b = readRawByte();
      result |= (long) (b & 0x7F) << shift;
      if ((b & 0x80) == 0) {
        return result;
      }
    }
    throw newMalformedVarintException();
  }

  private byte readRawByte() throws IOException {
    if (pos == limit) {
      throw newTruncatedException();
    }
    return buffer[pos++];
  }

  private void skipRawVarint() throws IOException {
    if (limit - pos >= MAX_VARINT_SIZE) {
      skipRawVarintFastPath();
    } else {
      skipRawVarintSlowPath();
    }
  }

  public double readDouble() throws IOException {
    return Double.longBitsToDouble(readRawLittleEndian64());
  }

  private long readRawLittleEndian64() throws IOException {
    int tempPos = pos;

    if (limit - tempPos < FIXED64_SIZE) {
      throw newTruncatedException();
    }

    final byte[] buffer = this.buffer;
    pos = tempPos + FIXED64_SIZE;
    return ((buffer[tempPos] & 0xffL)
        | ((buffer[tempPos + 1] & 0xffL) << 8)
        | ((buffer[tempPos + 2] & 0xffL) << 16)
        | ((buffer[tempPos + 3] & 0xffL) << 24)
        | ((buffer[tempPos + 4] & 0xffL) << 32)
        | ((buffer[tempPos + 5] & 0xffL) << 40)
        | ((buffer[tempPos + 6] & 0xffL) << 48)
        | ((buffer[tempPos + 7] & 0xffL) << 56));
  }

  private void skipRawVarintFastPath() throws IOException {
    for (int i = 0; i < MAX_VARINT_SIZE; i++) {
      if (buffer[pos++] >= 0) {
        return;
      }
    }
    throw newMalformedVarintException();
  }

  private void skipRawVarintSlowPath() throws IOException {
    for (int i = 0; i < MAX_VARINT_SIZE; i++) {
      if (readRawByte() >= 0) {
        return;
      }
    }
    throw newMalformedVarintException();
  }

  private void skipRawBytes(final int length) throws IOException {
    if (length >= 0 && length <= (limit - pos)) {
      // We have all the bytes we need already.
      pos += length;
      return;
    }

    if (length < 0) {
      throw newNegativeException();
    }
    throw newTruncatedException();
  }

  private static IOException newNegativeException() {
    return new IOException(
        "CodedInputStream encountered an embedded string or message "
            + "which claimed to have negative size.");
  }

  private static IOException newTruncatedException() {
    return new IOException(
        "While parsing a protocol message, the input ended unexpectedly "
            + "in the middle of a field.  This could mean either that the "
            + "input has been truncated or that an embedded message "
            + "misreported its own length.");
  }

  private static IOException newMalformedVarintException() {
    return new IOException("CodedInputStream encountered a malformed varint.");
  }
}
