/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.jackrabbit.core.version;

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

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.version.VersionException;

import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry;
import org.apache.jackrabbit.core.observation.EventStateCollection;
import org.apache.jackrabbit.core.observation.EventStateCollectionFactory;
import org.apache.jackrabbit.core.state.ChangeLog;
import org.apache.jackrabbit.core.state.ItemState;
import org.apache.jackrabbit.core.state.ItemStateCacheFactory;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.ItemStateListener;
import org.apache.jackrabbit.core.state.NoSuchItemStateException;
import org.apache.jackrabbit.core.state.NodeReferences;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.core.state.XAItemStateManager;
import org.apache.jackrabbit.core.virtual.VirtualItemStateProvider;
import org.apache.jackrabbit.core.virtual.VirtualNodeState;
import org.apache.jackrabbit.core.virtual.VirtualPropertyState;
import org.apache.jackrabbit.data.core.InternalXAResource;
import org.apache.jackrabbit.data.core.TransactionContext;
import org.apache.jackrabbit.data.core.TransactionException;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.name.NameConstants;

/**
 * Implementation of a {@link InternalVersionManager} that works in an XA environment.
 * Works as a filter between a version manager client and the global version
 * manager.
 */
public class InternalXAVersionManager extends InternalVersionManagerBase
        implements EventStateCollectionFactory, VirtualItemStateProvider, InternalXAResource {

    /**
     * Attribute name for associated change log.
     */
    private static final String CHANGE_LOG_ATTRIBUTE_NAME = "XAVersionManager.ChangeLog";

    /**
     * Attribute name for items.
     */
    private static final String ITEMS_ATTRIBUTE_NAME = "VersionItems";

    /**
     * Repository version manager.
     */
    private final InternalVersionManagerImpl vMgr;

    /**
     * The session that uses this version manager.
     */
    private SessionImpl session;

    /**
     * Items that have been modified and are part of the XA environment.
     */
    private Map<NodeId, InternalVersionItem> xaItems;

    /**
     * flag that indicates if the version manager was locked during prepare
     */
    private boolean vmgrLocked = false;

    /**
     * The global write lock on the version manager.
     */
    private VersioningLock.WriteLock vmgrLock;

    /**
     * Persistent root node of the version histories.
     */
    private final NodeStateEx historyRoot;

    /**
     * Persistent root node of the activities.
     */
    private final NodeStateEx activitiesRoot;

    /**
     * Creates a new instance of this class.
     *
     * @param vMgr the underlying version manager
     * @param ntReg node type registry
     * @param session the session
     * @param cacheFactory cache factory
     * @throws RepositoryException if a an error occurs
     */
    public InternalXAVersionManager(InternalVersionManagerImpl vMgr, NodeTypeRegistry ntReg,
                            SessionImpl session, ItemStateCacheFactory cacheFactory)
            throws RepositoryException {
        super(ntReg, vMgr.historiesId, vMgr.activitiesId, vMgr.getNodeIdFactory());
        this.vMgr = vMgr;
        this.session = session;
        this.stateMgr = XAItemStateManager.createInstance(vMgr.getSharedStateMgr(),
                this, CHANGE_LOG_ATTRIBUTE_NAME, cacheFactory);

        NodeState state;
        try {
            state = (NodeState) stateMgr.getItemState(historiesId);
        } catch (ItemStateException e) {
            throw new RepositoryException("Unable to retrieve history root", e);
        }
        this.historyRoot = new NodeStateEx(stateMgr, ntReg, state, NameConstants.JCR_VERSIONSTORAGE);
        try {
            state = (NodeState) stateMgr.getItemState(activitiesId);
        } catch (ItemStateException e) {
            throw new RepositoryException("Unable to retrieve activities root", e);
        }
        this.activitiesRoot = new NodeStateEx(stateMgr, ntReg, state, NameConstants.JCR_ACTIVITIES);
    }

    //------------------------------------------< EventStateCollectionFactory >

    /**
     * @inheritDoc
     */
    public EventStateCollection createEventStateCollection()
            throws RepositoryException {
        return vMgr.getEscFactory().createEventStateCollection(session);
    }

    //-------------------------------------------------------< InternalVersionManager >

    /**
     * {@inheritDoc}
     */
    public VirtualItemStateProvider getVirtualItemStateProvider() {
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected VersionHistoryInfo createVersionHistory(Session session,
                                                      NodeState node,
                                                      NodeId copiedFrom)
            throws RepositoryException {

        if (isInXA()) {
            NodeStateEx state = internalCreateVersionHistory(node, copiedFrom);
            InternalVersionHistory history =
                new InternalVersionHistoryImpl(vMgr, state);
            xaItems.put(state.getNodeId(), history);
            Name root = NameConstants.JCR_ROOTVERSION;
            return new VersionHistoryInfo(
                    state.getNodeId(),
                    state.getState().getChildNodeEntry(root, 1).getId());
        }
        return vMgr.createVersionHistory(session, node, copiedFrom);
    }

    /**
     * {@inheritDoc}
     */
    public NodeId createActivity(Session session, String title)
            throws RepositoryException {
        if (isInXA()) {
            NodeStateEx state = internalCreateActivity(title);
            InternalActivityImpl activity =
                new InternalActivityImpl(vMgr, state);
            xaItems.put(state.getNodeId(), activity);
            return state.getNodeId();
        } else {
            return vMgr.createActivity(session, title);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void removeActivity(Session session, NodeId nodeId)
            throws RepositoryException {

        if (isInXA()) {
            InternalActivityImpl act = (InternalActivityImpl) getItem(nodeId);
            internalRemoveActivity(act);
        }
        vMgr.removeActivity(session, nodeId);
    }

    /**
     * {@inheritDoc}
     * <p>
     * Before modifying activity, make a local copy of it.
     */
    @Override
    protected void internalRemoveActivity(InternalActivityImpl activity)
            throws VersionException, RepositoryException {
        if (activity.getVersionManager() != this) {
            activity = makeLocalCopy(activity);
            xaItems.put(activity.getId(), activity);
        }
        super.internalRemoveActivity(activity);
    }

    /**
     * {@inheritDoc}
     */
    public NodeId canCheckout(NodeStateEx state, NodeId activityId) throws RepositoryException {
        return vMgr.canCheckout(state, activityId);
    }

    /**
     * {@inheritDoc}
     */
    public InternalVersion checkin(
            Session session, NodeStateEx node, Calendar created)
            throws RepositoryException {
        if (isInXA()) {
            return checkin(node, created);
        } else {
            return vMgr.checkin(session, node, created);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void removeVersion(Session session, InternalVersionHistory history,
                              Name versionName)
            throws RepositoryException {
        if (isInXA()) {
            internalRemoveVersion((InternalVersionHistoryImpl) history, versionName);
        } else {
            vMgr.removeVersion(session, history, versionName);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void removeVersionHistory(Session session, InternalVersionHistory history)
            throws RepositoryException {
        if (isInXA()) {
            internalRemoveVersionHistory((InternalVersionHistoryImpl) history);
        } else {
            vMgr.removeVersionHistory(session, history);
        }
    }

    /**
     * {@inheritDoc}
     */
    public InternalVersion setVersionLabel(Session session,
                                           InternalVersionHistory history,
                                           Name version,
                                           Name label, boolean move)
            throws RepositoryException {

        if (isInXA()) {
            return setVersionLabel((InternalVersionHistoryImpl) history,
                    version, label, move);
        } else {
            return vMgr.setVersionLabel(session, history, version, label, move);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void close() throws Exception {
        stateMgr.dispose();
    }

    //---------------------------------------------< VirtualItemStateProvider >

    /**
     * {@inheritDoc}
     */
    public boolean isVirtualRoot(ItemId id) {
        return false;
    }

    /**
     * {@inheritDoc}
     */
    public NodeId getVirtualRootId() {
        // never used
        return null;
    }

    public NodeId[] getVirtualRootIds() {
        // never used
        return null;
    }

    /**
     * {@inheritDoc}
     */
    public VirtualPropertyState createPropertyState(VirtualNodeState parent,
                                                    Name name, int type,
                                                    boolean multiValued)
            throws RepositoryException {

        throw new IllegalStateException("Read-only");
    }

    /**
     * {@inheritDoc}
     */
    public VirtualNodeState createNodeState(VirtualNodeState parent, Name name,
                                            NodeId id, Name nodeTypeName)
            throws RepositoryException {

        throw new IllegalStateException("Read-only");
    }

    /**
     * {@inheritDoc}
     */
    public boolean setNodeReferences(ChangeLog references) {
        ChangeLog changeLog = ((XAItemStateManager) stateMgr).getChangeLog();
        if (changeLog != null) {
            for (NodeReferences refs : references.modifiedRefs()) {
                changeLog.modified(refs);
            }
            return true;
        } else {
            return false;
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * Return item states for changes only. Global version manager will return
     * other items.
     */
    public ItemState getItemState(ItemId id)
            throws NoSuchItemStateException, ItemStateException {

        ChangeLog changeLog = ((XAItemStateManager) stateMgr).getChangeLog();
        if (changeLog != null) {
            return changeLog.get(id);
        }
        throw new NoSuchItemStateException("State not in change log: " + id);
    }

    /**
     * {@inheritDoc}
     */
    public boolean hasItemState(ItemId id) {
        ChangeLog changeLog = ((XAItemStateManager) stateMgr).getChangeLog();
        if (changeLog != null) {
            return changeLog.has(id);
        }
        return false;
    }

    /**
     * {@inheritDoc}
     */
    public NodeReferences getNodeReferences(NodeId id)
            throws NoSuchItemStateException, ItemStateException {

        ChangeLog changeLog = ((XAItemStateManager) stateMgr).getChangeLog();
        if (changeLog != null) {
            return changeLog.getReferencesTo(id);
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    public boolean hasNodeReferences(NodeId id) {
        ChangeLog changeLog = ((XAItemStateManager) stateMgr).getChangeLog();
        if (changeLog != null) {
            return changeLog.getReferencesTo(id) != null;
        }
        return false;
    }

    /**
     * {@inheritDoc}
     * <p>
     * Not needed.
     */
    public void addListener(ItemStateListener listener) {
    }

    /**
     * {@inheritDoc}
     * <p>
     * Not needed.
     */
    public void removeListener(ItemStateListener listener) {
    }

    //-----------------------------------------------< InternalVersionManagerBase >

    /**
     * {@inheritDoc}
     */
    @Override
    protected NodeStateEx getHistoryRoot() {
        return historyRoot;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected NodeStateEx getActivitiesRoot() {
        return activitiesRoot;
    }

    /**
     * {@inheritDoc}
     */
     @Override
     protected InternalVersionItem getItem(NodeId id) throws RepositoryException {
        InternalVersionItem item = null;
        if (xaItems != null) {
            item = xaItems.get(id);
        }
        if (item == null) {
            item = vMgr.getItem(id);
        }
        return item;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected boolean hasItem(NodeId id) {
        if (xaItems != null && xaItems.containsKey(id)) {
            return true;
        }
        return vMgr.hasItem(id);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected boolean hasItemReferences(NodeId id)
            throws RepositoryException {
        return session.getNodeById(id).getReferences().hasNext();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected NodeStateEx getNodeStateEx(NodeId parentNodeId)
            throws RepositoryException {
        try {
            NodeState state = (NodeState) stateMgr.getItemState(parentNodeId);
            return new NodeStateEx(stateMgr, ntReg, state, null);
        } catch (ItemStateException e) {
            throw new RepositoryException(e);
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * Before modifying version history given, make a local copy of it.
     */
    @Override
    protected InternalVersion internalCheckin(
            InternalVersionHistoryImpl history,
            NodeStateEx node, boolean simple, Calendar created)
            throws RepositoryException {

        if (history.getVersionManager() != this) {
            history = makeLocalCopy(history);
            xaItems.put(history.getId(), history);
        }
        InternalVersion version =
            super.internalCheckin(history, node, simple, created);
        NodeId frozenNodeId = version.getFrozenNodeId();
        InternalVersionItem frozenNode = createInternalVersionItem(frozenNodeId);
        if (frozenNode != null) {
            xaItems.put(frozenNodeId, frozenNode);
        }
        return version;
    }

    /**
     * {@inheritDoc}
     * <p>
     * Before modifying version history given, make a local copy of it.
     */
    @Override
    protected void internalRemoveVersion(InternalVersionHistoryImpl history, Name name)
            throws VersionException, RepositoryException {

        if (history.getVersionManager() != this) {
            history = makeLocalCopy(history);
            xaItems.put(history.getId(), history);
            // also put 'successor' and 'predecessor' version items to xaItem sets
            InternalVersion v = history.getVersion(name);
            for (InternalVersion v1 : v.getSuccessors()) {
                xaItems.put(v1.getId(), v1);
            }
            for (InternalVersion v1 : v.getPredecessors()) {
                xaItems.put(v1.getId(), v1);
            }
        }
        super.internalRemoveVersion(history, name);
    }

    /**
     * {@inheritDoc}
     * <p>
     * Before modifying version history given, make a local copy of it.
     */
    @Override
    protected InternalVersion setVersionLabel(InternalVersionHistoryImpl history,
                                              Name version, Name label,
                                              boolean move)
            throws RepositoryException {

        if (history.getVersionManager() != this) {
            history = makeLocalCopy(history);
            xaItems.put(history.getId(), history);
        }
        return super.setVersionLabel(history, version, label, move);
    }

    /**
     * {@inheritDoc}
     * <p>
     * Put the version object into our cache.
     */
    @Override
    protected void versionCreated(InternalVersion version) {
        xaItems.put(version.getId(), version);
    }

    /**
     * {@inheritDoc}
     * <p>
     * Remove the version object from our cache.
     */
    @Override
    protected void versionDestroyed(InternalVersion version) {
        xaItems.remove(version.getId());
    }

    //-------------------------------------------------------------------< XA >

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public void associate(TransactionContext tx) {
        ((XAItemStateManager) stateMgr).associate(tx);

        Map<NodeId, InternalVersionItem> xaItems = null;
        if (tx != null) {
            xaItems = (Map<NodeId, InternalVersionItem>) tx.getAttribute(ITEMS_ATTRIBUTE_NAME);
            if (xaItems == null) {
                xaItems = new HashMap<NodeId, InternalVersionItem>();
                tx.setAttribute(ITEMS_ATTRIBUTE_NAME, xaItems);
            }
        }
        this.xaItems = xaItems;
    }

    /**
     * {@inheritDoc}
     * <p>
     * Delegate the call to our XA item state manager.
     */
    public void beforeOperation(TransactionContext tx) {
        ((XAItemStateManager) stateMgr).beforeOperation(tx);
    }

    /**
     * {@inheritDoc}
     * <p>
     * Delegate the call to our XA item state manager.
     */
    public void prepare(TransactionContext tx) throws TransactionException {
        if (vmgrLocked) {
            ((XAItemStateManager) stateMgr).prepare(tx);
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * Delegate the call to our XA item state manager. If successful, inform
     * global repository manager to update its caches.
     */
    @SuppressWarnings("unchecked")
    public void commit(TransactionContext tx) throws TransactionException {
        if (vmgrLocked) {
            ((XAItemStateManager) stateMgr).commit(tx);
            Map<NodeId, InternalVersionItem> xaItems =
                    (Map<NodeId, InternalVersionItem>) tx.getAttribute(ITEMS_ATTRIBUTE_NAME);
            vMgr.itemsUpdated(xaItems.values());
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * Delegate the call to our XA item state manager.
     */
    public void rollback(TransactionContext tx) {
        if (vmgrLocked) {
            ((XAItemStateManager) stateMgr).rollback(tx);
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * Delegate the call to our XA item state manager.
     */
    public void afterOperation(TransactionContext tx) {
        ((XAItemStateManager) stateMgr).afterOperation(tx);
    }

    /**
     * Returns an {@link InternalXAResource} that acquires a write lock on the
     * version manager in {@link InternalXAResource#prepare(TransactionContext)}.
     *
     * @return an internal XA resource.
     */
    public InternalXAResource getXAResourceBegin() {
        return new InternalXAResource() {
            public void associate(TransactionContext tx) {
            }

            public void beforeOperation(TransactionContext tx) {
            }

            public void prepare(TransactionContext tx) {
                vmgrLock = vMgr.acquireWriteLock();
                vmgrLocked = true;
            }

            public void commit(TransactionContext tx) {
                // JCR-2712: Ensure that the transaction is prepared
                if (!vmgrLocked) {
                    prepare(tx);
                }
            }

            public void rollback(TransactionContext tx) {
                // JCR-2712: Ensure that the transaction is prepared
                if (!vmgrLocked) {
                    prepare(tx);
                }
            }

            public void afterOperation(TransactionContext tx) {
            }
        };
    }

    /**
     * Returns an {@link InternalXAResource} that releases the write lock on the
     * version manager in {@link InternalXAResource#commit(TransactionContext)}
     * or {@link InternalXAResource#rollback(TransactionContext)}.
     *
     * @return an internal XA resource.
     */
    public InternalXAResource getXAResourceEnd() {
        return new InternalXAResource() {
            public void associate(TransactionContext tx) {
            }

            public void beforeOperation(TransactionContext tx) {
            }

            public void prepare(TransactionContext tx) {
            }

            public void commit(TransactionContext tx) {
                internalReleaseWriteLock();
            }

            public void rollback(TransactionContext tx) {
                internalReleaseWriteLock();
            }

            public void afterOperation(TransactionContext tx) {
            }

            private void internalReleaseWriteLock() {
                if (vmgrLocked) {
                    vmgrLock.release();
                    vmgrLocked = false;
                }
            }
        };
    }

    //-------------------------------------------------------< implementation >

    /**
     * Return a flag indicating whether this version manager is currently
     * associated with an XA transaction.
     * @return <code>true</code> if the version manager is in a transaction
     */
    private boolean isInXA() {
        return xaItems != null;
    }

    /**
     * Make a local copy of an internal version item. This will recreate the
     * (global) version item with state information from our own state
     * manager.
     * @param history source
     * @return the new copy
     * @throws RepositoryException if an error occurs
     */
    private InternalVersionHistoryImpl makeLocalCopy(InternalVersionHistoryImpl history)
            throws RepositoryException {
        VersioningLock.ReadLock lock = acquireReadLock();
        try {
            NodeState state = (NodeState) stateMgr.getItemState(history.getId());
            NodeStateEx stateEx = new NodeStateEx(stateMgr, ntReg, state, null);
            return new InternalVersionHistoryImpl(this, stateEx);
        } catch (ItemStateException e) {
            throw new RepositoryException("Unable to make local copy", e);
        } finally {
            lock.release();
        }
    }

    /**
     * Make a local copy of an internal version item. This will recreate the
     * (global) version item with state information from our own state
     * manager.
     * @param act source
     * @return the new copy
     * @throws RepositoryException if an error occurs
     */
    private InternalActivityImpl makeLocalCopy(InternalActivityImpl act)
            throws RepositoryException {
        VersioningLock.ReadLock lock = acquireReadLock();
        try {
            NodeState state = (NodeState) stateMgr.getItemState(act.getId());
            NodeStateEx stateEx = new NodeStateEx(stateMgr, ntReg, state, null);
            return new InternalActivityImpl(this, stateEx);
        } catch (ItemStateException e) {
            throw new RepositoryException("Unable to make local copy", e);
        } finally {
            lock.release();
        }
    }

    /**
     * Return a flag indicating whether an internal version item belongs to
     * a different XA environment.
     * @param item the item to check
     * @return <code>true</code> if in a different env
     */
    boolean differentXAEnv(InternalVersionItemImpl item) {
        if (item.getVersionManager() == this) {
            if (xaItems == null || !xaItems.containsKey(item.getId())) {
                return true;
            }
        }
        return false;
    }
}
