/*
 * 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.opcua.sdk.server.api;

import java.util.Collections;
import java.util.List;

import com.google.common.collect.Lists;
import org.eclipse.milo.opcua.sdk.core.Reference;
import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
import org.eclipse.milo.opcua.sdk.server.UaNodeManager;
import org.eclipse.milo.opcua.sdk.server.api.methods.MethodInvocationHandler;
import org.eclipse.milo.opcua.sdk.server.api.services.MethodServices;
import org.eclipse.milo.opcua.sdk.server.nodes.AttributeContext;
import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaNodeContext;
import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectTypeNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaServerNode;
import org.eclipse.milo.opcua.sdk.server.nodes.factories.NodeFactory;
import org.eclipse.milo.opcua.stack.core.AttributeId;
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.DiagnosticInfo;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
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.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.CallMethodRequest;
import org.eclipse.milo.opcua.stack.core.types.structured.CallMethodResult;
import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId;
import org.eclipse.milo.opcua.stack.core.types.structured.ViewDescription;
import org.eclipse.milo.opcua.stack.core.types.structured.WriteValue;
import org.eclipse.milo.opcua.stack.core.util.Unit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class ManagedAddressSpace implements AddressSpace {

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

    private final UaNodeContext nodeContext;
    private final NodeFactory nodeFactory;

    private final OpcUaServer server;
    private final UaNodeManager nodeManager;

    public ManagedAddressSpace(OpcUaServer server) {
        this(server, new UaNodeManager());
    }

    public ManagedAddressSpace(OpcUaServer server, UaNodeManager nodeManager) {
        this.server = server;

        this.nodeManager = nodeManager;

        nodeContext = new UaNodeContext() {
            @Override
            public OpcUaServer getServer() {
                return ManagedAddressSpace.this.getServer();
            }

            @Override
            public NodeManager<UaNode> getNodeManager() {
                return ManagedAddressSpace.this.getNodeManager();
            }
        };

        nodeFactory = createNodeFactory();
    }

    protected NodeFactory createNodeFactory() {
        return new NodeFactory(nodeContext);
    }

    protected OpcUaServer getServer() {
        return server;
    }

    protected UaNodeContext getNodeContext() {
        return nodeContext;
    }

    protected NodeFactory getNodeFactory() {
        return nodeFactory;
    }

    protected UaNodeManager getNodeManager() {
        return nodeManager;
    }

    @Override
    public void browse(BrowseContext context, ViewDescription viewDescription, NodeId nodeId) {
        if (nodeManager.containsNode(nodeId)) {
            List<Reference> references = nodeManager.getReferences(nodeId);

            logger.debug("Browsed {} references for {}", references.size(), nodeId);

            context.success(references);
        } else {
            context.failure(StatusCodes.Bad_NodeIdUnknown);
        }
    }

    @Override
    public void getReferences(BrowseContext context, ViewDescription viewDescription, NodeId nodeId) {
        List<Reference> references = nodeManager.getReferences(nodeId);

        logger.debug("Got {} references for {}", references.size(), nodeId);

        context.success(references);
    }

    @Override
    public void registerNodes(RegisterNodesContext context, List<NodeId> nodeIds) {
        context.success(nodeIds);
    }

    @Override
    public void unregisterNodes(UnregisterNodesContext context, List<NodeId> nodeIds) {
        context.success(Collections.nCopies(nodeIds.size(), Unit.VALUE));
    }

    @Override
    public void read(
        ReadContext context,
        Double maxAge,
        TimestampsToReturn timestamps,
        List<ReadValueId> readValueIds
    ) {

        List<DataValue> results = Lists.newArrayListWithCapacity(readValueIds.size());

        for (ReadValueId readValueId : readValueIds) {
            UaServerNode node = nodeManager.get(readValueId.getNodeId());

            if (node != null) {
                DataValue value = node.readAttribute(
                    new AttributeContext(context),
                    readValueId.getAttributeId(),
                    timestamps,
                    readValueId.getIndexRange(),
                    readValueId.getDataEncoding()
                );

                logger.debug("Read value {} from attribute {} of {}",
                    value.getValue().getValue(),
                    AttributeId.from(readValueId.getAttributeId())
                        .map(Object::toString).orElse("unknown"),
                    node.getNodeId()
                );

                results.add(value);
            } else {
                results.add(new DataValue(StatusCodes.Bad_NodeIdUnknown));
            }
        }

        context.success(results);
    }

    @Override
    public void write(
        WriteContext context,
        List<WriteValue> writeValues
    ) {

        List<StatusCode> results = Lists.newArrayListWithCapacity(writeValues.size());

        for (WriteValue writeValue : writeValues) {
            UaServerNode node = nodeManager.get(writeValue.getNodeId());

            if (node != null) {
                try {
                    node.writeAttribute(
                        new AttributeContext(context),
                        writeValue.getAttributeId(),
                        writeValue.getValue(),
                        writeValue.getIndexRange()
                    );

                    results.add(StatusCode.GOOD);

                    logger.debug(
                        "Wrote value {} to {} attribute of {}",
                        writeValue.getValue().getValue(),
                        AttributeId.from(writeValue.getAttributeId())
                            .map(Object::toString).orElse("unknown"),
                        node.getNodeId()
                    );
                } catch (UaException e) {
                    logger.error("Unable to write value={}", writeValue.getValue(), e);
                    results.add(e.getStatusCode());
                }
            } else {
                results.add(new StatusCode(StatusCodes.Bad_NodeIdUnknown));
            }
        }

        context.success(results);
    }

    /**
     * Invoke one or more methods belonging to this {@link MethodServices}.
     *
     * @param context  the {@link CallContext}.
     * @param requests The {@link CallMethodRequest}s for the methods to invoke.
     */
    @Override
    public void call(CallContext context, List<CallMethodRequest> requests) {
        List<CallMethodResult> results = Lists.newArrayListWithCapacity(requests.size());

        for (CallMethodRequest request : requests) {
            try {
                MethodInvocationHandler handler = getInvocationHandler(
                    request.getObjectId(),
                    request.getMethodId()
                );

                results.add(handler.invoke(context, request));
            } catch (UaException e) {
                results.add(
                    new CallMethodResult(
                        e.getStatusCode(),
                        new StatusCode[0],
                        new DiagnosticInfo[0],
                        new Variant[0]
                    )
                );
            } catch (Throwable t) {
                LoggerFactory.getLogger(getClass())
                    .error("Uncaught Throwable invoking method handler for methodId={}.", request.getMethodId(), t);

                results.add(
                    new CallMethodResult(
                        new StatusCode(StatusCodes.Bad_InternalError),
                        new StatusCode[0],
                        new DiagnosticInfo[0],
                        new Variant[0]
                    )
                );
            }
        }

        context.success(results);
    }

    /**
     * Get the {@link MethodInvocationHandler} for the method identified by {@code methodId}.
     *
     * @param objectId the {@link NodeId} identifying the object the method will be invoked on.
     * @param methodId the {@link NodeId} identifying the method.
     * @return the {@link MethodInvocationHandler} for {@code methodId}.
     * @throws UaException a {@link UaException} containing the appropriate operation result if
     *                     either the object or method can't be found.
     */
    protected MethodInvocationHandler getInvocationHandler(NodeId objectId, NodeId methodId) throws UaException {
        UaNode node = nodeManager.getNode(objectId)
            .orElseThrow(() -> new UaException(StatusCodes.Bad_NodeIdUnknown));

        UaMethodNode methodNode = null;

        if (node instanceof UaObjectNode) {
            UaObjectNode objectNode = (UaObjectNode) node;

            methodNode = objectNode.findMethodNode(methodId);
        } else if (node instanceof UaObjectTypeNode) {
            UaObjectTypeNode objectTypeNode = (UaObjectTypeNode) node;

            methodNode = objectTypeNode.findMethodNode(methodId);
        }

        if (methodNode != null) {
            return methodNode.getInvocationHandler();
        } else {
            throw new UaException(StatusCodes.Bad_MethodInvalid);
        }
    }

}
