// Copyright (c) 2025, The Robot Web Tools Contributors
//
// 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 rclnodejs from './native_loader.js';
import DistroUtils from './distro.js';
import {
  OperationError,
  RangeValidationError,
  TypeValidationError,
} from './errors.js';
import Entity from './entity.js';

/**
 * Enumeration for PublisherEventCallbacks event types.
 * @enum {number}
 */
const PublisherEventType = {
  /** @member {number} */
  PUBLISHER_OFFERED_DEADLINE_MISSED: 0,
  /** @member {number} */
  PUBLISHER_LIVELINESS_LOST: 1,
  /** @member {number} */
  PUBLISHER_OFFERED_INCOMPATIBLE_QOS: 2,
  /** @member {number} */
  PUBLISHER_INCOMPATIBLE_TYPE: 3,
  /** @member {number} */
  PUBLISHER_MATCHED: 4,
};

/**
 * Enumeration for SubscriptionEventCallbacks event types.
 * @enum {number}
 */
const SubscriptionEventType = {
  /** @member {number} */
  SUBSCRIPTION_REQUESTED_DEADLINE_MISSED: 0,
  /** @member {number} */
  SUBSCRIPTION_LIVELINESS_CHANGED: 1,
  /** @member {number} */
  SUBSCRIPTION_REQUESTED_INCOMPATIBLE_QOS: 2,
  /** @member {number} */
  SUBSCRIPTION_MESSAGE_LOST: 3,
  /** @member {number} */
  SUBSCRIPTION_INCOMPATIBLE_TYPE: 4,
  /** @member {number} */
  SUBSCRIPTION_MATCHED: 5,
};

/**
 * Check if a publisher event type is supported by the active RMW implementation.
 *
 * Only available in ROS 2 Rolling and later, where the underlying rcl API
 * (`rcl_publisher_event_type_is_supported`) is provided.
 *
 * @param {number} eventType - A {@link PublisherEventType} value.
 * @return {boolean} True if the event type is supported by the active RMW
 *   implementation, false otherwise.
 * @throws {OperationError} if invoked on a ROS distro older than Rolling.
 * @throws {TypeValidationError} if eventType is not a number.
 * @throws {RangeValidationError} if eventType is not a valid
 *   {@link PublisherEventType} value.
 */
function isPublisherEventTypeSupported(eventType) {
  if (typeof rclnodejs.isPublisherEventTypeSupported !== 'function') {
    throw new OperationError(
      'isPublisherEventTypeSupported is only available in ROS 2 Rolling and later',
      {
        code: 'UNSUPPORTED_ROS_VERSION',
        entityType: 'publisher event type',
        details: {
          requiredVersion: 'rolling',
          currentVersion: DistroUtils.getDistroId(),
        },
      }
    );
  }
  if (typeof eventType !== 'number') {
    throw new TypeValidationError('eventType', eventType, 'number', {
      entityType: 'publisher event type',
    });
  }
  if (!Object.values(PublisherEventType).includes(eventType)) {
    throw new RangeValidationError(
      'eventType',
      eventType,
      'one of PublisherEventType values',
      { entityType: 'publisher event type' }
    );
  }
  return rclnodejs.isPublisherEventTypeSupported(eventType);
}

/**
 * Check if a subscription event type is supported by the active RMW implementation.
 *
 * Only available in ROS 2 Rolling and later, where the underlying rcl API
 * (`rcl_subscription_event_type_is_supported`) is provided.
 *
 * @param {number} eventType - A {@link SubscriptionEventType} value.
 * @return {boolean} True if the event type is supported by the active RMW
 *   implementation, false otherwise.
 * @throws {OperationError} if invoked on a ROS distro older than Rolling.
 * @throws {TypeValidationError} if eventType is not a number.
 * @throws {RangeValidationError} if eventType is not a valid
 *   {@link SubscriptionEventType} value.
 */
