Resource.java

package nl.tudelft.simulation.dsol.formalisms.flow;

import java.util.HashMap;
import java.util.Map;

import org.djutils.event.EventType;
import org.djutils.exceptions.Throw;
import org.djutils.metadata.MetaData;
import org.djutils.metadata.ObjectDescriptor;

import nl.tudelft.simulation.dsol.SimRuntimeException;
import nl.tudelft.simulation.dsol.simulators.DevsSimulatorInterface;
import nl.tudelft.simulation.dsol.statistics.SimPersistent;

/**
 * A resource defines a shared and limited amount that can be claimed by, e.g., an entity.
 * <p>
 * Copyright (c) 2002-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
 * for project information <a href="https://simulation.tudelft.nl/dsol/manual/" target="_blank">DSOL Manual</a>. The DSOL
 * project is distributed under a three-clause BSD-style license, which can be found at
 * <a href="https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">DSOL License</a>.
 * </p>
 * @author <a href="https://www.linkedin.com/in/peterhmjacobs">Peter Jacobs </a>
 * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
 * @param <T> the simulation time type
 * @param <R> the resource type
 */
public abstract class Resource<T extends Number & Comparable<T>, R extends Resource<T, R>> extends Block<T>
{
    /** persistent statistic for the resource utilization. */
    private SimPersistent<T> utilizationStatistic = null;

    /** The release type that indicates how the queue is searched for requests when capacity is available. */
    private ReleaseType releaseType = ReleaseType.FIRST_ONLY;

    /** the queue of requests for this resource. */
    private final Queue<T> requestQueue;

    /** UTILIZATION_EVENT is fired on activity that decreases or increases the utilization. */
    public static final EventType UTILIZATION_EVENT = new EventType(new MetaData("UTILIZATION_EVENT", "Utilization changed",
            new ObjectDescriptor("newUtilization", "new utilization", Double.class)));

    /**
     * Create a new Resource with a capacity and a specific request comparator, e.g., LIFO or sorted on an attribute.
     * @param id the id of this resource
     * @param simulator the simulator
     */
    public Resource(final String id, final DevsSimulatorInterface<T> simulator)
    {
        super(id, simulator);
        this.requestQueue = new Queue<T>(id + "_queue", simulator);
    }

    /**
     * Return the request queue for this resource.
     * @return the request queue for this resource
     */
    public Queue<T> getRequestQueue()
    {
        return this.requestQueue;
    }

    /**
     * Set the release type that indicates how the queue is searched for requests when capacity is available.
     * @param releaseType the new release type
     * @return this object for method chaining
     */
    @SuppressWarnings("unchecked")
    public R setReleaseType(final ReleaseType releaseType)
    {
        this.releaseType = releaseType;
        return (R) this;
    }

    /**
     * Return the release type that indicates how the queue is searched for requests when capacity is available.
     * @return the release type that indicates how the queue is searched for requests when capacity is available
     */
    public ReleaseType getReleaseType()
    {
        return this.releaseType;
    }

    /**
     * Return the maximum, and thus original capacity of the resource.
     * @return capacity the maximum, and thus original capacity of the resource.
     */
    public abstract Number getCapacity();

    /**
     * Return the amount of currently claimed capacity.
     * @return the amount of currently claimed capacity.
     */
    public abstract Number getClaimedCapacity();

    /**
     * Turn on the default statistics for this queue block.
     * @return the Queue instance for method chaining
     */
    @SuppressWarnings("unchecked")
    public R setDefaultStatistics()
    {
        if (!hasDefaultStatistics())
        {
            this.utilizationStatistic = new SimPersistent<>("Resource.Unilization:" + getBlockNumber(),
                    getId() + " utilization", getSimulator().getModel(), this, UTILIZATION_EVENT);
            fireTimedEvent(UTILIZATION_EVENT,
                    getCapacity().doubleValue() == 0.0 ? 0.0 : getClaimedCapacity().doubleValue() / getCapacity().doubleValue(),
                    getSimulator().getSimulatorTime());
            this.requestQueue.setDefaultStatistics();
        }
        return (R) this;
    }

    /**
     * Return whether statistics are turned on for this Storage block.
     * @return whether statistics are turned on for this Storage block.
     */
    public boolean hasDefaultStatistics()
    {
        return this.utilizationStatistic != null;
    }

