/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 * 
 *    (C) 2003-2008, Open Source Geospatial Foundation (OSGeo)
 *    
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.data;

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

import org.geotools.data.Transaction.State;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;


/**
 * Provides In-Process FeatureLocking support for DataStore implementations.
 * 
 * <p>
 * If at all possible DataStore implementations should provide a real Feature
 * Locking support that is persisted to disk or database and resepected by
 * other processes.
 * </p>
 * 
 * <p>
 * This class provides a stop gap solution that implementations may use for
 * GeoServer compatability.
 * </p>
 *
 * @author Jody Garnett, Refractions Research
 * @author Chris Holmes, TOPP
 *
 * @task REVISIT: I'm not sure that the map within a map is a good idea, it
 *       makes things perhaps too complicated.  A nasty bug came about with
 *       releasing, as allLocks put locks into a new collection, and the
 *       iterator just removed them from that set instead of from the storage.
 *       This is now fixed, but the loop to do it is really damn complex.
 *       I'm not sure of the solution, but there should be something that is
 *       less confusing.
 *
 * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/modules/library/main/src/main/java/org/geotools/data/InProcessLockingManager.java $
 */
public class InProcessLockingManager implements LockingManager {

    /** lockTable access by typeName stores Transactions or MemoryLocks */
    protected Map lockTables = new HashMap();

    /**
     * Aquire lock on featureID.
     * 
     * <p>
     * This method will fail if Lock is already held by another.
     * </p>
     *
     * @param typeName TypeName storing feature
     * @param featureID FeatureID to lock
     * @param transaction Transaction to lock against
     * @param featureLock FeatureLock describing lock request
     *
     * @throws FeatureLockException Indicates a problem with the lock request
     */
    public synchronized void lockFeatureID(String typeName, String featureID,
        Transaction transaction, FeatureLock featureLock)
        throws FeatureLockException {
        Lock lock = getLock(typeName, featureID);

        // This is a loop so we can wait on Transaction Locks
        //
        while (lock != null) {
            // we have a conflict
            if (lock instanceof TransactionLock) {
                TransactionLock tlock = (TransactionLock) lock;

                if (transaction == tlock.transaction) {
                    // lock already held by this transacstion
                    // we could just consider returning here
                    //
                    throw new FeatureLockException("Transaction Lock is already held by this Transaction",
                        featureID);
                } else {
                    // we should wait till it is available and then grab
                    // the lock
                    try {
                        synchronized (tlock) {
                            tlock.wait();
                        }

                        lock = getLock(typeName, featureID);
                    } catch (InterruptedException interupted) {
                        throw new FeatureLockException("Interupted while waiting for Transaction Lock",
                            featureID, interupted);
                    }
                }
            } else if (lock instanceof MemoryLock) {
                MemoryLock mlock = (MemoryLock) lock;
                throw new FeatureLockException(
                    "Feature Lock is held by Authorization " + mlock.authID,
                    featureID);
            } else {
                throw new FeatureLockException("Lock is already held " + lock,
                    featureID);
            }
        }

        // Lock is Available
        //
        lock = createLock(transaction, featureLock);
        locks(typeName).put(featureID, lock);
    }

    /**
     * Lock for typeName & featureID if it exists.
     * 
     * <p>
     * This method will not return expired locks.
     * </p>
     *
     * @param typeName
     * @param featureID
     *
     * @return Lock if exists, or null
     */
    protected Lock getLock(String typeName, String featureID) {
        Map locks = locks(typeName);
        //LOGGER.info("checking for lock " + typeName + ", " + featureID
        //    + " in locks " + locks);

        synchronized (locks) {
            if (locks.containsKey(featureID)) {
                Lock lock = (Lock) locks.get(featureID);

                if (lock.isExpired()) {
                    locks.remove(featureID);
                    //LOGGER.info("returning null");

                    return null;
                } else {
                    //LOGGER.info("returing " + lock);

                    return lock;
                }
            } else {
                //LOGGER.info("locks did not contain key, returning null");

                // not found
                return null;
            }
        }
    }

