// Copyright (c) 2026 The Robot Web Tools Contributors. 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 QoS = require('./qos.js');
const {
  Parameter,
  ParameterType,
  ParameterDescriptor,
} = require('./parameter.js');

/**
 * Enum of overridable QoS policy kinds.
 * Each value corresponds to a QoS property on the {@link QoS} class.
 * @enum {number}
 */
const QoSPolicyKind = Object.freeze({
  HISTORY: 1,
  DEPTH: 2,
  RELIABILITY: 3,
  DURABILITY: 4,
  LIVELINESS: 5,
  AVOID_ROS_NAMESPACE_CONVENTIONS: 6,
});

// Maps QoSPolicyKind -> { qosProp, paramKey, paramType, toParam, fromParam }
const POLICY_MAP = {
  [QoSPolicyKind.HISTORY]: {
    qosProp: 'history',
    paramKey: 'history',
    enumObj: QoS.HistoryPolicy,
    paramType: ParameterType.PARAMETER_STRING,
    toParam: (val, enumObj) => _enumToString(val, enumObj),
    fromParam: (val, enumObj) => _stringToEnum(val, enumObj),
  },
  [QoSPolicyKind.DEPTH]: {
    qosProp: 'depth',
    paramKey: 'depth',
    paramType: ParameterType.PARAMETER_INTEGER,
    toParam: (val) => BigInt(val),
    fromParam: (val) => Number(val),
  },
  [QoSPolicyKind.RELIABILITY]: {
    qosProp: 'reliability',
    paramKey: 'reliability',
    enumObj: QoS.ReliabilityPolicy,
    paramType: ParameterType.PARAMETER_STRING,
    toParam: (val, enumObj) => _enumToString(val, enumObj),
    fromParam: (val, enumObj) => _stringToEnum(val, enumObj),
  },
  [QoSPolicyKind.DURABILITY]: {
    qosProp: 'durability',
    paramKey: 'durability',
    enumObj: QoS.DurabilityPolicy,
    paramType: ParameterType.PARAMETER_STRING,
    toParam: (val, enumObj) => _enumToString(val, enumObj),
    fromParam: (val, enumObj) => _stringToEnum(val, enumObj),
  },
  [QoSPolicyKind.LIVELINESS]: {
    qosProp: 'liveliness',
    paramKey: 'liveliness',
    enumObj: QoS.LivelinessPolicy,
    paramType: ParameterType.PARAMETER_STRING,
    toParam: (val, enumObj) => _enumToString(val, enumObj),
    fromParam: (val, enumObj) => _stringToEnum(val, enumObj),
  },
  [QoSPolicyKind.AVOID_ROS_NAMESPACE_CONVENTIONS]: {
    qosProp: 'avoidRosNameSpaceConventions',
    paramKey: 'avoid_ros_namespace_conventions',
    paramType: ParameterType.PARAMETER_BOOL,
    toParam: (val) => val,
    fromParam: (val) => Boolean(val),
  },
};

/**
 * Convert a numeric enum value to a lowercase string name.
 * @param {number} val - enum numeric value
 * @param {Object} enumObj - the enum object (e.g. QoS.HistoryPolicy)
 * @returns {string}
 */
function _enumToString(val, enumObj) {
  for (const [key, v] of Object.entries(enumObj)) {
    if (v === val) {
      // Strip the RMW prefix: "RMW_QOS_POLICY_HISTORY_KEEP_LAST" -> "keep_last"
      const parts = key.split('_');
      // Find the index after the policy name (HISTORY, RELIABILITY, etc.)
      // Pattern: RMW_QOS_POLICY_<POLICY>_<VALUE>
      const policyNames = [
        'HISTORY',
        'RELIABILITY',
        'DURABILITY',
        'LIVELINESS',
      ];
      let valueStart = 4; // default: skip RMW_QOS_POLICY_<X>_
      for (let i = 3; i < parts.length; i++) {
        if (policyNames.includes(parts[i])) {
          valueStart = i + 1;
          break;
        }
      }
      return parts.slice(valueStart).join('_').toLowerCase();
    }
  }
  return String(val);
}

/**
 * Convert a lowercase string back to a numeric enum value.
 * @param {string} str - the string value (e.g. "keep_last")
 * @param {Object} enumObj - the enum object
 * @returns {number}
 */
