/*
 * Copyright (c) 2020 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.methods;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.sdk.client.nodes.UaMethodNode;
import org.eclipse.milo.opcua.sdk.client.nodes.UaObjectNode;
import org.eclipse.milo.opcua.stack.core.StatusCodes;
import org.eclipse.milo.opcua.stack.core.UaException;
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.structured.Argument;
import org.eclipse.milo.opcua.stack.core.types.structured.CallMethodRequest;

/**
 * A callable method belonging to an ObjectNode.
 */
public class UaMethod {

    private final OpcUaClient client;
    private final UaObjectNode objectNode;
    private final UaMethodNode methodNode;
    private final Argument[] inputArguments;
    private final Argument[] outputArguments;

    public UaMethod(
        OpcUaClient client,
        UaObjectNode objectNode,
        UaMethodNode methodNode,
        Argument[] inputArguments,
        Argument[] outputArguments
    ) {

        this.client = client;
        this.objectNode = objectNode;
        this.methodNode = methodNode;
        this.inputArguments = inputArguments;
        this.outputArguments = outputArguments;
    }

    /**
     * Get the {@link UaObjectNode} this method belongs to.
     *
     * @return the {@link UaObjectNode} this method belongs to.
     */
    public UaObjectNode getObjectNode() {
        return objectNode;
    }

    /**
     * Get the {@link UaMethodNode} for this method.
     *
     * @return the {@link UaMethodNode} for this method.
     */
    public UaMethodNode getMethodNode() {
        return methodNode;
    }

    /**
     * Get the input arguments for this method.
     * <p>
     * The array will be empty if there are no inputs.
     *
     * @return the input arguments for this method.
     */
    public Argument[] getInputArguments() {
        return inputArguments;
    }

    /**
     * Get the output arguments for this method.
     * <p>
     * The array will be empty if there are no outputs.
     *
     * @return the output arguments for this method.
     */
    public Argument[] getOutputArguments() {
        return outputArguments;
    }

    /**
     * Call this method, passing the values in {@code inputs} for the input arguments.
     *
     * @param inputs an array of {@link Variant}s containing the input values.
     * @return an array of {@link Variant}s containing the output values.
     * @throws UaException       if a service-level error occurs.
     * @throws UaMethodException if the operation-level result was not good.
     */
    public Variant[] call(Variant[] inputs) throws UaMethodException, UaException {
        try {
            return callAsync(inputs).get();
        } catch (ExecutionException | InterruptedException e) {
            throw UaException.extract(e)
                .orElse(new UaException(StatusCodes.Bad_UnexpectedError, e));
        }
    }

    /**
     * Call this method, passing the values in {@code inputs} for the input arguments.
     * <p>
     * This method completes asynchronously.
     *
     * @param inputs an array of {@link Variant}s containing the input values.
     * @return a {@link CompletableFuture} that completes successfully with the output values or
     * completes exceptionally if an operation- or service-level error occurs.
     */
    public CompletableFuture<Variant[]> callAsync(Variant[] inputs) {
        CallMethodRequest request = new CallMethodRequest(
            objectNode.getNodeId(),
            methodNode.getNodeId(),
            inputs
        );

        return client.call(request).thenApply(result -> {
            StatusCode statusCode = result.getStatusCode();

            if (statusCode.isGood()) {
                return result.getOutputArguments();
            } else {
                throw new RuntimeException(new UaMethodException(
                    statusCode,
                    result.getInputArgumentResults(),
                    result.getInputArgumentDiagnosticInfos()
                ));
            }
        });
    }

}
