// Copyright (c) 2025 Mahmoud Alghalayini. All rights reserved.
//
// 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.

'use strict';

const { ValidationError } = require('./errors.js');

/**
 * Check if a value is a TypedArray
 * @param {*} value - The value to check
 * @returns {boolean} True if the value is a TypedArray
 */
function isTypedArray(value) {
  return ArrayBuffer.isView(value) && !(value instanceof DataView);
}

/**
 * Check if a value needs JSON conversion (BigInt, functions, etc.)
 * @param {*} value - The value to check
 * @returns {boolean} True if the value needs special JSON handling
 */
function needsJSONConversion(value) {
  return (
    typeof value === 'bigint' ||
    typeof value === 'function' ||
    typeof value === 'undefined' ||
    value === Infinity ||
    value === -Infinity ||
    (typeof value === 'number' && isNaN(value))
  );
}

/**
 * Convert a message to plain arrays (TypedArray -> regular Array)
 * @param {*} obj - The object to convert
 * @returns {*} The converted object with plain arrays
 */
function toPlainArrays(obj) {
  if (obj === null || obj === undefined) {
    return obj;
  }

  if (isTypedArray(obj)) {
    return Array.from(obj);
  }

  if (Array.isArray(obj)) {
    return obj.map((item) => toPlainArrays(item));
  }

  if (typeof obj === 'object' && obj !== null) {
    const result = {};
    for (const key in obj) {
      if (Object.hasOwn(obj, key)) {
        result[key] = toPlainArrays(obj[key]);
      }
    }
    return result;
  }

  return obj;
}

/**
 * Convert a message to be fully JSON-safe
 * @param {*} obj - The object to convert
 * @returns {*} The JSON-safe converted object
 */
function toJSONSafe(obj) {
  if (obj === null || obj === undefined) {
    return obj;
  }

  if (isTypedArray(obj)) {
    return Array.from(obj).map((item) => toJSONSafe(item));
  }

  if (needsJSONConversion(obj)) {
    if (typeof obj === 'bigint') {
      // Convert BigInt to string with 'n' suffix to indicate it was a BigInt
      return obj.toString() + 'n';
    }
    if (obj === Infinity) return 'Infinity';
    if (obj === -Infinity) return '-Infinity';
    if (typeof obj === 'number' && isNaN(obj)) return 'NaN';
    if (typeof obj === 'undefined') return null;
    if (typeof obj === 'function') return '[Function]';
  }

  if (Array.isArray(obj)) {
    return obj.map((item) => toJSONSafe(item));
  }

  if (typeof obj === 'object' && obj !== null) {
    const result = {};
    for (const key in obj) {
      if (Object.hasOwn(obj, key)) {
        result[key] = toJSONSafe(obj[key]);
      }
    }
    return result;
  }

  return obj;
}

/**
 * Convert a message to a JSON string
 * @param {*} obj - The object to convert
 * @param {number} [space] - Space parameter for JSON.stringify formatting
 * @returns {string} The JSON string representation
 */
function toJSONString(obj, space) {
  const jsonSafeObj = toJSONSafe(obj);
  return JSON.stringify(jsonSafeObj, null, space);
}

/**
 * Apply serialization mode conversion to a message object
 * @param {*} message - The message object to convert
 * @param {string} serializationMode - The serialization mode ('default', 'plain', 'json')
 * @returns {*} The converted message
 */
function applySerializationMode(message, serializationMode) {
  switch (serializationMode) {
    case 'default':
      // No conversion needed - use native rclnodejs behavior
      return message;

    case 'plain':
      // Convert TypedArrays to regular arrays
      return toPlainArrays(message);

    case 'json':
      // Convert to fully JSON-safe format
      return toJSONSafe(message);

    default:
      throw new ValidationError(
        `Invalid serializationMode: ${serializationMode}. Valid modes are: 'default', 'plain', 'json'`,
        {
          code: 'INVALID_SERIALIZATION_MODE',
          argumentName: 'serializationMode',
          providedValue: serializationMode,
          expectedType: "'default' | 'plain' | 'json'",
        }
      );
  }
}

/**
 * Validate serialization mode
 * @param {string} mode - The serialization mode to validate
 * @returns {boolean} True if valid
 */
function isValidSerializationMode(mode) {
  return ['default', 'plain', 'json'].includes(mode);
}

/**
 * Inverse of {@link toJSONSafe} for 64-bit integer fields.
 *
 * `toJSONSafe` encodes `bigint` as the string `"<n>n"` so values survive
 * `JSON.stringify`. `reviveBigInts` walks an arbitrary JSON value and
 * converts any such string back into a real `bigint`. Everything else
 * passes through unchanged. Used by the rosocket gateway and the Web
 * Runtime dispatcher to rehydrate inbound JSON before handing it to
 * rclnodejs.
 *
 * Returned objects use a null prototype and skip the well-known
 * prototype-pollution keys (`__proto__`, `constructor`, `prototype`)
 * because the input is attacker-controllable JSON arriving from a
 * remote peer.
 *
 * @param {*} value
 * @returns {*}
 */
function reviveBigInts(value) {
  if (value === null || typeof value !== 'object') {
    if (typeof value === 'string' && /^-?\d+n$/.test(value)) {
      return BigInt(value.slice(0, -1));
    }
    return value;
  }
  if (Array.isArray(value)) return value.map(reviveBigInts);
  const out = Object.create(null);
  for (const k of Object.keys(value)) {
    if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
    out[k] = reviveBigInts(value[k]);
  }
  return out;
}

module.exports = {
  isTypedArray,
  needsJSONConversion,
  toPlainArrays,
  toJSONSafe,
  toJSONString,
  applySerializationMode,
  isValidSerializationMode,
  reviveBigInts,
};