function isSubscriptionEventTypeSupported(eventType) {
  if (typeof rclnodejs.isSubscriptionEventTypeSupported !== 'function') {
    throw new OperationError(
      'isSubscriptionEventTypeSupported is only available in ROS 2 Rolling and later',
      {
        code: 'UNSUPPORTED_ROS_VERSION',
        entityType: 'subscription event type',
        details: {
          requiredVersion: 'rolling',
          currentVersion: DistroUtils.getDistroId(),
        },
      }
    );
  }
  if (typeof eventType !== 'number') {
    throw new TypeValidationError('eventType', eventType, 'number', {
      entityType: 'subscription event type',
    });
  }
  if (!Object.values(SubscriptionEventType).includes(eventType)) {
    throw new RangeValidationError(
      'eventType',
      eventType,
      'one of SubscriptionEventType values',
      { entityType: 'subscription event type' }
    );
  }
  return rclnodejs.isSubscriptionEventTypeSupported(eventType);
}

class EventHandler extends Entity {
  constructor(handle, callback, eventType, eventTypeName) {
    super(handle, null, null);
    this._callback = callback;
    this._eventType = eventType;
    this._eventTypeName = eventTypeName;
  }

  takeData() {
    const data = rclnodejs.takeEvent(this._handle, {
      [this._eventTypeName]: this._eventType,
    });
    if (this._callback) {
      this._callback(data);
    }
  }
}

/**
 * @class - Class representing a ROS 2 PublisherEventCallbacks
 * @hideconstructor
 */
class PublisherEventCallbacks {
  constructor() {
    if (DistroUtils.getDistroId() < DistroUtils.getDistroId('jazzy')) {
      throw new OperationError(
        'PublisherEventCallbacks is only available in ROS 2 Jazzy and later',
        {
          code: 'UNSUPPORTED_ROS_VERSION',
          entityType: 'publisher event callbacks',
          details: {
            requiredVersion: 'jazzy',
            currentVersion: DistroUtils.getDistroId(),
          },
        }
      );
    }
    this._deadline = null;
    this._incompatible_qos = null;
    this._liveliness = null;
    this._incompatible_type = null;
    this._matched = null;
    this._eventHandlers = [];
  }

  /**
   * Set deadline missed callback.
   * @param {function} callback - The callback function to be called.
   */
  set deadline(callback) {
    this._deadline = callback;
  }

  /**
   * Get deadline missed callback.
   * @return {function} - The callback function.
   */
  get deadline() {
    return this._deadline;
  }

  /**
   * Set incompatible QoS callback.
   * @param {function} callback - The callback function to be called.
   */
  set incompatibleQos(callback) {
    this._incompatible_qos = callback;
  }

  /**
   * Get incompatible QoS callback.
   * @return {function} - The callback function.
   */
  get incompatibleQos() {
    return this._incompatible_qos;
  }

  /**
   * Set liveliness lost callback.
   * @param {function} callback - The callback function to be called.
   */
  set liveliness(callback) {
    this._liveliness = callback;
  }

  /**
   * Get liveliness lost callback.
   * @return {function} - The callback function.
   */
  get liveliness() {
    return this._liveliness;
  }

  /**
   * Set incompatible type callback.
   * @param {function} callback - The callback function to be called.
   */
  set incompatibleType(callback) {
    this._incompatible_type = callback;
  }

  /**
   * Get incompatible type callback.
   * @return {function} - The callback function.
   */
  get incompatibleType() {
    return this._incompatible_type;
  }

  /**
   * Set matched callback.
   * @param {function} callback - The callback function to be called.
   */
  set matched(callback) {
    this._matched = callback;
  }

  /**
   * Get matched callback.
   * @return {function} - The callback function.
   */
  get matched() {
    return this._matched;
  }

