/*
 * Copyright (c) 2021 the Eclipse Milo Authors
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */

package org.eclipse.milo.opcua.sdk.client.api.subscriptions;

import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

import com.google.common.collect.ImmutableList;
import org.eclipse.milo.opcua.sdk.client.api.UaSession;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime;
import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
import org.eclipse.milo.opcua.stack.core.types.enumerated.MonitoringMode;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoredItemCreateRequest;
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoredItemModifyRequest;

public interface UaSubscription {

    /**
     * @return the server-assigned id for this {@link UaSubscription}.
     */
    UInteger getSubscriptionId();

    /**
     * @return the last requested publishing interval of this {@link UaSubscription}.
     */
    double getRequestedPublishingInterval();

    /**
     * @return the actual/revised publishing interval of this {@link UaSubscription}.
     */
    double getRevisedPublishingInterval();

    /**
     * @return the last requested lifetime count of this {@link UaSubscription}.
     */
    UInteger getRequestedLifetimeCount();

    /**
     * @return the actual/revised lifetime count of this {@link UaSubscription}.
     */
    UInteger getRevisedLifetimeCount();

    /**
     * @return the last requested max keep-alive count of this {@link UaSubscription}.
     */
    UInteger getRequestedMaxKeepAliveCount();

    /**
     * @return the actual/revised max keep-alive count of this {@link UaSubscription}.
     */
    UInteger getRevisedMaxKeepAliveCount();

    /**
     * @return the maximum number of notifications that will be returned in any publish response.
     */
    UInteger getMaxNotificationsPerPublish();

    /**
     * @return {@code true} if publishing is enabled.
     */
    boolean isPublishingEnabled();

    /**
     * @return the relative priority assigned to this {@link UaSubscription}.
     */
    UByte getPriority();

    /**
     * @return an {@link ImmutableList} of this {@link UaSubscription}'s {@link UaMonitoredItem}s.
     */
    ImmutableList<UaMonitoredItem> getMonitoredItems();

    /**
     * @return the next available client handle, unique within the context of this {@link UaSubscription}.
     */
    UInteger nextClientHandle();

    /**
     * Create one or more {@link UaMonitoredItem}s.
     * <p>
     * Callers must check the quality of each of the returned {@link UaMonitoredItem}s; it is not to be assumed that
     * all items were created successfully. Any item with a bad quality will not be updated nor will it be part of the
     * subscription's bookkeeping.
     * <p>
     * <b>WARNING:</b> items must be created in {@link MonitoringMode#Sampling} and then later set to
     * {@link MonitoringMode#Reporting} <i>after</i> consumers have been set in order to avoid a race condition where
     * the initial values arrive before the consumers are set. Alternatively, you can use
     * {@link #createMonitoredItems(TimestampsToReturn, List, ItemCreationCallback)} to avoid this race condition.
     *
     * @param timestampsToReturn the {@link TimestampsToReturn}.
     * @param itemsToCreate      a list of {@link MonitoredItemCreateRequest}s.
     * @return a list of {@link UaMonitoredItem}s.
     */
    CompletableFuture<List<UaMonitoredItem>> createMonitoredItems(TimestampsToReturn timestampsToReturn,
                                                                  List<MonitoredItemCreateRequest> itemsToCreate);

    /**
     * Create one or more {@link UaMonitoredItem}s.
     * <p>
     * Callers must check the quality of each of the returned {@link UaMonitoredItem}s; it is not to be assumed that
     * all items were created successfully. Any item with a bad quality will not be updated nor will it be part of the
     * subscription's bookkeeping.
     * <p>
     * {@code itemCreationCallback} will be invoked for each successfully created {@link UaMonitoredItem}. Callers
     * should use this opportunity to register any value or event consumers on the item, as this is the only time in
     * which it is guaranteed no values or events will be delivered to the item yet.
     *
     * @param timestampsToReturn   the {@link TimestampsToReturn}.
     * @param itemsToCreate        a list of {@link MonitoredItemCreateRequest}s.
     * @param itemCreationCallback callback to be invoked for each successfully created {@link UaMonitoredItem}.
     * @return a list of {@link UaMonitoredItem}s.
     */
    CompletableFuture<List<UaMonitoredItem>> createMonitoredItems(
        TimestampsToReturn timestampsToReturn,
        List<MonitoredItemCreateRequest> itemsToCreate,
        ItemCreationCallback itemCreationCallback);