    /**
     * Return the utilization statistic.
     * @return the utilization statistic
     */
    public SimPersistent<T> getUtilizationStatistic()
    {
        return this.utilizationStatistic;
    }

    /**
     * Resource with floating point capacity.
     * @param <T> the time type
     */
    public static class DoubleCapacity<T extends Number & Comparable<T>> extends Resource<T, DoubleCapacity<T>>
    {
        /** capacity defines the maximum capacity of the resource. */
        private double capacity;

        /** claimedCapacity defines the currently claimed capacity. */
        private double claimedCapacity = 0.0;

        /** the claims per entity; will be removed if claim fully released. */
        private Map<Entity<T>, Double> claimMap = new HashMap<>();

        /**
         * Create a new Resource with floating point capacity and a specific request comparator, e.g., LIFO or sorted on an
         * attribute.
         * @param id the id of this resource
         * @param simulator the simulator
         * @param capacity the capacity of the resource
         */
        public DoubleCapacity(final String id, final DevsSimulatorInterface<T> simulator, final double capacity)
        {
            super(id, simulator);
            this.capacity = capacity;
        }

        @Override
        public Double getCapacity()
        {
            return this.capacity;
        }

        @Override
        public Double getClaimedCapacity()
        {
            return this.claimedCapacity;
        }

        /**
         * Return the currently available capacity on this resource. This method is implemented as
         * <code>return this.getCapacity()-this.getClaimedCapacity()</code>
         * @return the currently available capacity on this resource.
         */
        public double getAvailableCapacity()
        {
            return this.capacity - this.claimedCapacity;
        }

        /**
         * Add the amount to the claimed capacity.
         * @param amount the amount which is added to the claimed capacity
         */
        private synchronized void changeClaimedCapacity(final double amount)
        {
            this.claimedCapacity += amount;
            if (Math.abs(this.claimedCapacity) < 8 * Math.ulp(amount))
                this.claimedCapacity = 0.0;
            fireTimedEvent(UTILIZATION_EVENT,
                    getCapacity().doubleValue() == 0.0 ? 0.0 : getClaimedCapacity().doubleValue() / getCapacity().doubleValue(),
                    getSimulator().getSimulatorTime());
        }

        /**
         * Set the capacity of the resource to a new value. The <code>releaseCapacity</code> method is called after updating the
         * capacity because in case of a capacity increase, the resource could allow one or more new claims on the capacity from
         * the queue.
         * @param capacity the new maximal capacity
         */
        public void setCapacity(final double capacity)
        {
            this.capacity = capacity;
            this.releaseCapacity(null, 0.0);
        }

        /**
         * Request an amount of capacity from the resource, without a priority. A dummy priority value of 0 is used.
         * @param entity the entity claiming the amount from the resource
         * @param amount the requested amount
         * @param requestor the RequestorInterface requesting the amount
         */
        public synchronized void requestCapacity(final Entity<T> entity, final double amount,
                final CapacityRequestor.DoubleCapacity<T> requestor)
        {
            this.requestCapacity(entity, amount, requestor, 0);
        }

        /**
         * Request an amount of capacity from the resource, with an integer priority value.
         * @param entity the entity claiming the amount from the resource
         * @param amount the requested amount
         * @param requestor the RequestorInterface requesting the amount
         * @param priority the priority of the request
         */
        public synchronized void requestCapacity(final Entity<T> entity, final double amount,
                final CapacityRequestor.DoubleCapacity<T> requestor, final int priority)
        {
            Throw.when(amount < 0.0, SimRuntimeException.class, "requested capacity on resource cannot be < 0.0");
            Throw.whenNull(entity, "entity cannot be null");
            if ((this.claimedCapacity + amount) <= this.capacity)
            {
                changeClaimedCapacity(amount);
                this.claimMap.put(entity, this.claimMap.getOrDefault(entity, 0.0) + amount);
                getSimulator().scheduleEventNow(requestor, "receiveRequestedCapacity",
                        new Object[] {entity, Double.valueOf(amount), this});
            }
            else
            {
                synchronized (getRequestQueue())
                {
                    getRequestQueue().add(entity, amount, requestor, priority);
                }
            }
        }

