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

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import com.google.common.base.Preconditions;
import org.eclipse.milo.opcua.sdk.core.Reference;
import org.eclipse.milo.opcua.sdk.core.nodes.MethodNode;
import org.eclipse.milo.opcua.sdk.core.nodes.MethodNodeProperties;
import org.eclipse.milo.opcua.sdk.core.nodes.Node;
import org.eclipse.milo.opcua.sdk.core.nodes.ObjectNode;
import org.eclipse.milo.opcua.sdk.server.api.NodeManager;
import org.eclipse.milo.opcua.sdk.server.api.methods.MethodInvocationHandler;
import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilter;
import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilterChain;
import org.eclipse.milo.opcua.stack.core.AttributeId;
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.unsigned.UInteger;
import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass;
import org.eclipse.milo.opcua.stack.core.types.structured.Argument;
import org.jetbrains.annotations.Nullable;

import static org.eclipse.milo.opcua.sdk.core.Reference.ALWAYS_GENERATES_EVENT_PREDICATE;
import static org.eclipse.milo.opcua.sdk.core.Reference.HAS_MODELLING_RULE_PREDICATE;
import static org.eclipse.milo.opcua.sdk.core.Reference.HAS_PROPERTY_PREDICATE;
import static org.eclipse.milo.opcua.sdk.core.util.StreamUtil.opt2stream;

public class UaMethodNode extends UaNode implements MethodNode {

    private volatile MethodInvocationHandler handler = MethodInvocationHandler.NOT_IMPLEMENTED;

    private volatile Boolean executable;
    private volatile Boolean userExecutable;

    public UaMethodNode(
        UaNodeContext context,
        NodeId nodeId,
        QualifiedName browseName,
        LocalizedText displayName,
        LocalizedText description,
        UInteger writeMask,
        UInteger userWriteMask,
        Boolean executable,
        Boolean userExecutable) {

        super(context, nodeId, NodeClass.Method,
            browseName, displayName, description, writeMask, userWriteMask);

        this.executable = executable;
        this.userExecutable = userExecutable;
    }

    @Override
    public Boolean isExecutable() {
        return (Boolean) filterChain.getAttribute(this, AttributeId.Executable);
    }

    @Override
    public Boolean isUserExecutable() {
        return (Boolean) filterChain.getAttribute(this, AttributeId.UserExecutable);
    }

    @Override
    public void setExecutable(Boolean executable) {
        filterChain.setAttribute(this, AttributeId.Executable, executable);
    }

    @Override
    public void setUserExecutable(Boolean userExecutable) {
        filterChain.setAttribute(this, AttributeId.UserExecutable, userExecutable);
    }

    @Override
    public synchronized Object getAttribute(AttributeId attributeId) {
        switch (attributeId) {
            case Executable:
                return executable;

            case UserExecutable:
                return userExecutable;

            default:
                return super.getAttribute(attributeId);
        }
    }

    @Override
    public synchronized void setAttribute(AttributeId attributeId, Object value) {
        switch (attributeId) {
            case Executable:
                executable = (Boolean) value;
                break;

            case UserExecutable:
                userExecutable = (Boolean) value;
                break;

            default:
                super.setAttribute(attributeId, value);
                return; // prevent firing an attribute change
        }

        fireAttributeChanged(attributeId, value);
    }

    public List<Node> getPropertyNodes() {
        return getReferences().stream()
            .filter(HAS_PROPERTY_PREDICATE)
            .flatMap(r -> opt2stream(getManagedNode(r.getTargetNodeId())))
            .collect(Collectors.toList());
    }

    public Optional<ObjectNode> getModellingRuleNode() {
        Node node = getReferences().stream()
            .filter(HAS_MODELLING_RULE_PREDICATE)
            .findFirst()
            .flatMap(r -> getManagedNode(r.getTargetNodeId()))
            .orElse(null);

        ObjectNode objectNode = (node instanceof ObjectNode) ? (ObjectNode) node : null;

        return Optional.ofNullable(objectNode);
    }