  createEventHandlers(publisherHandle) {
    if (this._deadline) {
      const deadlineHandle = rclnodejs.createPublisherEventHandle(
        publisherHandle,
        PublisherEventType.PUBLISHER_OFFERED_DEADLINE_MISSED
      );
      this._eventHandlers.push(
        new EventHandler(
          deadlineHandle,
          this._deadline,
          PublisherEventType.PUBLISHER_OFFERED_DEADLINE_MISSED,
          'publisher_event_type'
        )
      );
    }

    if (this._incompatible_qos) {
      const incompatibleQosHandle = rclnodejs.createPublisherEventHandle(
        publisherHandle,
        PublisherEventType.PUBLISHER_OFFERED_INCOMPATIBLE_QOS
      );
      this._eventHandlers.push(
        new EventHandler(
          incompatibleQosHandle,
          this._incompatible_qos,
          PublisherEventType.PUBLISHER_OFFERED_INCOMPATIBLE_QOS,
          'publisher_event_type'
        )
      );
    }

    if (this._liveliness) {
      const livelinessHandle = rclnodejs.createPublisherEventHandle(
        publisherHandle,
        PublisherEventType.PUBLISHER_LIVELINESS_LOST
      );
      this._eventHandlers.push(
        new EventHandler(
          livelinessHandle,
          this._liveliness,
          PublisherEventType.PUBLISHER_LIVELINESS_LOST,
          'publisher_event_type'
        )
      );
    }

    if (this._incompatible_type) {
      const incompatibleTypeHandle = rclnodejs.createPublisherEventHandle(
        publisherHandle,
        PublisherEventType.PUBLISHER_INCOMPATIBLE_TYPE
      );
      this._eventHandlers.push(
        new EventHandler(
          incompatibleTypeHandle,
          this._incompatible_type,
          PublisherEventType.PUBLISHER_INCOMPATIBLE_TYPE,
          'publisher_event_type'
        )
      );
    }

    if (this._matched) {
      const matchedHandle = rclnodejs.createPublisherEventHandle(
        publisherHandle,
        PublisherEventType.PUBLISHER_MATCHED
      );
      this._eventHandlers.push(
        new EventHandler(
          matchedHandle,
          this._matched,
          PublisherEventType.PUBLISHER_MATCHED,
          'publisher_event_type'
        )
      );
    }

    return this._eventHandlers;
  }

  get eventHandlers() {
    return this._eventHandlers;
  }
}

/**
 * @class - Class representing a ROS 2 SubscriptionEventCallbacks
 * @hideconstructor
 */
class SubscriptionEventCallbacks {
  constructor() {
    if (DistroUtils.getDistroId() < DistroUtils.getDistroId('jazzy')) {
      throw new OperationError(
        'SubscriptionEventCallbacks is only available in ROS 2 Jazzy and later',
        {
          code: 'UNSUPPORTED_ROS_VERSION',
          entityType: 'subscription event callbacks',
          details: {
            requiredVersion: 'jazzy',
            currentVersion: DistroUtils.getDistroId(),
          },
        }
      );
    }
    this._deadline = null;
    this._incompatible_qos = null;
    this._liveliness = null;
    this._message_lost = null;
    this._incompatible_type = null;
    this._matched = null;
    this._eventHandlers = [];
  }

  /**
   * Set the callback for deadline missed event.
   * @param {function} callback - The callback function to be called.
   */
  set deadline(callback) {
    this._deadline = callback;
  }

  /**
   * Get the callback for deadline missed event.
   * @return {function} - The callback function.
   */
  get deadline() {
    return this._deadline;
  }

  /**
   * Set the callback for incompatible QoS event.
   * @param {function} callback - The callback function to be called.
   */
  set incompatibleQos(callback) {
    this._incompatible_qos = callback;
  }

  /**
   * Get the callback for incompatible QoS event.
   * @return {function} - The callback function.
   */
  get incompatibleQos() {
    return this._incompatible_qos;
  }

  /**
   * Set the callback for liveliness changed event.
   * @param {function} callback - The callback function to be called.
   */
  set liveliness(callback) {
    this._liveliness = callback;
  }

  /**
   * Get the callback for liveliness changed event.
   * @return {function} - The callback function.
   */
  get liveliness() {
    return this._liveliness;
  }

  /**
   * Set the callback for message lost event.
   * @param {function} callback - The callback function to be called.
   */
  set messageLost(callback) {
    this._message_lost = callback;
  }

