// Copyright (C) 2022 The Android Open Source Project
//
// Licensed 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.

import {length as utf8Len, write as utf8Write} from '@protobufjs/utf8';
import {assertTrue} from '../base/logging';
import {isString} from '../base/object_utils';

// A token that can be appended to an `ArrayBufferBuilder`.
export type ArrayBufferToken = string | number | Uint8Array;

// Return the length, in bytes, of a token to be inserted.
function tokenLength(token: ArrayBufferToken): number {
  if (isString(token)) {
    return utf8Len(token);
  } else if (token instanceof Uint8Array) {
    return token.byteLength;
  } else {
    assertTrue(token >= 0 && token <= 0xffffffff);
    // 32-bit integers take 4 bytes
    return 4;
  }
}

// Insert a token into the buffer, at position `byteOffset`.
//
// @param dataView A DataView into the buffer to write into.
// @param typedArray A Uint8Array view into the buffer to write into.
// @param byteOffset Position to write at, in the buffer.
// @param token Token to insert into the buffer.
function insertToken(
  dataView: DataView,
  typedArray: Uint8Array,
  byteOffset: number,
  token: ArrayBufferToken,
): void {
  if (isString(token)) {
    // Encode the string in UTF-8
    const written = utf8Write(token, typedArray, byteOffset);
    assertTrue(written === utf8Len(token));
  } else if (token instanceof Uint8Array) {
    // Copy the bytes from the other array
    typedArray.set(token, byteOffset);
  } else {
    assertTrue(token >= 0 && token <= 0xffffffff);
    // 32-bit little-endian value
    dataView.setUint32(byteOffset, token, true);
  }
}

// Like a string builder, but for an ArrayBuffer instead of a string. This
// allows us to assemble messages to send/receive over the wire. Data can be
// appended to the buffer using `append()`. The data we append can be of the
// following types:
//
// - string: the ASCII string is appended. Throws an error if there are
//           non-ASCII characters.
// - number: the number is appended as a 32-bit little-endian integer.
// - Uint8Array: the bytes are appended as-is to the buffer.
export class ArrayBufferBuilder {
  private readonly tokens: ArrayBufferToken[] = [];

  // Return an `ArrayBuffer` that is the concatenation of all the tokens.
  toArrayBuffer(): ArrayBuffer {
    // Calculate the size of the buffer we need.
    let byteLength = 0;
    for (const token of this.tokens) {
      byteLength += tokenLength(token);
    }
    // Allocate the buffer.
    const buffer = new ArrayBuffer(byteLength);
    const dataView = new DataView(buffer);
    const typedArray = new Uint8Array(buffer);
    // Fill the buffer with the tokens.
    let byteOffset = 0;
    for (const token of this.tokens) {
      insertToken(dataView, typedArray, byteOffset, token);
      byteOffset += tokenLength(token);
    }
    assertTrue(byteOffset === byteLength);
    // Return the values.
    return buffer;
  }

  // Add one or more tokens to the value of this object.
  append(token: ArrayBufferToken): void {
    this.tokens.push(token);
  }
}