    /**
     * Modify one or more {@link UaMonitoredItem}s.
     *
     * @param timestampsToReturn the {@link TimestampsToReturn} to set for each item.
     * @param itemsToModify      a list of {@link MonitoredItemModifyRequest}s.
     * @return a {@link CompletableFuture} containing a list of {@link StatusCode}s, the size and order matching that
     * of {@code itemsToModify}.
     */
    CompletableFuture<List<StatusCode>> modifyMonitoredItems(TimestampsToReturn timestampsToReturn,
                                                             List<MonitoredItemModifyRequest> itemsToModify);

    /**
     * Delete on or more {@link UaMonitoredItem}s.
     *
     * @param itemsToDelete the items to delete.
     * @return a {@link CompletableFuture} containing a list of {@link StatusCode}s, the size and order matching that
     * of {@code itemsToDelete}.
     */
    CompletableFuture<List<StatusCode>> deleteMonitoredItems(List<UaMonitoredItem> itemsToDelete);

    /**
     * Set the {@link MonitoringMode} for one or more {@link UaMonitoredItem}s.
     *
     * @param monitoringMode the {@link MonitoringMode} to set.
     * @param items          the {@link UaMonitoredItem}s to set the mode on.
     * @return a {@link CompletableFuture} containing a list of {@link StatusCode}s, the size and order matching that
     * of {@code items}.
     */
    CompletableFuture<List<StatusCode>> setMonitoringMode(MonitoringMode monitoringMode, List<UaMonitoredItem> items);

    /**
     * Set the publishing mode for this subscription.
     *
     * @param publishingEnabled {@code true} if publishing should be enabled.
     * @return a {@link CompletableFuture} containing a {@link StatusCode} representing the result of this operation.
     */
    CompletableFuture<StatusCode> setPublishingMode(boolean publishingEnabled);

    /**
     * Add triggering links between the item identified by {@code triggerItemId} and the items identified by
     * {@code linksToAdd}.
     *
     * @param triggeringItemId the id of the triggering item.
     * @param linksToAdd       the ids of the triggered items.
     * @return a list of {@link StatusCode}s corresponding to the items in {@code linksToAdd}.
     */
    CompletableFuture<List<StatusCode>> addTriggeringLinks(UInteger triggeringItemId, List<UInteger> linksToAdd);

    /**
     * Add triggering links between the {@code triggeringItem} and the items in {@code linksToAdd}.
     *
     * @param triggeringItem the triggering item.
     * @param linksToAdd     the triggered items.
     * @return a list of {@link StatusCode}s corresponding to the items in {@code linksToAdd}.
     */
    default CompletableFuture<List<StatusCode>> addTriggeringLinks(
        UaMonitoredItem triggeringItem,
        List<UaMonitoredItem> linksToAdd
    ) {

        return addTriggeringLinks(
            triggeringItem.getMonitoredItemId(),
            linksToAdd.stream()
                .map(UaMonitoredItem::getMonitoredItemId)
                .collect(Collectors.toList())
        );
    }

    /**
     * Remove triggering links between the item identified by {@code triggeringItemId} and the items identified by
     * {@code linksToRemove}.
     *
     * @param triggeringItemId the id of the triggering item.
     * @param linksToRemove    the ids of the triggered items.
     * @return a list of {@link StatusCode}s corresponding to the items in {@code linksToRemove}.
     */
    CompletableFuture<List<StatusCode>> removeTriggeringLinks(UInteger triggeringItemId, List<UInteger> linksToRemove);

    /**
     * Remove triggering links between {@code triggeringItem} and the items in {@code linksToRemove}.
     *
     * @param triggeringItem the triggering item.
     * @param linksToRemove  the triggered items.
     * @return a list of {@link StatusCode}s corresponding to the items in {@code linksToRemove}.
     */
    default CompletableFuture<List<StatusCode>> removeTriggeringLinks(
        UaMonitoredItem triggeringItem,
        List<UaMonitoredItem> linksToRemove
    ) {

        return removeTriggeringLinks(
            triggeringItem.getMonitoredItemId(),
            linksToRemove.stream()
                .map(UaMonitoredItem::getMonitoredItemId)
                .collect(Collectors.toList())
        );
    }