  /**
   * Get the callback for message lost event.
   * @return {function} - The callback function.
   */
  get messageLost() {
    return this._message_lost;
  }

  /**
   * Set the callback for incompatible type event.
   * @param {function} callback - The callback function to be called.
   */
  set incompatibleType(callback) {
    this._incompatible_type = callback;
  }

  /**
   * Get the callback for incompatible type event.
   * @return {function} - The callback function.
   */
  get incompatibleType() {
    return this._incompatible_type;
  }

  /**
   * Set the callback for matched event.
   * @param {function} callback - The callback function to be called.
   */
  set matched(callback) {
    this._matched = callback;
  }

  /**
   * Get the callback for matched event.
   * @return {function} - The callback function.
   */
  get matched() {
    return this._matched;
  }

  createEventHandlers(subscriptionHandle) {
    if (this._deadline) {
      const deadlineHandle = rclnodejs.createSubscriptionEventHandle(
        subscriptionHandle,
        SubscriptionEventType.SUBSCRIPTION_REQUESTED_DEADLINE_MISSED
      );
      this._eventHandlers.push(
        new EventHandler(
          deadlineHandle,
          this._deadline,
          SubscriptionEventType.SUBSCRIPTION_REQUESTED_DEADLINE_MISSED,
          'subscription_event_type'
        )
      );
    }

    if (this._incompatible_qos) {
      const incompatibleQosHandle = rclnodejs.createSubscriptionEventHandle(
        subscriptionHandle,
        SubscriptionEventType.SUBSCRIPTION_REQUESTED_INCOMPATIBLE_QOS
      );
      this._eventHandlers.push(
        new EventHandler(
          incompatibleQosHandle,
          this._incompatible_qos,
          SubscriptionEventType.SUBSCRIPTION_REQUESTED_INCOMPATIBLE_QOS,
          'subscription_event_type'
        )
      );
    }

    if (this._liveliness) {
      const livelinessHandle = rclnodejs.createSubscriptionEventHandle(
        subscriptionHandle,
        SubscriptionEventType.SUBSCRIPTION_LIVELINESS_CHANGED
      );
      this._eventHandlers.push(
        new EventHandler(
          livelinessHandle,
          this._liveliness,
          SubscriptionEventType.SUBSCRIPTION_LIVELINESS_CHANGED,
          'subscription_event_type'
        )
      );
    }

    if (this._message_lost) {
      const messageLostHandle = rclnodejs.createSubscriptionEventHandle(
        subscriptionHandle,
        SubscriptionEventType.SUBSCRIPTION_MESSAGE_LOST
      );
      this._eventHandlers.push(
        new EventHandler(
          messageLostHandle,
          this._message_lost,
          SubscriptionEventType.SUBSCRIPTION_MESSAGE_LOST,
          'subscription_event_type'
        )
      );
    }

    if (this._incompatible_type) {
      const incompatibleTypeHandle = rclnodejs.createSubscriptionEventHandle(
        subscriptionHandle,
        SubscriptionEventType.SUBSCRIPTION_INCOMPATIBLE_TYPE
      );
      this._eventHandlers.push(
        new EventHandler(
          incompatibleTypeHandle,
          this._incompatible_type,
          SubscriptionEventType.SUBSCRIPTION_INCOMPATIBLE_TYPE,
          'subscription_event_type'
        )
      );
    }

    if (this._matched) {
      const matchedHandle = rclnodejs.createSubscriptionEventHandle(
        subscriptionHandle,
        SubscriptionEventType.SUBSCRIPTION_MATCHED
      );
      this._eventHandlers.push(
        new EventHandler(
          matchedHandle,
          this._matched,
          SubscriptionEventType.SUBSCRIPTION_MATCHED,
          'subscription_event_type'
        )
      );
    }

    return this._eventHandlers;
  }
}

export {
  PublisherEventCallbacks,
  PublisherEventType,
  SubscriptionEventCallbacks,
  SubscriptionEventType,
  isPublisherEventTypeSupported,
  isSubscriptionEventTypeSupported,
};