    /**
     * Creates the right sort of In-Process Lock.
     *
     * @param transaction
     * @param featureLock
     *
     * @return In-Process Lock
     *
     * @throws FeatureLockException When a Transaction  lock is requested
     *         against Transaction.AUTO_COMMIT
     */
    protected synchronized Lock createLock(Transaction transaction,
        FeatureLock featureLock) throws FeatureLockException {
        if (featureLock == FeatureLock.TRANSACTION) {
            // we need a Transacstion Lock
            if (transaction == Transaction.AUTO_COMMIT) {
                throw new FeatureLockException(
                    "We cannot issue a Transaction lock against AUTO_COMMIT");
            }

            TransactionLock lock = (TransactionLock) transaction.getState(this);

            if (lock == null) {
                lock = new TransactionLock();
                transaction.putState(this, lock);

                return lock;
            } else {
                return lock;
            }
        } else {
            return new MemoryLock(featureLock);
        }
    }

    /**
     * Access to a Map of locks for typeName
     *
     * @param typeName typeName
     *
     * @return Map of Transaction or MemoryLock by featureID
     */
    protected Map locks(String typeName) {
        synchronized (lockTables) {
            if (lockTables.containsKey(typeName)) {
                return (Map) lockTables.get(typeName);
            } else {
                Map locks = new HashMap();
                lockTables.put(typeName, locks);

                return locks;
            }
        }
    }

    /**
     * Set of all locks.
     *
     * @return Set of all locks
     */
    protected Set allLocks() {
        synchronized (lockTables) {
            Set set = new HashSet();
            Map fidLocks;

            for (Iterator i = lockTables.values().iterator(); i.hasNext();) {
                fidLocks = (Map) i.next();
                set.addAll(fidLocks.values());
            }

            return set;
        }
    }

    /**
     * Checks mutability of featureID for this transaction.
     * 
     * <p>
     * Two behaviors are defined by FeatureLocking:
     * </p>
     * 
     * <ul>
     * <li>
     * TransactionLock (Blocking): lock held by a Transaction<br>
     * Authorization is granted to the Transaction holding the Lock. Conflict
     * will result in a block until the Transaction holding the lock
     * completes. (This behavior is equivalent to a Database row-lock, or a
     * java synchronized statement)
     * </li>
     * <li>
     * FeatureLock (Error): lock held by a FeatureLock<br>
     * Authorization is based on the set of Authorization IDs held by the
     * provided Transaction. Conflict will result in an error.  (This behavior
     * is equivalent to the WFS locking specification)
     * </li>
     * </ul>
     * 
     * <p>
     * Right now we are just going to error out with an exception
     * </p>
     *
     * @param typeName Feature type to check against
     * @param featureID FeatureID to check
     * @param transaction Provides Authorization
     *
     * @throws FeatureLockException If transaction does not have sufficient
     *         authroization
     */
    public void assertAccess(String typeName, String featureID,
        Transaction transaction) throws FeatureLockException {
        Lock lock = getLock(typeName, featureID);

        //LOGGER.info("asserting access on lock for " + typeName + ", fid: "
	//  + featureID + ", transaction: " + transaction + ", lock " + lock);

        if ((lock != null) && !lock.isAuthorized(transaction)) {
            throw new FeatureLockException(
                "Transaction does not have authorization for " + typeName + ":"
                + featureID);
        }
    }

    /**
     * Provides a wrapper on the provided writer that checks locks.
     *
     * @param writer FeatureWriter requiring access control
     * @param transaction Transaction being used
     *
     * @return FeatureWriter with lock checking
     */
    public FeatureWriter<SimpleFeatureType, SimpleFeature> checkedWriter(final FeatureWriter<SimpleFeatureType, SimpleFeature> writer,
        final Transaction transaction) {
        SimpleFeatureType featureType = writer.getFeatureType();
        final String typeName = featureType.getTypeName();

        return new DelegatingFeatureWriter<SimpleFeatureType, SimpleFeature>() {
                SimpleFeature live = null;
                
                public SimpleFeatureType getFeatureType() {
                    return writer.getFeatureType();
                }

                public SimpleFeature next() throws IOException {
                    live = writer.next();

                    return live;
                }

                public void remove() throws IOException {
                    if (live != null) {
                        assertAccess(typeName, live.getID(), transaction);
                    }

                    writer.remove();
                    live = null;
                }

                public void write() throws IOException {
                    if (live != null) {
                        assertAccess(typeName, live.getID(), transaction);
                    }

                    writer.write();
                    live = null;
                }

                public boolean hasNext() throws IOException {
                    live = null;

                    return writer.hasNext();
                }

                public void close() throws IOException {
                    live = null;
                    writer.close();
                }
            };
    }

