/*
 * The MIT License
 *
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package hudson.model;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

/**
 * List of {@link Resource}s that an activity needs.
 *
 * <p>
 * There are two ways to access resources. Read and write.
 * As with usual reader/writer pattern, multiple read accesses can
 * co-exist concurrently, but write access requires exclusive access
 * (the number of allowed concurrent write activity is determined by
 * {@link Resource#numConcurrentWrite}.
 *
 * @author Kohsuke Kawaguchi
 * @since 1.121
 */
public final class ResourceList {

    private static final Logger LOGGER = Logger.getLogger(ResourceList.class.getName());

    /**
     * All resources (R/W.)
     */
    private final Set<Resource> all = new HashSet<>();

    /**
     * Write accesses. Values are the # of write counts that this list uses.
     *
     * The values are mostly supposed to be 1, but when {@link ResourceController}
     * uses a list to keep track of the union of all the activities, it can get larger.
     */
    private final Map<Resource, Integer> write = new HashMap<>();
    private static final Integer MAX_INT = Integer.MAX_VALUE;

    /**
     * Creates union of all resources.
     */
    public static ResourceList union(ResourceList... lists) {
        return union(Arrays.asList(lists));
    }

    /**
     * Creates union of all resources.
     */
    public static ResourceList union(Collection<ResourceList> lists) {
        switch (lists.size()) {
        case 0:
            return EMPTY;
        case 1:
            return lists.iterator().next();
        default:
            ResourceList r = new ResourceList();
            for (ResourceList l : lists) {
                r.all.addAll(l.all);
                for (Map.Entry<Resource, Integer> e : l.write.entrySet())
                    r.write.put(e.getKey(), unbox(r.write.get(e.getKey())) + e.getValue());
            }
            return r;
        }
    }

    /**
     * Adds a resource for read access.
     */
    public ResourceList r(Resource r) {
        all.add(r);
        return this;
    }

    /**
     * Adds a resource for write access.
     */
    public ResourceList w(Resource r) {
        all.add(r);
        write.put(r, unbox(write.get(r)) + 1);
        return this;
    }

    /**
     * Checks if this resource list and that resource list has any conflicting
     * resource access.
     */
    public boolean isCollidingWith(ResourceList that) {
        return getConflict(that) != null;
    }

    /**
     * Returns the resource in this list that's colliding with the given resource list.
     */
    public Resource getConflict(ResourceList that) {
        Resource r = _getConflict(this, that);
        if (r != null)     return r;
        return _getConflict(that, this);
    }

    private Resource _getConflict(ResourceList lhs, ResourceList rhs) {
        for (Map.Entry<Resource, Integer> r : lhs.write.entrySet()) {
            for (Resource l : rhs.all) {
                Integer v = rhs.write.get(l);
                if (v != null) // this is write/write conflict.
                    v += r.getValue();
                else // Otherwise set it to a very large value, since it's read/write conflict
                    v = MAX_INT;
                if (r.getKey().isCollidingWith(l, unbox(v))) {
                    LOGGER.info("Collision with " + r + " and " + l);
                    return r.getKey();
                }
            }
        }
        return null;
    }

    @Override
    public String toString() {
        Map<Resource, String> m = new HashMap<>();
        for (Resource r : all)
            m.put(r, "R");
        for (Map.Entry<Resource, Integer> e : write.entrySet())
            m.put(e.getKey(), "W" + e.getValue());
        return m.toString();
    }

    /**
     * {@link Integer} unbox operation that treats null as 0.
     */
    private static int unbox(Integer x) {
        return x == null ? 0 : x;
    }

    /**
     * Empty resource list.
     */
    public static final ResourceList EMPTY = new ResourceList();
}
