/*
 * Copyright (c) 2019 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.examples.client;

import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.stream.Stream;

import com.google.common.base.MoreObjects;
import com.google.common.collect.Lists;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.sdk.client.nodes.UaNode;
import org.eclipse.milo.opcua.stack.core.Identifiers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class BrowseAsyncExample implements ClientExample {

    public static void main(String[] args) throws Exception {
        BrowseAsyncExample example = new BrowseAsyncExample();

        new ClientExampleRunner(example).run();
    }

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void run(OpcUaClient client, CompletableFuture<OpcUaClient> future) throws Exception {
        // synchronous connect
        client.connect().get();

        // start browsing at root folder
        UaNode rootNode = client.getAddressSpace().getNode(Identifiers.RootFolder);

        Tree<UaNode> tree = new Tree<>(rootNode);

        long startTime = System.nanoTime();
        browseRecursive(client, tree).get();
        long endTime = System.nanoTime();

        traverse(tree, 0, (depth, n) -> logger.info(indent(depth) + n.getBrowseName().getName()));

        logger.info(
            "Browse took {}ms",
            TimeUnit.MILLISECONDS.convert(endTime - startTime, TimeUnit.NANOSECONDS));

        future.complete(client);
    }

    private CompletableFuture<Void> browseRecursive(OpcUaClient client, Tree<UaNode> tree) {
        return client.getAddressSpace().browseNodesAsync(tree.node).thenCompose(nodes -> {
            // Add each child node to the tree
            nodes.forEach(tree::addChild);

            // For each child node browse for its children
            Stream<CompletableFuture<Void>> futures =
                tree.children.stream().map(child -> browseRecursive(client, child));

            // Return a CompletableFuture that completes when the child browses complete
            return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new));
        });
    }

    private static String indent(int depth) {
        StringBuilder s = new StringBuilder();
        for (int i = 0; i < depth; i++) {
            s.append("  ");
        }
        return s.toString();
    }

    private static <T> void traverse(Tree<T> tree, int depth, BiConsumer<Integer, T> consumer) {
        consumer.accept(depth, tree.node);

        tree.children.forEach(child -> traverse(child, depth + 1, consumer));
    }

    private static class Tree<T> {

        final List<Tree<T>> children = Lists.newCopyOnWriteArrayList();

        final T node;

        Tree(T node) {
            this.node = node;
        }

        void addChild(T child) {
            children.add(new Tree<>(child));
        }

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this)
                .add("node", node)
                .add("children", children)
                .toString();
        }

    }

}
