/*
 * Copyright (c) 2022 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;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;

import org.eclipse.milo.opcua.sdk.core.DataTypeTree;
import org.eclipse.milo.opcua.stack.client.UaStackClient;
import org.eclipse.milo.opcua.stack.core.AttributeId;
import org.eclipse.milo.opcua.stack.core.Identifiers;
import org.eclipse.milo.opcua.stack.core.NamespaceTable;
import org.eclipse.milo.opcua.stack.core.StatusCodes;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.serialization.UaResponseMessage;
import org.eclipse.milo.opcua.stack.core.types.OpcUaDefaultBinaryEncoding;
import org.eclipse.milo.opcua.stack.core.types.OpcUaDefaultXmlEncoding;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName;
import org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseDirection;
import org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseResultMask;
import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.BrowseDescription;
import org.eclipse.milo.opcua.stack.core.types.structured.ReadRequest;
import org.eclipse.milo.opcua.stack.core.types.structured.ReadResponse;
import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId;
import org.eclipse.milo.opcua.stack.core.types.structured.ReferenceDescription;
import org.eclipse.milo.opcua.stack.core.types.structured.RequestHeader;
import org.eclipse.milo.opcua.stack.core.util.FutureUtils;
import org.eclipse.milo.opcua.stack.core.util.Tree;
import org.eclipse.milo.opcua.stack.core.util.Unit;

import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;

/**
 * Builds a {@link DataTypeTree} by recursively browsing the DataType hierarchy starting at
 * {@link Identifiers#BaseDataType}.
 */
public final class DataTypeTreeBuilder {

    private DataTypeTreeBuilder() {}

    /**
     * Build a {@link DataTypeTree} by recursively browsing the DataType hierarchy starting at
     * {@link Identifiers#BaseDataType}.
     *
     * @param client a connected {@link OpcUaClient}.
     * @return a {@link DataTypeTree}.
     * @throws UaException if an unrecoverable error occurs while building the tree.
     */
    public static DataTypeTree build(OpcUaClient client) throws UaException {
        try {
            return buildAsync(client).get();
        } catch (InterruptedException e) {
            throw new UaException(StatusCodes.Bad_UnexpectedError, e);
        } catch (ExecutionException e) {
            throw UaException.extract(e)
                .orElse(new UaException(StatusCodes.Bad_UnexpectedError, e));
        }
    }

    /**
     * Build a {@link DataTypeTree} by recursively browsing the DataType hierarchy starting at
     * {@link Identifiers#BaseDataType}.
     *
     * @param client  a connected {@link UaStackClient}.
     * @param session an active {@link OpcUaSession}.
     * @return a {@link DataTypeTree}.
     * @throws UaException if an unrecoverable error occurs while building the tree.
     */
    public static DataTypeTree build(UaStackClient client, OpcUaSession session) throws UaException {
        try {
            return buildAsync(client, session).get();
        } catch (InterruptedException e) {
            throw new UaException(StatusCodes.Bad_UnexpectedError, e);
        } catch (ExecutionException e) {
            throw UaException.extract(e)
                .orElse(new UaException(StatusCodes.Bad_UnexpectedError, e));
        }
    }

    /**
     * Build a {@link DataTypeTree} by recursively browsing the DataType hierarchy starting at
     * {@link Identifiers#BaseDataType}.
     *
     * @param client a connected {@link OpcUaClient}.
     * @return a {@link DataTypeTree}.
     */
    public static CompletableFuture<DataTypeTree> buildAsync(OpcUaClient client) {
        return client.getSession().thenCompose(
            session ->
                buildAsync(client.getStackClient(), session)
        );
    }

    /**
     * Build a {@link DataTypeTree} by recursively browsing the DataType hierarchy starting at
     * {@link Identifiers#BaseDataType}.
     *
     * @param client  a connected {@link UaStackClient}.
     * @param session an active {@link OpcUaSession}.
     * @return a {@link DataTypeTree}.
     */
    public static CompletableFuture<DataTypeTree> buildAsync(UaStackClient client, OpcUaSession session) {
        Tree<DataTypeTree.DataType> root = new Tree<>(
            null,
            new DataTypeTree.DataType(
                QualifiedName.parse("0:BaseDataType"),
                Identifiers.BaseDataType,
                null,
                null
            )
        );

        return readNamespaceTable(client, session)
            .thenCompose(namespaceTable -> addChildren(root, client, session, namespaceTable))
            .thenApply(u -> new DataTypeTree(root));
    }