        /**
         * Release an amount of capacity from the resource.
         * @param entity the entity releasing the amount from the resource
         * @param amount the amount to release
         */
        public void releaseCapacity(final Entity<T> entity, final double amount)
        {
            Throw.when(amount < 0.0, SimRuntimeException.class, "released capacity on resource cannot be < 0.0");
            Throw.when(entity == null && amount != 0, IllegalArgumentException.class, "entity cannot be null when amount > 0");
            if (entity != null && amount != 0.0)
            {
                Throw.when(amount - this.claimedCapacity > 8 * Math.ulp(amount), IllegalStateException.class,
                        "Trying to release more capacity than claimed for this resource");
                Throw.when(amount - this.claimMap.getOrDefault(entity, 0.0) > 8 * Math.ulp(amount), IllegalStateException.class,
                        "Trying to release more capacity than originally claimed by this entity");
                changeClaimedCapacity(-Math.min(this.claimedCapacity, amount));
                double newClaim = this.claimMap.get(entity) - amount;
                if (Math.abs(newClaim) < 8 * Math.ulp(amount))
                    this.claimMap.remove(entity);
                else
                    this.claimMap.put(entity, newClaim);
            }
            synchronized (getRequestQueue())
            {
                for (var cr = getRequestQueue().iterator(); cr.hasNext();)
                {
                    var request = (CapacityRequest.DoubleCapacity<T>) cr.next();
                    if ((this.capacity - this.claimedCapacity) >= request.getAmount())
                    {
                        changeClaimedCapacity(request.getAmount());
                        this.claimMap.put(request.getEntity(),
                                this.claimMap.getOrDefault(request.getEntity(), 0.0) + request.getAmount());
                        request.getRequestor().receiveRequestedCapacity(request.getEntity(), request.getAmount(), this);
                        synchronized (getRequestQueue())
                        {
                            cr.remove();
                            getRequestQueue().remove(request); // for the statistics
                        }
                    }
                    else if (getReleaseType().equals(ReleaseType.FIRST_ONLY))
                    {
                        return;
                    }
                }
            }
        }
    }

    /**
     * Resource with integer capacity.
     * @param <T> the time type
     */
    public static class IntegerCapacity<T extends Number & Comparable<T>> extends Resource<T, IntegerCapacity<T>>
    {
        /** capacity defines the maximum capacity of the resource. */
        private int capacity;

        /** claimedCapacity defines the currently claimed capacity. */
        private int claimedCapacity = 0;

        /** the claims per entity; will be removed if claim fully released. */
        private Map<Entity<T>, Integer> claimMap = new HashMap<>();

        /**
         * Create a new Resource with floating point capacity and a specific request comparator, e.g., LIFO or sorted on an
         * attribute.
         * @param id the id of this resource
         * @param simulator the simulator
         * @param capacity the capacity of the resource
         */
        public IntegerCapacity(final String id, final DevsSimulatorInterface<T> simulator, final int capacity)
        {
            super(id, simulator);
            this.capacity = capacity;
        }

        @Override
        public Integer getCapacity()
        {
            return this.capacity;
        }

        @Override
        public Integer getClaimedCapacity()
        {
            return this.claimedCapacity;
        }

        /**
         * Return the currently available capacity on this resource. This method is implemented as
         * <code>return this.getCapacity()-this.getClaimedCapacity()</code>
         * @return the currently available capacity on this resource.
         */
        public int getAvailableCapacity()
        {
            return this.capacity - this.claimedCapacity;
        }

        /**
         * Add the amount to the claimed capacity.
         * @param amount the amount which is added to the claimed capacity
         */
        private synchronized void changeClaimedCapacity(final int amount)
        {
            this.claimedCapacity += amount;
            fireTimedEvent(UTILIZATION_EVENT,
                    getCapacity().intValue() == 0 ? 0.0 : getClaimedCapacity().doubleValue() / getCapacity().doubleValue(),
                    getSimulator().getSimulatorTime());
        }

        /**
         * Set the capacity of the resource to a new value. The <code>releaseCapacity</code> method is called after updating the
         * capacity because in case of a capacity increase, the resource could allow one or more new claims on the capacity from
         * the queue.
         * @param capacity the new maximal capacity
         */
        public void setCapacity(final int capacity)
        {
            this.capacity = capacity;
            this.releaseCapacity(null, 0);
        }

