/*
 * 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.nodes.filters;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedDeque;

import org.eclipse.milo.opcua.sdk.server.Session;
import org.eclipse.milo.opcua.sdk.server.nodes.UaNode;
import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilterContext.GetAttributeContext;
import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilterContext.SetAttributeContext;
import org.eclipse.milo.opcua.stack.core.AttributeId;
import org.jetbrains.annotations.Nullable;

public class AttributeFilterChain {

    private final ConcurrentLinkedDeque<AttributeFilter> filters = new ConcurrentLinkedDeque<>();

    /**
     * Create an empty {@link AttributeFilterChain}.
     */
    public AttributeFilterChain() {}

    /**
     * Create an {@link AttributeFilterChain} with {@code filter} added to it.
     *
     * @param filter the filter to add.
     */
    public AttributeFilterChain(AttributeFilter filter) {
        filters.add(filter);
    }

    /**
     * Create an {@link AttributeFilterChain} with {@code filters} added to it.
     *
     * @param filters the filters to add.
     */
    public AttributeFilterChain(List<AttributeFilter> filters) {
        this.filters.addAll(filters);
    }

    /**
     * Get the value for the attribute identified by {@code attributeId} from {@code node}.
     *
     * @param node        the {@link UaNode} to get the attribute value from.
     * @param attributeId the {@link AttributeId} of the attribute to get the value of.
     * @return the value for the attribute identified by {@code attributeId} from {@code node}.
     */
    public Object getAttribute(UaNode node, AttributeId attributeId) {
        return getAttribute(null, node, attributeId);
    }

    /**
     * Get the value for the attribute identified by {@code attributeId} from {@code node}.
     *
     * @param session     the {@link Session} the attribute request is originating from, if there is one.
     * @param node        the {@link UaNode} to get the attribute value from.
     * @param attributeId the {@link AttributeId} of the attribute to get the value of.
     * @return the value for the attribute identified by {@code attributeId} from {@code node}.
     */
    public Object getAttribute(@Nullable Session session, UaNode node, AttributeId attributeId) {
        Iterator<AttributeFilter> filterIterator = filters.iterator();

        AttributeFilter filter = filterIterator.hasNext() ?
            filterIterator.next() :
            AttributeFilter.DEFAULT_INSTANCE;

        GetAttributeContext ctx = new GetAttributeContext(session, node, filterIterator);

        Object value = filter.getAttribute(ctx, attributeId);

        if (ctx.isObservable()) {
            node.fireAttributeChanged(attributeId, value);
        }

        return value;
    }

    /**
     * Set the value for the attribute identified by {@code attributeId}.
     *
     * @param node        the {@link UaNode} to set the attribute value on.
     * @param attributeId the {@link AttributeId} of the attribute to set the value of.
     * @param value       the value to set.
     */
    public void setAttribute(UaNode node, AttributeId attributeId, Object value) {
        setAttribute(null, node, attributeId, value);
    }

    /**
     * Set the value for the attribute identified by {@code attributeId}.
     *
     * @param session     the {@link Session} the attribute request is originating from, if there is one.
     * @param node        the {@link UaNode} to set the attribute value on.
     * @param attributeId the {@link AttributeId} of the attribute to set the value of.
     * @param value       the value to set.
     */
    public void setAttribute(@Nullable Session session, UaNode node, AttributeId attributeId, Object value) {
        Iterator<AttributeFilter> filterIterator = filters.iterator();

        AttributeFilter filter = filterIterator.hasNext() ?
            filterIterator.next() :
            AttributeFilter.DEFAULT_INSTANCE;

        SetAttributeContext ctx = new SetAttributeContext(session, node, filterIterator);

        filter.setAttribute(ctx, attributeId, value);
    }

    /**
     * Add {@code attributeFilter} to the front of this filter chain.
     *
     * @param attributeFilter the {@link AttributeFilter} to add.
     * @return this {@link AttributeFilterChain}.
     */
    public AttributeFilterChain addFirst(AttributeFilter attributeFilter) {
        filters.addFirst(attributeFilter);

        return this;
    }

    /**
     * Add {@code attributeFilters} (sequentially) to the front of this filter chain.
     *
     * @param attributeFilters the {@link AttributeFilter}s to add.
     * @return this {@link AttributeFilterChain}.
     */
    public AttributeFilterChain addFirst(AttributeFilter... attributeFilters) {
        Arrays.stream(attributeFilters).forEach(this::addFirst);

        return this;
    }

    /**
     * Add {@code attributeFilters} (sequentially) to the front of this filter chain.
     *
     * @param attributeFilters the {@link AttributeFilter}s to add.
     * @return this {@link AttributeFilterChain}.
     */
    public AttributeFilterChain addFirst(Collection<AttributeFilter> attributeFilters) {
        attributeFilters.forEach(this::addFirst);

        return this;
    }

    /**
     * Add {@code attributeFilter} to the end of this filter chain.
     *
     * @param attributeFilter the {@link AttributeFilter} to add.
     * @return this {@link AttributeFilterChain}.
     */
    public AttributeFilterChain addLast(AttributeFilter attributeFilter) {
        filters.addLast(attributeFilter);

        return this;
    }

    /**
     * Add {@code attributeFilters} (sequentially) to the end of this filter chain.
     *
     * @param attributeFilters the {@link AttributeFilter}s to add.
     * @return this {@link AttributeFilterChain}.
     */
    public AttributeFilterChain addLast(AttributeFilter... attributeFilters) {
        Arrays.stream(attributeFilters).forEach(this::addLast);

        return this;
    }

    /**
     * Add {@code attributeFilters} (sequentially) to the end of this filter chain.
     *
     * @param attributeFilters the {@link AttributeFilter}s to add.
     * @return this {@link AttributeFilterChain}.
     */
    public AttributeFilterChain addLast(Collection<AttributeFilter> attributeFilters) {
        attributeFilters.forEach(this::addLast);

        return this;
    }

    /**
     * Get a List containing all the {@link AttributeFilter}s belonging to this chain.
     *
     * @return a List containing all the {@link AttributeFilter}s belonging to this chain.
     */
    public List<AttributeFilter> getFilters() {
        return new ArrayList<>(filters);
    }

}
