/*
 * 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.karaf.features.internal.region;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.equinox.region.Region;
import org.eclipse.equinox.region.RegionDigraphVisitor;
import org.eclipse.equinox.region.RegionFilter;

/**
 * {@link AbstractRegionDigraphVisitor} is an abstract base class for {@link RegionDigraphVisitor} implementations
 */
public abstract class AbstractRegionDigraphVisitor<C> implements RegionDigraphVisitor {

    private final Collection<C> allCandidates;
    private final Deque<Set<C>> allowedDeque = new ArrayDeque<>();
    private final Deque<Collection<C>> filteredDeque = new ArrayDeque<>();
    private Set<C> allowed = new HashSet<>();

    public AbstractRegionDigraphVisitor(Collection<C> candidates) {
        this.allCandidates = candidates;
    }

    public Collection<C> getAllowed() {
        return allowed;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean visit(Region region) {
        Collection<C> candidates = filteredDeque.isEmpty() ? allCandidates : filteredDeque.peek();
        for (C candidate : candidates) {
            if (contains(region, candidate)) {
                allowed.add(candidate);
            }
        }
        // there is no need to traverse edges of this region,
        // it contains all the remaining filtered candidates
        return !allowed.containsAll(candidates);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean preEdgeTraverse(RegionFilter regionFilter) {
        // Find the candidates filtered by the previous edge
        Collection<C> filtered = filteredDeque.isEmpty() ? allCandidates : filteredDeque.peek();
        Collection<C> candidates = new ArrayList<>(filtered);
        // remove any candidates contained in the current region
        candidates.removeAll(allowed);
        // apply the filter across remaining candidates
        candidates.removeIf(candidate -> !isAllowed(candidate, regionFilter));
        if (candidates.isEmpty()) {
            return false; // this filter does not apply; avoid traversing this edge
        }
        // push the filtered candidates for the next region
        filteredDeque.push(candidates);
        // push the allowed
        allowedDeque.push(allowed);
        allowed = new HashSet<>();
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void postEdgeTraverse(RegionFilter regionFilter) {
        filteredDeque.poll();
        Collection<C> candidates = allowed;
        allowed = allowedDeque.pop();
        allowed.addAll(candidates);
    }

    /**
     * Determines whether the given region contains the given candidate.
     *
     * @param region    the {@link Region}
     * @param candidate the candidate
     * @return <code>true</code> if and only if the given region contains the given candidate
     */
    protected abstract boolean contains(Region region, C candidate);

    /**
     * Determines whether the given candidate is allowed by the given {@link RegionFilter}.
     *
     * @param candidate the candidate
     * @param filter    the filter
     * @return <code>true</code> if and only if the given candidate is allowed by the given filter
     */
    protected abstract boolean isAllowed(C candidate, RegionFilter filter);
}