    /**
     * Release indicated featureID, must have correct authroization.
     *
     * @param typeName
     * @param featureID
     * @param transaction
     * @param featureLock
     *
     * @throws IOException If lock could not be released
     */
    public synchronized void unLockFeatureID(String typeName, String featureID,
        Transaction transaction, FeatureLock featureLock)
        throws IOException {
        assertAccess(typeName, featureID, transaction);
        locks(typeName).remove(featureID);
    }

    /**
     * Refresh locks held by the authorization <code>authID</code>.
     * 
     * <p>
     * (remember that the lock may have expired)
     * </p>
     *
     * @param authID Authorization identifing Lock to refresh
     * @param transaction Transaction with authorization for lockID
     *
     * @return <code>true</code> if lock was found and refreshed
     *
     * @throws IOException If transaction not authorized to refresh authID
     * @throws IllegalArgumentException If authID or transaction not provided
     */
    public synchronized boolean refresh(String authID, Transaction transaction)
        throws IOException {
        if (authID == null) {
            throw new IllegalArgumentException("lockID required");
        }

        if ((transaction == null) || (transaction == Transaction.AUTO_COMMIT)) {
            throw new IllegalArgumentException(
                "Tansaction required (with authorization for " + authID + ")");
        }

        Lock lock;
        boolean refresh = false;

        for (Iterator i = allLocks().iterator(); i.hasNext();) {
            lock = (Lock) i.next();

            if (lock.isExpired()) {
                i.remove();
            } else if (lock.isMatch(authID)) {
                if (lock.isAuthorized(transaction)) {
                    lock.refresh();
                    refresh = true;
                } else {
                    throw new IOException("Not authorized to refresh " + lock);
                }
            }
        }

        return refresh;
    }

    /**
     * Release locks held by the authorization <code>authID</code>.
     * 
     * <p>
     * (remember that the lock may have expired)
     * </p>
     *
     * @param authID Authorization identifing Lock to release
     * @param transaction Transaction with authorization for lockID
     *
     * @return <code>true</code> if lock was found and released
     *
     * @throws IOException If transaction not authorized to release authID
     * @throws IllegalArgumentException If authID or transaction not provided
     */
    public boolean release(String authID, Transaction transaction)
        throws IOException {
        //LOGGER.info("release called on lock: " + authID + ", trans: "
	//  + transaction);

        if (authID == null) {
            throw new IllegalArgumentException("lockID required");
        }

        if ((transaction == null) || (transaction == Transaction.AUTO_COMMIT)) {
            throw new IllegalArgumentException(
                "Tansaction required (with authorization for " + authID + ")");
        }

        Lock lock;
        boolean release = false;

        //This could be done more efficiently, and perhaps cleaner,
        //but these maps within a map are just nasty.  The previous way of
        //calling iterator.remove() didn't actually remove anything, as it
        //was only iterating through the values of a map, which I believe
        //java just copies, so it's immutable.  Or perhaps we just moved
        //through too many iterator layers...
        for (Iterator i = lockTables.values().iterator(); i.hasNext();) {
            Map fidMap = (Map) i.next();
            Set unLockedFids = new HashSet();

            for (Iterator j = fidMap.keySet().iterator(); j.hasNext();) {
                String fid = (String) j.next();
                lock = (Lock) fidMap.get(fid);
                //LOGGER.info("checking lock " + lock + ", is match "
                //    + lock.isMatch(authID));

                if (lock.isExpired()) {
                    unLockedFids.add(fid);

                    //fidMap.remove(fid); concurrent modification error.
                } else if (lock.isMatch(authID)) {
                    //LOGGER.info("matches, is authorized: "
                    //    + lock.isAuthorized(transaction));

                    if (lock.isAuthorized(transaction)) {
                        unLockedFids.add(fid);

                        //fidMap.remove(fid);
                        release = true;
                    } else {
                        throw new IOException("Not authorized to release "
                            + lock);
                    }
                }
            }

            for (Iterator k = unLockedFids.iterator(); k.hasNext();) {
                fidMap.remove(k.next());
            }
        }

        return release;
    }