    public List<Node> getAlwaysGeneratesEventNodes() {
        return getReferences().stream()
            .filter(ALWAYS_GENERATES_EVENT_PREDICATE)
            .flatMap(r -> opt2stream(getManagedNode(r.getTargetNodeId())))
            .collect(Collectors.toList());
    }

    public MethodInvocationHandler getInvocationHandler() {
        return handler;
    }

    public void setInvocationHandler(MethodInvocationHandler handler) {
        this.handler = handler;
    }

    /**
     * Get the value of the NodeVersion Property, if it exists.
     *
     * @return the value of the NodeVersion Property, if it exists.
     * @see MethodNodeProperties#NodeVersion
     */
    @Nullable
    public String getNodeVersion() {
        return getProperty(MethodNodeProperties.NodeVersion).orElse(null);
    }

    /**
     * Get the value of the InputArguments Property, if it exists.
     *
     * @return the value of the InputArguments Property, if it exists.
     * @see MethodNodeProperties#InputArguments
     */
    @Nullable
    public Argument[] getInputArguments() {
        return getProperty(MethodNodeProperties.InputArguments).orElse(null);
    }

    /**
     * Get the value of the OutputArguments Property, if it exists.
     *
     * @return the value of the OutputArguments Property, if it exists.
     * @see MethodNodeProperties#OutputArguments
     */
    @Nullable
    public Argument[] getOutputArguments() {
        return getProperty(MethodNodeProperties.OutputArguments).orElse(null);
    }

    /**
     * Set the value of the NodeVersion Property.
     * <p>
     * A PropertyNode will be created if it does not already exist.
     *
     * @param nodeVersion the value to set.
     * @see MethodNodeProperties#NodeVersion
     */
    public void setNodeVersion(String nodeVersion) {
        setProperty(MethodNodeProperties.NodeVersion, nodeVersion);
    }

    /**
     * Set the value of the InputArguments Property.
     * <p>
     * A PropertyNode will be created if it does not already exist.
     *
     * @param inputArguments the value to set.
     * @see MethodNodeProperties#InputArguments
     */
    public void setInputArguments(Argument[] inputArguments) {
        setProperty(MethodNodeProperties.InputArguments, inputArguments);
    }

    /**
     * Set the value of the OutputArguments Property.
     * <p>
     * A PropertyNode will be created if it does not already exist.
     *
     * @param outputArguments the value to set.
     * @see MethodNodeProperties#OutputArguments
     */
    public void setOutputArguments(Argument[] outputArguments) {
        setProperty(MethodNodeProperties.OutputArguments, outputArguments);
    }

    /**
     * @return a new {@link UaMethodNodeBuilder}.
     */
    public static UaMethodNodeBuilder builder(UaNodeContext context) {
        return new UaMethodNodeBuilder(context);
    }

    /**
     * Build a {@link UaMethodNode} using the {@link UaMethodNodeBuilder} supplied to the
     * {@code build} function.
     *
     * @param context a {@link UaNodeContext}.
     * @param build   a function that accepts a {@link UaMethodNodeBuilder} and uses it to build
     *                and return a {@link UaMethodNode}.
     * @return a {@link UaMethodNode} built using the supplied {@link UaMethodNodeBuilder}.
     */
    public static UaMethodNode build(
        UaNodeContext context,
        Function<UaMethodNodeBuilder, UaMethodNode> build
    ) {

        UaMethodNodeBuilder builder = new UaMethodNodeBuilder(context);

        return build.apply(builder);
    }

    public static class UaMethodNodeBuilder implements Supplier<UaMethodNode> {

        private final List<AttributeFilter> attributeFilters = new ArrayList<>();

        private final List<Reference> references = new ArrayList<>();

        private NodeId nodeId;
        private QualifiedName browseName;
        private LocalizedText displayName;
        private LocalizedText description = LocalizedText.NULL_VALUE;
        private UInteger writeMask = UInteger.MIN;
        private UInteger userWriteMask = UInteger.MIN;