        /**
         * Request an amount of capacity from the resource, without a priority. A dummy priority value of 0 is used.
         * @param entity the entity claiming the amount from the resource
         * @param amount the requested amount
         * @param requestor the RequestorInterface requesting the amount
         */
        public synchronized void requestCapacity(final Entity<T> entity, final int amount,
                final CapacityRequestor.IntegerCapacity<T> requestor)
        {
            this.requestCapacity(entity, amount, requestor, 0);
        }

        /**
         * Request an amount of capacity from the resource, with an integer priority value.
         * @param entity the entity claiming the amount from the resource
         * @param amount the requested amount
         * @param requestor the RequestorInterface requesting the amount
         * @param priority the priority of the request
         */
        public synchronized void requestCapacity(final Entity<T> entity, final int amount,
                final CapacityRequestor.IntegerCapacity<T> requestor, final int priority)
        {
            Throw.when(amount < 0, SimRuntimeException.class, "requested capacity on resource cannot be < 0");
            Throw.whenNull(entity, "entity cannot be null");
            if ((this.claimedCapacity + amount) <= this.capacity)
            {
                changeClaimedCapacity(amount);
                this.claimMap.put(entity, this.claimMap.getOrDefault(entity, 0) + amount);
                getSimulator().scheduleEventNow(requestor, "receiveRequestedCapacity",
                        new Object[] {entity, Integer.valueOf(amount), this});
            }
            else
            {
                synchronized (getRequestQueue())
                {
                    getRequestQueue().add(entity, amount, requestor, priority);
                }
            }
        }

        /**
         * Release an amount of capacity from the resource.
         * @param entity the entity releasing the amount from the resource
         * @param amount the amount to release
         */
        public void releaseCapacity(final Entity<T> entity, final int amount)
        {
            Throw.when(amount < 0, SimRuntimeException.class, "released capacity on resource cannot be < 0");
            Throw.when(entity == null && amount != 0, IllegalArgumentException.class, "entity cannot be null when amount > 0");
            if (entity != null && amount != 0.0)
            {
                Throw.when(amount > this.claimedCapacity, IllegalStateException.class,
                        "Trying to release more capacity than claimed for this resource");
                Throw.when(amount > this.claimMap.getOrDefault(entity, 0), IllegalStateException.class,
                        "Trying to release more capacity than originally claimed by this entity");
                changeClaimedCapacity(-Math.min(this.claimedCapacity, amount));
                int newClaim = this.claimMap.get(entity) - amount;
                if (Math.abs(newClaim) == 0)
                    this.claimMap.remove(entity);
                else
                    this.claimMap.put(entity, newClaim);
            }
            synchronized (getRequestQueue())
            {
                for (var cr = getRequestQueue().iterator(); cr.hasNext();)
                {
                    var request = (CapacityRequest.IntegerCapacity<T>) cr.next();
                    if ((this.capacity - this.claimedCapacity) >= request.getAmount())
                    {
                        this.changeClaimedCapacity(request.getAmount());
                        this.claimMap.put(request.getEntity(),
                                this.claimMap.getOrDefault(request.getEntity(), 0) + request.getAmount());
                        request.getRequestor().receiveRequestedCapacity(request.getEntity(), request.getAmount(), this);
                        synchronized (getRequestQueue())
                        {
                            cr.remove();
                            getRequestQueue().remove(request); // for the statistics
                        }
                    }
                    else if (getReleaseType().equals(ReleaseType.FIRST_ONLY))
                    {
                        return;
                    }
                }
            }
        }
    }

    /**
     * The release type, indicating in what way available capacity will be provided to waiting requests: is it only the first
     * requests in the queue (depending on the sorting algorithm) that receive capacity, or is the entire queue searched for
     * requests that may benefit from the still available capacity?
     */
    public static enum ReleaseType
    {
        /** FIRST_ONLY means that the search for requests that can use capacity stops at the first non-fit. */
        FIRST_ONLY,

        /** ENTIRE_QUEUE means that the search for requests that can use capacity searches the entire queue. */
        ENTIRE_QUEUE;
    }
}