function _stringToEnum(str, enumObj) {
  const upper = str.toUpperCase();
  for (const [key, val] of Object.entries(enumObj)) {
    if (key.endsWith('_' + upper)) {
      return val;
    }
  }
  throw new Error(`Unknown QoS policy value: "${str}"`);
}

/**
 * Options for overriding QoS policies via ROS parameters.
 *
 * When passed to `createPublisher()` or `createSubscription()`, the node
 * will declare read-only parameters for each specified policy kind. These
 * parameters can be set via command-line arguments, launch files, or
 * parameter files to override the QoS profile at startup.
 *
 * Parameter naming convention:
 *   `qos_overrides.<topic>.<publisher|subscription>[_<entityId>].<policy>`
 *
 * @example
 * // Override history, depth, and reliability via parameters
 * const sub = node.createSubscription(
 *   'std_msgs/msg/String', '/chatter',
 *   { qos: rclnodejs.QoS.profileDefault,
 *     qosOverridingOptions: QoSOverridingOptions.withDefaultPolicies() },
 *   (msg) => console.log(msg.data)
 * );
 * // Now you can override via CLI:
 * //   --ros-args -p "qos_overrides./chatter.subscription.depth:=20"
 */
class QoSOverridingOptions {
  /**
   * @param {Array<QoSPolicyKind>} policyKinds - Which QoS policies to expose as parameters.
   * @param {Object} [opts]
   * @param {function} [opts.callback] - Optional validation callback. Receives the
   *   final QoS profile after overrides are applied. Should return
   *   `{successful: true}` or `{successful: false, reason: '...'}`.
   * @param {string} [opts.entityId] - Optional suffix to disambiguate multiple
   *   publishers/subscriptions on the same topic.
   */
  constructor(policyKinds, opts = {}) {
    this._policyKinds = Array.from(policyKinds);
    this._callback = opts.callback || null;
    this._entityId = opts.entityId || null;
  }

  get policyKinds() {
    return this._policyKinds;
  }

  get callback() {
    return this._callback;
  }

  get entityId() {
    return this._entityId;
  }

  /**
   * Create options that override history, depth, and reliability —
   * the most commonly tuned policies.
   * @param {Object} [opts]
   * @param {function} [opts.callback] - Validation callback.
   * @param {string} [opts.entityId] - Entity disambiguation suffix.
   * @returns {QoSOverridingOptions}
   */
  static withDefaultPolicies(opts = {}) {
    return new QoSOverridingOptions(
      [QoSPolicyKind.HISTORY, QoSPolicyKind.DEPTH, QoSPolicyKind.RELIABILITY],
      opts
    );
  }
}

/**
 * Resolve QoS profile string to a mutable QoS object.
 * If already a QoS instance, return as-is.
 * @param {QoS|string} qos
 * @returns {QoS}
 */