        private boolean executable = true;
        private boolean userExecutable = true;

        private final UaNodeContext context;

        public UaMethodNodeBuilder(UaNodeContext context) {
            this.context = context;
        }

        /**
         * @see #build()
         */
        @Override
        public UaMethodNode get() {
            return build();
        }

        /**
         * Build and return the {@link UaMethodNode}.
         * <p>
         * The following fields are required: NodeId, BrowseName, DisplayName.
         *
         * @return a {@link UaMethodNode} built from the configuration of this builder.
         */
        public UaMethodNode build() {
            Preconditions.checkNotNull(nodeId, "NodeId cannot be null");
            Preconditions.checkNotNull(browseName, "BrowseName cannot be null");
            Preconditions.checkNotNull(displayName, "DisplayName cannot be null");

            UaMethodNode node = new UaMethodNode(
                context,
                nodeId,
                browseName,
                displayName,
                description,
                writeMask,
                userWriteMask,
                executable,
                userExecutable
            );

            references.forEach(node::addReference);

            node.getFilterChain().addLast(attributeFilters);

            return node;
        }

        /**
         * Build the {@link UaMethodNode} using the configured values and add it to the
         * {@link NodeManager} from the {@link UaNodeContext}.
         *
         * @return a {@link UaMethodNode} built from the configured values.
         * @see #build()
         */
        public UaMethodNode buildAndAdd() {
            UaMethodNode node = build();
            context.getNodeManager().addNode(node);
            return node;
        }

        public UaMethodNodeBuilder setNodeId(NodeId nodeId) {
            this.nodeId = nodeId;
            return this;
        }

        public UaMethodNodeBuilder setBrowseName(QualifiedName browseName) {
            this.browseName = browseName;
            return this;
        }

        public UaMethodNodeBuilder setDisplayName(LocalizedText displayName) {
            this.displayName = displayName;
            return this;
        }

        public UaMethodNodeBuilder setDescription(LocalizedText description) {
            this.description = description;
            return this;
        }

        public UaMethodNodeBuilder setWriteMask(UInteger writeMask) {
            this.writeMask = writeMask;
            return this;
        }

        public UaMethodNodeBuilder setUserWriteMask(UInteger userWriteMask) {
            this.userWriteMask = userWriteMask;
            return this;
        }

        public UaMethodNodeBuilder setExecutable(boolean executable) {
            this.executable = executable;
            return this;
        }

        public UaMethodNodeBuilder setUserExecutable(boolean userExecutable) {
            this.userExecutable = userExecutable;
            return this;
        }

        public NodeId getNodeId() {
            return nodeId;
        }

        public QualifiedName getBrowseName() {
            return browseName;
        }

        public LocalizedText getDisplayName() {
            return displayName;
        }

        public LocalizedText getDescription() {
            return description;
        }

        public UInteger getWriteMask() {
            return writeMask;
        }

        public UInteger getUserWriteMask() {
            return userWriteMask;
        }

        public boolean isExecutable() {
            return executable;
        }

        public boolean isUserExecutable() {
            return userExecutable;
        }

        /**
         * Add an {@link AttributeFilter} that will be added to the node's
         * {@link AttributeFilterChain} when it's built.
         * <p>
         * The order filters are added in this builder is maintained.
         *
         * @param attributeFilter the {@link AttributeFilter} to add.
         * @return this {@link UaMethodNodeBuilder}.
         */
        public UaMethodNodeBuilder addAttributeFilter(AttributeFilter attributeFilter) {
            attributeFilters.add(attributeFilter);
            return this;
        }

        /**
         * Add a {@link Reference} to the node when it's built.
         *
         * @param reference the {@link Reference} to add.
         * @return this {@link UaMethodNodeBuilder}.
         */
        public UaMethodNodeBuilder addReference(Reference reference) {
            references.add(reference);
            return this;
        }

    }

}
