/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.io.uri.impl;

import net.ranides.assira.collection.maps.OpenMap;

import java.io.IOException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * CHandleManager stores all opened resources of type "C" associated with path "P".
 *
 * You can use shared resources, which are opened only once when first client used it
 * and closed only once when last client closed it.
 *
 * You can use forked resources, which should be used only by one client.
 *
 * One path can be associated with at most one shared resources
 * and any number of forked resources.
 *
 * Manager is able to promote forked resources to shared state and vice versa, to avoid
 * opening unecessary connections.
 *
 * Transition from forked to shared must be implemented in "reset" method.
 *
 * Reference to forked resource must be stored internally by client.
 *
 * Reference to shared resource must NOT be stored internally by client.
 *
 * @param <P> P
 * @param <C> C
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public abstract class CHandleManager<P,C> {
    
    private final Map<P, CResource<C>> resources = new OpenMap<>();

    /**
     * This method opens resource.
     * It must not be called directly.
     *
     * Please note, that manager tries to call this method as late as possible.
     *
     * @param path path
     * @return opened resource
     * @throws IOException on error
     */
    protected abstract C connect(P path) throws IOException;

    /**
     * This method closes resource.
     * It must not be called directly.
     *
     * Both shared and forked resources are closed using this method, in the end.
     *
     * @param channel channel
     * @throws IOException on error
     */
    protected abstract void disconnect(C channel) throws IOException;

    /**
     * This method does not close resource, but resets its state.
     * It must not be called directly.
     *
     * This method is called, if one of "forked" resources is moved back to "shared" pool.
     *
     * Shared resources should not be used in statefull way, but forked resources could be.
     * You should clear all state, for example, push/flush all pending changes.
     *
     * In best case, the same channel is retured (possibly with cleared state)
     * In worst case, method is allowed to completely close resource and return new one.
     *
     * @param channel channel
     * @return channel
     * @throws IOException on error
     */
    protected abstract C reset(C channel) throws IOException;

    /**
     * Tells manager, that you will use this shared resource in the future.
     *
     * It does not open resource. Opening resource is responsibility of {@link #shared(Object)} method,
     * but you must call "open" before first use of resource.
     *
     * Manager tracks number of clients who reserved resource.
     * This method increments usage counter. You must call "close"
     * when you don't plan to use resource anymore.
     *
     * @param path path
     */
    public void open(P path) {
        synchronized(resources) {
            CResource<C> res = resources.get(path);
            if(res == null) {
                resources.put(path, res = new CResource<>());
            }
            res.counter++;
        }
    }

    /**
     * Tells manager, that you won't use this shared resource in the future.
     * If you are the last user of shared resource, manager will close it.
     *
     * Method does not affect forked resources, which are still opened.
     * Forked resources must be closed using {@link #close(Object, Object)}.
     *
     * Manager tracks number of clients who reserved resource.
     * This method decrements usage counter. You can call "close"
     * only when you reserved resource in the past using "open".
     *
     * @param path path
     * @throws IOException on error
     */
    public void close(P path) throws IOException {
        synchronized(resources) {
            CResource<C> res = resources.get(path);
            if(res == null) {
                return;
            }
            if(0 == --res.counter && null != res.shared) {
                disconnect(res.shared);
                res.shared = null;
            }
        }
    }

    /**
     * Returns shared resource for specified path.
     *
     * If shared resource is not opened yet, it is opened.
     *
     * You should not store returned reference, it must be obtained on every use.
     * You should not obtain shared reference, if you never reserved it by using {@link #open(Object)}.
     *
     * @param path path
     * @return active, opened resource
     * @throws IOException on error
     */
    public C shared(P path) throws IOException {
        synchronized(this) {
            CResource<C> res = resources.get(path);
            if(res == null) {
                resources.put(path, res = new CResource<>());
            }
            if(null == res.shared) {
                res.shared = connect(path);
            }
            return res.shared;
        }
    }

    /**
     * Changes state of resource from shared to fork state.
     *
     * You can create forked resources even if shared resource was never opened.
     * In such case new forked connection is created and shared pool is not affected at all.
     *
     * If there is already opened shared resource, it moves opened connection
     * to "forked" pool and clears shared pool. Next access to shared resource
     * will open new shared connection.
     *
     * Forked resource can be used only by one client. Client must store
     * reference to forked resource internally, and close it using {@link #close(Object, Object)}.
     *
     * @param path path
     * @return active, opened resource
     * @throws IOException on error
     */
    public C fork(P path) throws IOException {
        synchronized(resources) {
            CResource<C> res = resources.get(path);
            if(res == null) {
                resources.put(path, res = new CResource<>());
            }

            C channel;
            if(null != res.shared) {
                res.forked.add(channel = res.shared);
                res.shared = null;
            } else {
                res.forked.add(channel = connect(path));
            }
            return channel;
        }
    }

    /**
     * Closes forked "channel".
     *
     * If channel is not in forked state, it does nothing
     *
     * Please note, that "fork" operation changes channel from shared state to fork state lazily.
     * Becuse of that, it is possible, that forked resource is removed from "shared" pool, but
     * shared resource is not recreated yet. In such case we don't close resource, but reset its
     * state and move back to shared pool.
     *
     * If shared resource is recreated, we simply disconnect channel.
     *
     * @param path path
     * @param channel channel
     * @throws IOException on error
     */
    public void close(P path, C channel) throws IOException {
        synchronized(resources) {
            CResource<C> res = resources.get(path);
            if(res == null) {
                return;
            }
            if(!res.forked.remove(channel)) {
                return;
            }
            if(null == res.shared && 0 != res.counter) {
                res.shared = reset(channel);
            } else {
                disconnect(channel);
            }
        }
    }
    
    private static final class CResource<V> {
        
        public V shared;
        
        public Set<V> forked = new HashSet<>();
        
        private int counter;

    }
    
}