    private static CompletableFuture<NamespaceTable> readNamespaceTable(UaStackClient client, OpcUaSession session) {
        RequestHeader requestHeader = client.newRequestHeader(
            session.getAuthenticationToken(),
            client.getConfig().getRequestTimeout()
        );

        CompletableFuture<UaResponseMessage> readFuture = client.sendRequest(
            new ReadRequest(
                requestHeader,
                0.0,
                TimestampsToReturn.Neither,
                new ReadValueId[]{
                    new ReadValueId(
                        Identifiers.Server_NamespaceArray,
                        AttributeId.Value.uid(),
                        null,
                        QualifiedName.NULL_VALUE)}
            )
        );

        return readFuture.thenApply(ReadResponse.class::cast).thenApply(response -> {
            DataValue dataValue = response.getResults()[0];
            String[] namespaceUris = (String[]) dataValue.getValue().getValue();
            NamespaceTable namespaceTable = new NamespaceTable();
            for (String namespaceUri : namespaceUris) {
                namespaceTable.addUri(namespaceUri);
            }
            return namespaceTable;
        });
    }

    private static CompletableFuture<Unit> addChildren(
        Tree<DataTypeTree.DataType> tree,
        UaStackClient client,
        OpcUaSession session,
        NamespaceTable namespaceTable
    ) {

        CompletableFuture<List<ReferenceDescription>> subtypes = browseSafe(
            client,
            session,
            new BrowseDescription(
                tree.getValue().getNodeId(),
                BrowseDirection.Forward,
                Identifiers.HasSubtype,
                false,
                uint(NodeClass.DataType.getValue()),
                uint(BrowseResultMask.All.getValue())
            )
        );

        CompletableFuture<List<DataTypeTree.DataType>> dataTypesFuture = subtypes.thenCompose(references -> {
            Stream<CompletableFuture<DataTypeTree.DataType>> dataTypeFutures =
                references.stream().map(dataTypeReference -> {
                    NodeId dataTypeId = dataTypeReference.getNodeId()
                        .toNodeId(namespaceTable)
                        .orElse(NodeId.NULL_VALUE);

                    CompletableFuture<List<ReferenceDescription>> encodings = browseSafe(
                        client,
                        session,
                        new BrowseDescription(
                            dataTypeId,
                            BrowseDirection.Forward,
                            Identifiers.HasEncoding,
                            false,
                            uint(NodeClass.Object.getValue()),
                            uint(BrowseResultMask.All.getValue())
                        )
                    );

                    return encodings.thenApply(encodingReferences -> {
                        NodeId binaryEncodingId = null;
                        NodeId xmlEncodingId = null;

                        for (ReferenceDescription r : encodingReferences) {
                            if (r.getBrowseName().equals(OpcUaDefaultBinaryEncoding.ENCODING_NAME)) {
                                binaryEncodingId = r.getNodeId().toNodeId(namespaceTable).orElse(null);
                            } else if (r.getBrowseName().equals(OpcUaDefaultXmlEncoding.ENCODING_NAME)) {
                                xmlEncodingId = r.getNodeId().toNodeId(namespaceTable).orElse(null);
                            }
                        }

                        return new DataTypeTree.DataType(
                            dataTypeReference.getBrowseName(),
                            dataTypeId,
                            binaryEncodingId,
                            xmlEncodingId
                        );
                    });
                });

            return FutureUtils.sequence(dataTypeFutures);
        });

        return dataTypesFuture
            .thenCompose(dataTypes -> {
                Stream<CompletableFuture<Unit>> futures = dataTypes.stream()
                    .map(tree::addChild)
                    .map(childNode -> addChildren(childNode, client, session, namespaceTable));

                return FutureUtils.sequence(futures);
            })
            .thenApply(v -> Unit.VALUE);
    }

    /**
     * Browse a {@link BrowseDescription} "safely", completing successfully
     * with an empty List if any exceptions occur.
     *
     * @param client            a {@link UaStackClient}.
     * @param session           an {@link OpcUaSession}.
     * @param browseDescription the {@link BrowseDescription}.
     * @return a List of {@link ReferenceDescription}s obtained by browsing {@code browseDescription}.
     */
    private static CompletableFuture<List<ReferenceDescription>> browseSafe(
        UaStackClient client,
        OpcUaSession session,
        BrowseDescription browseDescription
    ) {

        return BrowseHelper.browse(client, session, browseDescription, uint(0))
            .exceptionally(ex -> Collections.emptyList());
    }

}