    /**
     * Implment lockExists.
     * 
     * <p>
     * Remeber lock may have expired.
     * </p>
     *
     * @param authID
     *
     * @return true if lock exists for authID
     *
     * @see org.geotools.data.LockingManager#lockExists(java.lang.String)
     */
    public boolean exists(String authID) {
        //LOGGER.info("checking existence of lock: " + authID + " in "
        //    + allLocks());

        if (authID == null) {
            return false;
        }

        Lock lock;

        for (Iterator i = allLocks().iterator(); i.hasNext();) {
            lock = (Lock) i.next();

            if (lock.isExpired()) {
                i.remove();
            } else if (lock.isMatch(authID)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Represents In-Process locks for Transactions or FeatureLocks.
     *
     * @author Jody Garnett, Refractions Research
     */
    interface Lock {
        /**
         * Check if lock has expired, it will be removed if so
         *
         * @return <code>true</code> if Lock has gone stale
         */
        boolean isExpired();

        /**
         * Check if authID matches this lock
         *
         * @return <code>true</code> if authID matches
         */
        boolean isMatch(String authID);

        /**
         * Check if transaction is authorized for this lock
         *
         * @return <code>true</code> if transaction is authorized
         */
        boolean isAuthorized(Transaction transaction);

        /**
         * Refresh lock
         */
        void refresh();
    }

    /**
     * Class representing TransactionDuration locks.
     * 
     * <p>
     * Implements Transasction.State so it can remomve itself when commit() or
     * rollback() is called.
     * </p>
     * 
     * <p>
     * Threads may wait on this object, it will notify when it releases the
     * lock due to a commit or rollback opperation
     * </p>
     *
     * @author Jody Garnett, Refractions Research
     */
    class TransactionLock implements Lock, State {
        /** This will be non-null while lock is fresh */
        Transaction transaction;

        /**
         * A new TranasctionLock for use.
         * 
         * <p>
         * The lock will be stale until added to Tranasction.putState( key,
         * Lock )
         * </p>
         */
        TransactionLock() {
        }

        /**
         * Transaction locks do not match authIDs
         *
         * @param authID Authorization ID being checked
         *
         * @return <code>false</code>
         */
        public boolean isMatch(String authID) {
            return false;
        }

        /**
         * <code>true</code> if Lock has gone stale
         *
         * @return <code>true</code> if lock is stale
         */
        public boolean isExpired() {
            return transaction != null;
        }

        /**
         * TransactionLocks do not need to be refreshed
         */
        public void refresh() {
            // do not need to implement   
        }

        /**
         * <code>true </code> if tranasction is the same one that provided this
         * lock
         *
         * @param transaction Transaction to check authorization against
         *
         * @return true if transaction is authorized
         */
        public boolean isAuthorized(Transaction transaction) {
            return this.transaction == transaction;
        }

        public String toString() {
            return "TranasctionLock(" + !isExpired() + ")";
        }
    }

    /**
     * Class referenced by featureID in locks( typeName).
     * 
     * <p>
     * FeatureLock is the request - MemoryLock is the result.
     * </p>
     *
     * @author Jody Garnett, Refractions Reasearch Inc.
     */
    class MemoryLock implements Lock {
        String authID;
        long duration;
        long expiry;

        MemoryLock(FeatureLock lock) {
            this(lock.getAuthorization(), lock.getDuration());
        }

        MemoryLock(String id, long length) {
            authID = id;
            this.duration = length;
            expiry = System.currentTimeMillis() + length;
        }

        public boolean isMatch(String id) {
            return authID.equals(id);
        }

        public void refresh() {
            expiry = System.currentTimeMillis() + duration;
        }

        public boolean isExpired() {
            if (duration == 0) {
                return false; // perma lock
            }

            long now = System.currentTimeMillis();

            return now >= expiry;
        }

        public boolean isAuthorized(Transaction transaction) {
            //LOGGER.info("checking authorization on " + this.toString() + ", "
	    //  + ((transaction != Transaction.AUTO_COMMIT)
	    //  ? transaction.getAuthorizations().toString() : "autocommit"));

            return (transaction != Transaction.AUTO_COMMIT)
            && transaction.getAuthorizations().contains(authID);
        }

        public String toString() {
            if (duration == 0) {
                return "MemoryLock(" + authID + "|PermaLock)";
            }

            long now = System.currentTimeMillis();
            long delta = (expiry - now);
            long dur = duration;

            return "MemoryLock(" + authID + "|" + delta + "ms|" + dur + "ms)";
        }
    }
}