    /**
     * Add a {@link NotificationListener}.
     *
     * @param listener the {@link NotificationListener} to add.
     */
    void addNotificationListener(NotificationListener listener);

    /**
     * Remove a {@link NotificationListener}.
     *
     * @param listener the {@link NotificationListener} to remove.
     */
    void removeNotificationListener(NotificationListener listener);

    interface ItemCreationCallback {

        void onItemCreated(UaMonitoredItem item, int index);

    }


    interface NotificationListener {

        /**
         * A notification containing data value changes for this {@link UaSubscription} has arrived.
         * <p>
         * This callback is invoked after all individual item callbacks and is offered as an alternative to defining
         * callbacks on a per-item basis. Its use is not required.
         *
         * @param subscription   the {@link UaSubscription} that received the notification.
         * @param monitoredItems the {@link UaMonitoredItem}s that received a data value change.
         * @param dataValues     the corresponding {@link DataValue} change for each item in {@code monitoredItems}.
         * @param publishTime    the time on the server at which this notification was published.
         */
        default void onDataChangeNotification(UaSubscription subscription,
                                              List<UaMonitoredItem> monitoredItems,
                                              List<DataValue> dataValues,
                                              DateTime publishTime) {}

        /**
         * A notification containing events for this {@link UaSubscription} has arrived.
         * <p>
         * This callback is invoked after all individual item callbacks and is offered as an alternative to defining
         * callbacks on a per-item basis. Its use is not required.
         *
         * @param subscription   the {@link UaSubscription} that received the notification.
         * @param monitoredItems the {@link UaMonitoredItem}s that received an event notification.
         * @param eventFields    the corresponding event field values for each item in {@code monitoredItems}.
         * @param publishTime    the time on the server at which this notification was published.
         */
        default void onEventNotification(UaSubscription subscription,
                                         List<UaMonitoredItem> monitoredItems,
                                         List<Variant[]> eventFields,
                                         DateTime publishTime) {}

        /**
         * A keep-alive message was received.
         *
         * @param subscription the {@link UaSubscription} that received the keep-alive.
         * @param publishTime  the time the server published the keep-alive.
         */
        default void onKeepAliveNotification(UaSubscription subscription, DateTime publishTime) {}

        /**
         * A status change notification was received.
         *
         * @param subscription the {@link UaSubscription} that received the status change.
         * @param status       the new subscription status.
         */
        default void onStatusChangedNotification(UaSubscription subscription, StatusCode status) {}

        /**
         * Attempts to recover missed notification data have failed.
         * <p>
         * When a notification is missed a series of Republish requests are initiated to recover the missing data. If
         * republishing fails, or any of the notifications are no longer available, this callback will be invoked.
         *
         * @param subscription the subscription that missed notification data.
         */
        default void onNotificationDataLost(UaSubscription subscription) {}

        /**
         * A new {@link UaSession} was established, and upon attempting to transfer an existing subscription to this
         * new session, a failure occurred.
         * <p>
         * This subscription will be removed from {@link UaSubscriptionManager}'s bookkeeping. It must be re-created.
         *
         * @param subscription the {@link UaSubscription} that could not be transferred.
         * @param statusCode   the {@link StatusCode} for the transfer failure.
         */
        default void onSubscriptionTransferFailed(UaSubscription subscription, StatusCode statusCode) {}

        /**
         * The subscription watchdog timer has elapsed. The receiver of this callback should
         * consider deleting and re-creating this subscription or taking other appropriate action.
         * <p>
         * The timer elapses when 125% of the expected keep-alive interval has passed since receipt
         * of the last PublishResponse. The watchdog timer will not be restarted unless a new
         * PublishResponse for this subscription is received.
         * <p>
         * The expected keep-alive interval is {@code publishingInterval * keepAliveCount}
         * milliseconds.
         *
         * @param subscription the {@link UaSubscription} that has not received a response.
         */
        default void onSubscriptionWatchdogTimerElapsed(UaSubscription subscription) {}

    }

}
