/*
 * ModeShape (http://www.modeshape.org)
 *
 * Licensed 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.modeshape.jcr;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.jcr.AccessDeniedException;
import javax.jcr.InvalidItemStateException;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.lock.Lock;
import javax.jcr.lock.LockException;
import javax.jcr.lock.LockManager;
import org.modeshape.common.annotation.ThreadSafe;
import org.modeshape.common.util.CheckArg;
import org.modeshape.common.util.StringUtil;
import org.modeshape.jcr.RepositoryLockManager.ModeShapeLock;
import org.modeshape.jcr.cache.CachedNode;
import org.modeshape.jcr.cache.LockFailureException;
import org.modeshape.jcr.cache.NodeCache;
import org.modeshape.jcr.cache.NodeKey;
import org.modeshape.jcr.cache.SessionCache;

/**
 * A {@link LockManager} implementation owned and used by a {@link JcrSession session}. This object is relatively lightweight, and
 * maintains the set of lock tokens owned by this session plus references to the session and the repository-wide lock manager.
 */
@ThreadSafe
class JcrLockManager implements LockManager {

    private final JcrSession session;
    private final RepositoryLockManager lockManager;
    private final Set<String> lockTokens = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());

    JcrLockManager( JcrSession session,
                    RepositoryLockManager lockManager ) {
        this.session = session;
        this.lockManager = lockManager;
    }

    boolean hasLockToken( String token ) {
        return !StringUtil.isBlank(token) && lockTokens.contains(token);
    }

    /**
     * Unlocks all locks corresponding to the tokens held by the supplied session.
     * 
     * @throws RepositoryException if the session is not live
     */
    final void cleanLocks() throws RepositoryException {
        // clean the session-scoped locks: unlock all the nodes and remove the lock tokens from the system area
        Set<String> cleanedTokens = lockManager.cleanLocks(session);
        if (lockTokens.isEmpty()) {
            return;
        }
        // clean all open-scoped locks created via this session
        for (String lockToken : lockTokens) {
            if (!cleanedTokens.contains(lockToken)) {
                ModeShapeLock lock = lockManager.findLockByToken(lockToken);
                if (lock == null) {
                    // there is no existing lock for this token (it must've been removed from somewhere else)
                    continue;
                }
                // this is an open-scoped lock created via this session for which we'll have to update the 'held' flag
                if (!lockManager.setHeldBySession(session, lockToken, false)) {
                    // Generally not expected, because if the lock exists we can always change the lock-held value to false
                    // even when it is already false ...
                    throw new LockException(JcrI18n.invalidLockToken.text(lockToken));
                }
            }
        }
        // always clean our internal map of tokens
        lockTokens.clear();
    }

    @Override
    public void addLockToken( String lockToken ) throws LockException {
        CheckArg.isNotNull(lockToken, "lockToken");

        if (lockTokens.contains(lockToken)) {
            // We already hold the token ...
            return;
        }

        // Change the lock to be held by a session ...
        try {
            if (!lockManager.setHeldBySession(session, lockToken, true)) {
                throw new LockException(JcrI18n.lockTokenAlreadyHeld.text(lockToken));
            }
            lockTokens.add(lockToken);
        } catch (LockException e) {
            lockTokens.remove(lockToken);
            throw e;
        }
    }

    @Override
    public void removeLockToken( String lockToken ) throws LockException {
        CheckArg.isNotNull(lockToken, "lockToken");

        // Trivial case of giving a token back to ourself
        if (!lockTokens.contains(lockToken)) {
            // We don't already hold the token ...
            throw new LockException(JcrI18n.invalidLockToken.text(lockToken));
        }

        // Change the lock to be no longer held by a session ...
        try {
            if (!lockManager.setHeldBySession(session, lockToken, false)) {
                // Generally not expected, because if the lock exists we can always change the lock-held value to false
                // even when it is already false ...
                throw new LockException(JcrI18n.invalidLockToken.text(lockToken));
            }

            // Now remove the tokens from our session ...
            lockTokens.remove(lockToken);
        } catch (LockFailureException e) {
            lockTokens.remove(lockToken);
            throw new LockException(JcrI18n.invalidLockToken.text(lockToken));
        }
    }

    Set<String> lockTokens() {
        return Collections.unmodifiableSet(lockTokens);
    }

    @Override
    public String[] getLockTokens() {
        Set<String> tokens =  new HashSet<String>();
        for (String token : lockTokens) {
            ModeShapeLock lock = lockManager.findLockByToken(token);
            if (lock != null && !lock.isSessionScoped()) {
                tokens.add(token);
            }
        }
        return tokens.toArray(new String[tokens.size()]);
    }

    @Override
    public boolean isLocked( String absPath ) throws PathNotFoundException, RepositoryException {
        return getLowestLockAlongPath(session.node(session.absolutePathFor(absPath)), true) != null;
    }

    public boolean isLocked( AbstractJcrNode node ) throws PathNotFoundException, RepositoryException {
        return getLowestLockAlongPath(node, true) != null;
    }

    @Override
    public Lock getLock( String absPath ) throws PathNotFoundException, LockException, AccessDeniedException, RepositoryException {
        ModeShapeLock lock = getLowestLockAlongPath(session.node(session.absolutePathFor(absPath)), false);
        if (lock != null) return lock.lockFor(session);
        throw new LockException(JcrI18n.notLocked.text(absPath));
    }

    public Lock getLock( AbstractJcrNode node ) throws LockException, AccessDeniedException, RepositoryException {
        ModeShapeLock lock = getLowestLockAlongPath(node, false);
        if (lock != null) return lock.lockFor(session);
        throw new LockException(JcrI18n.notLocked.text(node.getPath()));
    }

    public Lock getLockIfExists( AbstractJcrNode node ) throws AccessDeniedException, RepositoryException {
        ModeShapeLock lock = getLowestLockAlongPath(node, false);
        return lock == null ? null : lock.lockFor(session);
    }

    private ModeShapeLock getLowestLockAlongPath(final AbstractJcrNode node, boolean skipExpiredLocks)
        throws PathNotFoundException, AccessDeniedException, RepositoryException {
        session.checkLive();
       
        SessionCache sessionCache = session.cache();
        NodeCache cache = sessionCache;
        NodeKey nodeKey = node.key();
        NodeKey key = nodeKey;
        while (key != null) {
            ModeShapeLock lock = lockManager.findLockFor(key);
            if (lock != null && (lock.isDeep() || nodeKey.equals(lock.getLockedNodeKey()))) {
                if (!lock.isExpired() || !skipExpiredLocks) {
                    // There is a lock that applies to 'node', either because the lock is actually on 'node' or because
                    // an ancestor node is locked with a deep lock...
                    return lock;
                }
            }
            // Otherwise, get the parent, but use the cache directly ...
            CachedNode cachedNode = cache.getNode(key);
            if (cachedNode == null) {
                // The node has been removed, so get the node from the workspace cache ...
                if (sessionCache == cache) {
                    cache = sessionCache.getWorkspace();
                    cachedNode = cache.getNode(key);
                }
                if (cachedNode == null) break;
            }
            key = cachedNode.getParentKey(cache);
        }
        return null;
    }

    @Override
    public boolean holdsLock( String absPath ) throws PathNotFoundException, RepositoryException {
        AbstractJcrNode node = session.node(session.absolutePathFor(absPath));
        return lockManager.findLockFor(node.key()) != null;
    }

    /**
     * Determine if this lock manager holds a lock on the supplied node.
     * 
     * @param node the node; may not be null
     * @return true if this lock maanger does hold a lock on the node, or false otherwise
     */
    public boolean holdsLock( AbstractJcrNode node ) {
        return lockManager.findLockFor(node.key()) != null;
    }

    @Override
    public Lock lock( String absPath,
                      boolean isDeep,
                      boolean isSessionScoped,
                      long timeoutHint,
                      String ownerInfo )
        throws LockException, PathNotFoundException, AccessDeniedException, InvalidItemStateException, RepositoryException {
        AbstractJcrNode node = session.node(session.absolutePathFor(absPath));
        return lock(node, isDeep, isSessionScoped, timeoutHint, ownerInfo);
    }

    /**
     * Attempt to obtain a lock on the supplied node.
     * 
     * @param node the node; may not be null
     * @param isDeep true if the lock should be a deep lock
     * @param isSessionScoped true if the lock should be scoped to the session
     * @param timeoutHint desired lock timeout in seconds (servers are free to ignore this value); specify {@link Long#MAX_VALUE}
     *        for no timeout.
     * @param ownerInfo a string containing owner information supplied by the client, and recorded on the lock; if null, then the
     *        session's user ID will be used
     * @return the lock; never null
     * @throws AccessDeniedException if the caller does not have privilege to lock the node
     * @throws InvalidItemStateException if the node has been modified and cannot be locked
     * @throws LockException if the lock could not be obtained
     * @throws RepositoryException if an error occurs updating the graph state
     */
    public Lock lock( AbstractJcrNode node,
                      boolean isDeep,
                      boolean isSessionScoped,
                      long timeoutHint,
                      String ownerInfo )
        throws LockException, AccessDeniedException, InvalidItemStateException, RepositoryException {
        if (!node.isLockable()) {
            throw new LockException(JcrI18n.nodeNotLockable.text(node.location()));
        }
        if (node.isLocked()) {
            throw new LockException(JcrI18n.alreadyLocked.text(node.location()));            
        }
        if (node.isNew()|| node.isModified()) {
            throw new InvalidItemStateException(JcrI18n.changedNodeCannotBeLocked.text(node.location()));
        }

        // Try to obtain the lock ...
        ModeShapeLock lock = lockManager.lock(session, node.node(), isDeep, isSessionScoped, timeoutHint, ownerInfo);
        String token = lock.getLockToken();
        lockTokens.add(token);
        return lock.lockFor(session);
    }

    @Override
    public void unlock( String absPath )
        throws PathNotFoundException, LockException, AccessDeniedException, InvalidItemStateException, RepositoryException {
        AbstractJcrNode node = session.node(session.absolutePathFor(absPath));
        unlock(node);
    }

    public void unlock( AbstractJcrNode node )
        throws PathNotFoundException, LockException, AccessDeniedException, InvalidItemStateException, RepositoryException {

        if (node.isModified()) {
            throw new InvalidItemStateException(JcrI18n.changedNodeCannotBeUnlocked.text(node.getPath()));
        }

        ModeShapeLock lock = lockManager.findLockFor(node.key());
        if (lock != null && !lockTokens.contains(lock.getLockToken())) {
            // Someone else holds the lock, so see if the user has the permission to break someone else's lock ...
            try {
                session.checkPermission(session.workspaceName(), node.path(), ModeShapePermissions.UNLOCK_ANY);
            } catch (AccessDeniedException e) {
                //expected by the TCK
                throw new LockException(e);
            }
        }

        // Remove the lock ...
        String lockToken = lockManager.unlock(session, node.key());
        lockTokens.remove(lockToken);
    }

}