function _resolveQoS(qos) {
  if (qos instanceof QoS) {
    return qos;
  }
  // Plain object with QoS fields — construct a QoS from its properties
  if (typeof qos === 'object' && qos !== null && !Array.isArray(qos)) {
    return new QoS(
      qos.history,
      qos.depth,
      qos.reliability,
      qos.durability,
      qos.liveliness,
      qos.avoidRosNameSpaceConventions
    );
  }
  // Profile string: create a QoS with the corresponding defaults
  // Values must match the rmw_qos_profile_* definitions in rmw/types.h
  switch (qos) {
    case QoS.profileDefault:
      return new QoS(
        QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_KEEP_LAST,
        10,
        QoS.ReliabilityPolicy.RMW_QOS_POLICY_RELIABILITY_RELIABLE,
        QoS.DurabilityPolicy.RMW_QOS_POLICY_DURABILITY_VOLATILE
      );
    case QoS.profileSystemDefault:
      return new QoS(
        QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_SYSTEM_DEFAULT,
        0,
        QoS.ReliabilityPolicy.RMW_QOS_POLICY_RELIABILITY_SYSTEM_DEFAULT,
        QoS.DurabilityPolicy.RMW_QOS_POLICY_DURABILITY_SYSTEM_DEFAULT,
        QoS.LivelinessPolicy.RMW_QOS_POLICY_LIVELINESS_SYSTEM_DEFAULT
      );
    case QoS.profileSensorData:
      return new QoS(
        QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_KEEP_LAST,
        5,
        QoS.ReliabilityPolicy.RMW_QOS_POLICY_RELIABILITY_BEST_EFFORT,
        QoS.DurabilityPolicy.RMW_QOS_POLICY_DURABILITY_VOLATILE
      );
    case QoS.profileServicesDefault:
      return new QoS(
        QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_KEEP_LAST,
        10,
        QoS.ReliabilityPolicy.RMW_QOS_POLICY_RELIABILITY_RELIABLE,
        QoS.DurabilityPolicy.RMW_QOS_POLICY_DURABILITY_VOLATILE
      );
    case QoS.profileParameters:
      return new QoS(
        QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_KEEP_LAST,
        1000,
        QoS.ReliabilityPolicy.RMW_QOS_POLICY_RELIABILITY_RELIABLE,
        QoS.DurabilityPolicy.RMW_QOS_POLICY_DURABILITY_VOLATILE
      );
    case QoS.profileParameterEvents:
      return new QoS(
        QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_KEEP_LAST,
        1000,
        QoS.ReliabilityPolicy.RMW_QOS_POLICY_RELIABILITY_RELIABLE,
        QoS.DurabilityPolicy.RMW_QOS_POLICY_DURABILITY_VOLATILE
      );
    case QoS.profileActionStatusDefault:
      return new QoS(
        QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_KEEP_LAST,
        1,
        QoS.ReliabilityPolicy.RMW_QOS_POLICY_RELIABILITY_RELIABLE,
        QoS.DurabilityPolicy.RMW_QOS_POLICY_DURABILITY_TRANSIENT_LOCAL
      );
    default:
      return new QoS(
        QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_KEEP_LAST,
        10,
        QoS.ReliabilityPolicy.RMW_QOS_POLICY_RELIABILITY_RELIABLE,
        QoS.DurabilityPolicy.RMW_QOS_POLICY_DURABILITY_VOLATILE
      );
  }
}

/**
 * Declare QoS override parameters on the node and apply any overrides
 * to the QoS profile in-place.
 *
 * @param {'publisher'|'subscription'} entityType
 * @param {Node} node
 * @param {string} topic - Fully resolved topic name.
 * @param {QoS} qos - Mutable QoS object (will be modified in-place).
 * @param {QoSOverridingOptions} options
 */
function declareQosParameters(entityType, node, topic, qos, options) {
  if (!options || options.policyKinds.length === 0) {
    return;
  }

  const idSuffix = options.entityId ? `_${options.entityId}` : '';
  const namePrefix = `qos_overrides.${topic}.${entityType}${idSuffix}`;

  for (const policyKind of options.policyKinds) {
    const mapping = POLICY_MAP[policyKind];
    if (!mapping) {
      continue;
    }

    const paramName = `${namePrefix}.${mapping.paramKey}`;
    const currentValue = qos[mapping.qosProp];
    const paramValue = mapping.toParam(currentValue, mapping.enumObj);

    const descriptor = new ParameterDescriptor(
      paramName,
      mapping.paramType,
      `QoS override for ${mapping.qosProp}`,
      true // readOnly
    );

    let param;
    try {
      param = node.declareParameter(
        new Parameter(paramName, mapping.paramType, paramValue),
        descriptor
      );
    } catch (e) {
      // Already declared (e.g. multiple entities on same topic) — reuse
      if (node.hasParameter(paramName)) {
        param = node.getParameter(paramName);
      } else {
        throw e;
      }
    }

    // Apply the (possibly overridden) parameter value back to QoS
    if (param && param.value !== paramValue) {
      qos[mapping.qosProp] = mapping.fromParam(param.value, mapping.enumObj);
    }
  }

  // Run validation callback if provided
  if (options.callback) {
    const result = options.callback(qos);
    if (result && !result.successful) {
      throw new Error(
        `QoS override validation failed: ${result.reason || 'unknown reason'}`
      );
    }
  }
}

module.exports = {
  QoSPolicyKind,
  QoSOverridingOptions,
  declareQosParameters,
  _resolveQoS,
};