// Copyright 2022 The Pigweed Authors
//
// 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
//
//     https://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.

/** Decodes arguments and formats them with the provided format string. */
import Long from 'long';

const SPECIFIER_REGEX =
  /%(\.([0-9]+))?(hh|h|ll|l|j|z|t|L)?([%csdioxXufFeEaAgGnp])/g;
// Conversion specifiers by type; n is not supported.
const SIGNED_INT = 'di'.split('');
const UNSIGNED_INT = 'oxXup'.split('');
const FLOATING_POINT = 'fFeEaAgG'.split('');

enum DecodedStatusFlags {
  // Status flags for a decoded argument. These values should match the
  // DecodingStatus enum in pw_tokenizer/internal/decode.h.
  OK = 0, // decoding was successful
  MISSING = 1, // the argument was not present in the data
  TRUNCATED = 2, // the argument was truncated during encoding
  DECODE_ERROR = 4, // an error occurred while decoding the argument
  SKIPPED = 8, // argument was skipped due to a previous error
}

interface DecodedArg {
  size: number;
  value: string | number | Long | null;
}

// ZigZag decode function from protobuf's wire_format module.
function zigzagDecode(value: Long, unsigned = false): Long {
  // 64 bit math is:
  //   signmask = (zigzag & 1) ? -1 : 0;
  //   twosComplement = (zigzag >> 1) ^ signmask;
  //
  // To work with 32 bit, we can operate on both but "carry" the lowest bit
  // from the high word by shifting it up 31 bits to be the most significant bit
  // of the low word.
  let bitsLow = value.low,
    bitsHigh = value.high;
  const signFlipMask = -(bitsLow & 1);
  bitsLow = ((bitsLow >>> 1) | (bitsHigh << 31)) ^ signFlipMask;
  bitsHigh = (bitsHigh >>> 1) ^ signFlipMask;
  return new Long(bitsLow, bitsHigh, unsigned);
}

export class PrintfDecoder {
  // Reads a unicode string from the encoded data.
  private decodeString(args: Uint8Array): DecodedArg {
    if (args.length === 0) return { size: 0, value: null };
    let sizeAndStatus = args[0];
    let status = DecodedStatusFlags.OK;

    if (sizeAndStatus & 0x80) {
      status |= DecodedStatusFlags.TRUNCATED;
      sizeAndStatus &= 0x7f;
    }

    const rawData = args.slice(0, sizeAndStatus + 1);
    const data = rawData.slice(1);
    if (data.length < sizeAndStatus) {
      status |= DecodedStatusFlags.DECODE_ERROR;
    }

    const decoded = new TextDecoder().decode(data);
    return { size: rawData.length, value: decoded };
  }

  private decodeSignedInt(args: Uint8Array): DecodedArg {
    return this._decodeInt(args);
  }

  private _decodeInt(args: Uint8Array, unsigned = false): DecodedArg {
    if (args.length === 0) return { size: 0, value: null };
    let count = 0;
    let result = new Long(0);
    let shift = 0;
    for (count = 0; count < args.length; count++) {
      const byte = args[count];
      result = result.or(
        Long.fromInt(byte, unsigned).and(0x7f).shiftLeft(shift),
      );
      if (!(byte & 0x80)) {
        return { value: zigzagDecode(result, unsigned), size: count + 1 };
      }
      shift += 7;
      if (shift >= 64) break;
    }

    return { size: 0, value: null };
  }

  private decodeUnsignedInt(
    args: Uint8Array,
    lengthSpecifier: string,
  ): DecodedArg {
    const arg = this._decodeInt(args, true);
    const bits = ['ll', 'j'].indexOf(lengthSpecifier) !== -1 ? 64 : 32;

    // Since ZigZag encoding is used, unsigned integers must be masked off to
    // their original bit length.
    if (arg.value !== null) {
      let num = arg.value as Long;
      if (bits === 32) {
        num = num.and(Long.fromInt(1).shiftLeft(bits).add(-1));
      } else {
        num = num.and(-1);
      }
      arg.value = num.toString();
    }
    return arg;
  }

  private decodeChar(args: Uint8Array): DecodedArg {
    const arg = this.decodeSignedInt(args);

    if (arg.value !== null) {
      const num = arg.value as Long;
      arg.value = String.fromCharCode(num.toInt());
    }
    return arg;
  }

  private decodeFloat(args: Uint8Array, precision: string): DecodedArg {
    if (args.length < 4) return { size: 0, value: '' };
    const floatValue = new DataView(args.buffer, args.byteOffset, 4).getFloat32(
      0,
      true,
    );
    if (precision)
      return { size: 4, value: floatValue.toFixed(parseInt(precision)) };
    return { size: 4, value: floatValue };
  }

  private format(
    specifierType: string,
    args: Uint8Array,
    precision: string,
    lengthSpecifier: string,
  ): DecodedArg {
    if (specifierType == '%') return { size: 0, value: '%' }; // literal %
    if (specifierType === 's') {
      return this.decodeString(args);
    }
    if (specifierType === 'c') {
      return this.decodeChar(args);
    }
    if (SIGNED_INT.indexOf(specifierType) !== -1) {
      return this.decodeSignedInt(args);
    }
    if (UNSIGNED_INT.indexOf(specifierType) !== -1) {
      return this.decodeUnsignedInt(args, lengthSpecifier);
    }
    if (FLOATING_POINT.indexOf(specifierType) !== -1) {
      return this.decodeFloat(args, precision);
    }

    // Unsupported specifier, return as-is
    return { size: 0, value: '%' + specifierType };
  }

  decode(formatString: string, args: Uint8Array): string {
    return formatString.replace(
      SPECIFIER_REGEX,
      (
        _specifier,
        _precisionFull,
        precision,
        lengthSpecifier,
        specifierType,
      ) => {
        const decodedArg = this.format(
          specifierType,
          args,
          precision,
          lengthSpecifier,
        );
        args = args.slice(decodedArg.size);
        if (decodedArg === null) return '';
        return String(decodedArg.value);
      },
    );
  }
}
