/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */
package org.apache.camel.component.milo.server.internal;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

import com.google.common.collect.Lists;
import org.apache.camel.component.milo.client.MiloClientConsumer;
import org.eclipse.milo.opcua.sdk.core.Reference;
import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
import org.eclipse.milo.opcua.sdk.server.api.AccessContext;
import org.eclipse.milo.opcua.sdk.server.api.DataItem;
import org.eclipse.milo.opcua.sdk.server.api.MonitoredItem;
import org.eclipse.milo.opcua.sdk.server.api.Namespace;
import org.eclipse.milo.opcua.sdk.server.api.ServerNodeMap;
import org.eclipse.milo.opcua.sdk.server.nodes.AttributeContext;
import org.eclipse.milo.opcua.sdk.server.nodes.ServerNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode;
import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel;
import org.eclipse.milo.opcua.stack.core.Identifiers;
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.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
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.builtin.StatusCode;
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort;
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.ReadValueId;
import org.eclipse.milo.opcua.stack.core.types.structured.WriteValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CamelNamespace implements Namespace {

    private static final Logger LOG = LoggerFactory.getLogger(MiloClientConsumer.class);

    private final UShort namespaceIndex;

    private final String namespaceUri;

    private final ServerNodeMap nodeManager;
    private final SubscriptionModel subscriptionModel;

    private final UaFolderNode folder;
    private final UaObjectNode itemsObject;

    private final Map<String, CamelServerItem> itemMap = new HashMap<>();

    public CamelNamespace(final UShort namespaceIndex, final String namespaceUri, final OpcUaServer server) {
        this.namespaceIndex = namespaceIndex;
        this.namespaceUri = namespaceUri;

        this.nodeManager = server.getNodeMap();
        this.subscriptionModel = new SubscriptionModel(server, this);

        // create structure

        {
            final NodeId nodeId = new NodeId(namespaceIndex, "camel");
            final QualifiedName name = new QualifiedName(namespaceIndex, "camel");
            final LocalizedText displayName = LocalizedText.english("Camel");

            this.folder = new UaFolderNode(this.nodeManager, nodeId, name, displayName);
            this.nodeManager.addNode(this.folder);
        }

        {
            final NodeId nodeId = new NodeId(namespaceIndex, "items");
            final QualifiedName name = new QualifiedName(namespaceIndex, "items");
            final LocalizedText displayName = LocalizedText.english("Items");
            this.itemsObject = new UaObjectNode(this.nodeManager, nodeId, name, displayName);
            this.folder.addComponent(this.itemsObject);
        }

        // register reference to structure

        try {
            server.getUaNamespace().addReference(Identifiers.ObjectsFolder, Identifiers.Organizes, true, this.folder.getNodeId().expanded(), NodeClass.Object);
        } catch (final UaException e) {
            throw new RuntimeException("Failed to register folder", e);
        }
    }

    @Override
    public UShort getNamespaceIndex() {
        return this.namespaceIndex;
    }

    @Override
    public String getNamespaceUri() {
        return this.namespaceUri;
    }

    @Override
    public CompletableFuture<List<Reference>> browse(final AccessContext context, final NodeId nodeId) {
        final ServerNode node = this.nodeManager.get(nodeId);

        if (node != null) {
            return CompletableFuture.completedFuture(node.getReferences());
        } else {
            final CompletableFuture<List<Reference>> f = new CompletableFuture<>();
            f.completeExceptionally(new UaException(StatusCodes.Bad_NodeIdUnknown));
            return f;
        }
    }

    @Override
    public void read(final ReadContext context, final Double maxAge, final TimestampsToReturn timestamps, final List<ReadValueId> readValueIds) {
        final List<DataValue> results = Lists.newArrayListWithCapacity(readValueIds.size());

        for (final ReadValueId id : readValueIds) {
            final ServerNode node = this.nodeManager.get(id.getNodeId());

            final DataValue value;

            if (node != null) {
                value = node.readAttribute(new AttributeContext(context), id.getAttributeId(), timestamps, id.getIndexRange(), null);
            } else {
                value = new DataValue(StatusCodes.Bad_NodeIdUnknown);
            }

            results.add(value);
        }

        context.complete(results);
    }

    @Override
    public void write(final WriteContext context, final List<WriteValue> writeValues) {
        final List<StatusCode> results = Lists.newArrayListWithCapacity(writeValues.size());

        for (final WriteValue writeValue : writeValues) {
            try {
                final ServerNode node = this.nodeManager.getNode(writeValue.getNodeId()).orElseThrow(() -> new UaException(StatusCodes.Bad_NodeIdUnknown));

                node.writeAttribute(new AttributeContext(context), writeValue.getAttributeId(), writeValue.getValue(), writeValue.getIndexRange());

                if (LOG.isTraceEnabled()) {
                    final Variant variant = writeValue.getValue().getValue();
                    final Object o = variant != null ? variant.getValue() : null;
                    LOG.trace("Wrote value={} to attributeId={} of {}", o, writeValue.getAttributeId(), writeValue.getNodeId());
                }

                results.add(StatusCode.GOOD);
            } catch (final UaException e) {
                results.add(e.getStatusCode());
            }
        }

        context.complete(results);
    }

    @Override
    public void onDataItemsCreated(final List<DataItem> dataItems) {
        this.subscriptionModel.onDataItemsCreated(dataItems);
    }

    @Override
    public void onDataItemsModified(final List<DataItem> dataItems) {
        this.subscriptionModel.onDataItemsModified(dataItems);
    }

    @Override
    public void onDataItemsDeleted(final List<DataItem> dataItems) {
        this.subscriptionModel.onDataItemsDeleted(dataItems);
    }

    @Override
    public void onMonitoringModeChanged(final List<MonitoredItem> monitoredItems) {
        this.subscriptionModel.onMonitoringModeChanged(monitoredItems);
    }

    public CamelServerItem getOrAddItem(final String itemId) {
        synchronized (this) {
            CamelServerItem item = this.itemMap.get(itemId);
            if (item == null) {
                item = new CamelServerItem(itemId, this.nodeManager, this.namespaceIndex, this.itemsObject);
                this.itemMap.put(itemId, item);
            }
            return item;
        }
    }

}
