/*
 * 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.state;

import javax.jcr.InvalidItemStateException;
import javax.jcr.ReferentialIntegrityException;
import javax.jcr.RepositoryException;

import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.id.NodeIdFactory;
import org.apache.jackrabbit.core.id.PropertyId;
import org.apache.jackrabbit.core.observation.EventStateCollectionFactory;
import org.apache.jackrabbit.spi.Name;

/**
 * Local <code>ItemStateManager</code> that isolates changes to
 * persistent states from other clients.
 */
public class LocalItemStateManager
        implements UpdatableItemStateManager, NodeStateListener {

    /**
     * cache of weak references to ItemState objects issued by this
     * ItemStateManager
     */
    private final ItemStateCache cache;

    /**
     * Shared item state manager
     */
    protected final SharedItemStateManager sharedStateMgr;

    /**
     * Event state collection factory.
     */
    protected final EventStateCollectionFactory factory;

    /**
     * Flag indicating whether this item state manager is in edit mode
     */
    private boolean editMode;

    /**
     * Change log
     */
    private final ChangeLog changeLog = new ChangeLog();

    /**
     * State change dispatcher.
     */
    private final transient StateChangeDispatcher dispatcher = new StateChangeDispatcher();

    /**
     * Creates a new <code>LocalItemStateManager</code> instance.
     * @param sharedStateMgr shared state manager
     * @param factory event state collection factory
     */
    protected LocalItemStateManager(SharedItemStateManager sharedStateMgr,
                                 EventStateCollectionFactory factory, ItemStateCacheFactory cacheFactory) {
        cache = new ItemStateReferenceCache(cacheFactory);
        this.sharedStateMgr = sharedStateMgr;
        this.factory = factory;
    }

    /**
     * Creates a new {@code LocalItemStateManager} instance and registers it as an {@link ItemStateListener}
     * with the given {@link SharedItemStateManager}.
     *
     * @param sharedStateMgr the {@link SharedItemStateManager}
     * @param factory the {@link EventStateCollectionFactory}
     * @param cacheFactory the {@link ItemStateCacheFactory}
     * @return a new {@code LocalItemStateManager} instance
     */
    public static LocalItemStateManager createInstance(SharedItemStateManager sharedStateMgr,
            EventStateCollectionFactory factory, ItemStateCacheFactory cacheFactory) {
        LocalItemStateManager mgr = new LocalItemStateManager(sharedStateMgr, factory, cacheFactory);
        sharedStateMgr.addListener(mgr);
        return mgr;
    }

    /**
     * Retrieve a node state from the parent shared state manager and
     * wraps it into a intermediate object that helps us handle local
     * modifications.
     *
     * @param id node id
     * @return node state
     * @throws NoSuchItemStateException
     * @throws ItemStateException
     */
    protected NodeState getNodeState(NodeId id)
            throws NoSuchItemStateException, ItemStateException {

        // load from parent manager and wrap
        NodeState state = (NodeState) sharedStateMgr.getItemState(id);
        state = new NodeState(state, state.getStatus(), false);

        // put it in cache
        cache.cache(state);

        // set parent container
        state.setContainer(this);
        return state;
    }

    /**
     * Retrieve a property state from the parent shared state manager and
     * wraps it into a intermediate object that helps us handle local
     * modifications.
     *
     * @param id property id
     * @return property state
     * @throws NoSuchItemStateException
     * @throws ItemStateException
     */
    protected PropertyState getPropertyState(PropertyId id)
            throws NoSuchItemStateException, ItemStateException {

        // load from parent manager and wrap
        PropertyState state = (PropertyState) sharedStateMgr.getItemState(id);
        state = new PropertyState(state, state.getStatus(), false);

        // put it in cache
        cache.cache(state);

        // set parent container
        state.setContainer(this);
        return state;
    }

    /**
     * Returns the change log that contains the current changes in this local
     * item state manager.
     *
     * @return the change log with the current changes.
     */
    protected ChangeLog getChanges() {
        return changeLog;
    }

    //-----------------------------------------------------< ItemStateManager >
    /**
     * {@inheritDoc}
     */
    public ItemState getItemState(ItemId id)
            throws NoSuchItemStateException, ItemStateException {

        // check change log
        ItemState state = changeLog.get(id);
        if (state != null) {
            return state;
        }

        // check cache. synchronized to ensure an entry is not created twice.
        synchronized (this) {
            state = cache.retrieve(id);
            if (state == null) {
                // regular behaviour
                if (id.denotesNode()) {
                    state = getNodeState((NodeId) id);
                } else {
                    state = getPropertyState((PropertyId) id);
                }
            }
            return state;
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean hasItemState(ItemId id) {

        // check items in change log
        try {
            ItemState state = changeLog.get(id);
            if (state != null) {
                return true;
            }
        } catch (NoSuchItemStateException e) {
            return false;
        }

        // check cache
        if (cache.isCached(id)) {
            return true;
        }

        // regular behaviour
        return sharedStateMgr.hasItemState(id);
    }

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

        // check change log
        NodeReferences refs = changeLog.getReferencesTo(id);
        if (refs != null) {
            return refs;
        }
        return sharedStateMgr.getNodeReferences(id);
    }

    /**
     * {@inheritDoc}
     */
    public boolean hasNodeReferences(NodeId id) {
        // check change log
        if (changeLog.getReferencesTo(id) != null) {
            return true;
        }
        return sharedStateMgr.hasNodeReferences(id);
    }


    //--------------------------------------------< UpdatableItemStateManager >
    /**
     * {@inheritDoc}
     */
    public synchronized void edit() throws IllegalStateException {
        if (editMode) {
            throw new IllegalStateException("Already in edit mode");
        }
        changeLog.reset();

        editMode = true;
    }

    /**
     * {@inheritDoc}
     */
    public boolean inEditMode() {
        return editMode;
    }

    /**
     * {@inheritDoc}
     */
    public NodeState createNew(
            NodeId id, Name nodeTypeName, NodeId parentId)
            throws RepositoryException {
        if (!editMode) {
            throw new RepositoryException("Not in edit mode");
        }

        boolean nonRandomId = true;
        if (id == null) {
            id = getNodeIdFactory().newNodeId();
            nonRandomId = false;
        }

        NodeState state = new NodeState(
                id, nodeTypeName, parentId, ItemState.STATUS_NEW, false);
        changeLog.added(state);
        state.setContainer(this);

        if (nonRandomId && !changeLog.deleted(id)
                && sharedStateMgr.hasItemState(id)) {
            throw new InvalidItemStateException(
                    "Node " + id + " already exists");
        }

        return state;
    }

    /**
     * Returns the local node state below the given transient one. If given
     * a fresh new node state, then a new local state is created and added
     * to the change log.
     *
     * @param transientState transient state
     * @return local node state
     * @throws RepositoryException if the local state could not be created
     */
    public NodeState getOrCreateLocalState(NodeState transientState)
            throws RepositoryException {
        NodeState localState = (NodeState) transientState.getOverlayedState();
        if (localState == null) {
            // The transient node state is new, create a new local state
            localState = new NodeState(
                    transientState.getNodeId(),
                    transientState.getNodeTypeName(),
                    transientState.getParentId(),
                    ItemState.STATUS_NEW,
                    false);
            changeLog.added(localState);
            localState.setContainer(this);
            try {
                transientState.connect(localState);
            } catch (ItemStateException e) {
                // should never happen
                throw new RepositoryException(e);
            }
        }
        return localState;
    }

    /**
     * {@inheritDoc}
     */
    public PropertyState createNew(Name propName, NodeId parentId)
            throws IllegalStateException {
        if (!editMode) {
            throw new IllegalStateException("Not in edit mode");
        }
        PropertyState state = new PropertyState(
                new PropertyId(parentId, propName), ItemState.STATUS_NEW, false);
        changeLog.added(state);
        state.setContainer(this);
        return state;
    }

    /**
     * {@inheritDoc}
     */
    public void store(ItemState state) throws IllegalStateException {
        if (!editMode) {
            throw new IllegalStateException("Not in edit mode");
        }
        changeLog.modified(state);
    }

    /**
     * {@inheritDoc}
     */
    public void destroy(ItemState state) throws IllegalStateException {
        assert state != null;
        if (!editMode) {
            throw new IllegalStateException("Not in edit mode");
        }
        changeLog.deleted(state);
    }

    /**
     * {@inheritDoc}
     */
    public void cancel() throws IllegalStateException {
        if (!editMode) {
            throw new IllegalStateException("Not in edit mode");
        }
        changeLog.undo(sharedStateMgr);

        editMode = false;
    }

    /**
     * {@inheritDoc}
     */
    public void update()
            throws ReferentialIntegrityException, StaleItemStateException,
            ItemStateException, IllegalStateException {
        if (!editMode) {
            throw new IllegalStateException("Not in edit mode");
        }
        // JCR-1813: Only execute the update when there are some changes
        if (changeLog.hasUpdates()) {
            update(changeLog);
            changeLog.reset();
        }

        editMode = false;
    }

    /**
     * End an update operation. Fetch the states and references from
     * the parent (shared) item manager, reconnect them to the items
     * collected in our (local) change log and overwrite the shared
     * items with our copies.
     *
     * @param changeLog change log containing local states and references
     * @throws ReferentialIntegrityException if a new or modified REFERENCE
     *                                       property refers to a non-existent
     *                                       target or if a removed node is still
     *                                       being referenced
     * @throws StaleItemStateException       if at least one of the affected item
     *                                       states has become stale in the meantime
     * @throws ItemStateException            if an error occurs
     */
    protected void update(ChangeLog changeLog)
            throws ReferentialIntegrityException, StaleItemStateException, ItemStateException {

        sharedStateMgr.update(changeLog, factory);
        changeLog.persisted();
    }

    /**
     * {@inheritDoc}
     */
    public void dispose() {
        sharedStateMgr.removeListener(this);

        // this LocalItemStateManager instance is no longer needed;
        // cached item states can now be safely discarded
        ItemState[] states = cache.retrieveAll();
        for (int i = 0; i < states.length; i++) {
            ItemState state = states[i];
            if (state != null) {
                dispatcher.notifyStateDiscarded(state);
                // let the item state know that it has been disposed
                state.onDisposed();
            }
        }

        // clear cache
        cache.evictAll();
        cache.dispose();
    }

    /**
     * Add an <code>ItemStateListener</code>
     * @param listener the new listener to be informed on modifications
     */
    public void addListener(ItemStateListener listener) {
        dispatcher.addListener(listener);
    }

    /**
     * Remove an <code>ItemStateListener</code>
     * @param listener an existing listener
     */
    public void removeListener(ItemStateListener listener) {
        dispatcher.removeListener(listener);
    }

    //----------------------------------------------------< ItemStateListener >

    /**
     * {@inheritDoc}
     * <p/>
     * Notification handler gets called for both local states that this state manager
     * has created, as well as states that were created by the shared state manager
     * we're listening to.
     */
    public void stateCreated(ItemState created) {
        ItemState local = null;
        if (created.getContainer() != this) {
            // shared state was created
            try {
                local = changeLog.get(created.getId());
                if (local != null) {
                    if (local.isNode() && local.getOverlayedState() != created) {
                        // mid-air collision of concurrent node state creation
                        // with same id (JCR-2272)
                        if (local.getStatus() == ItemState.STATUS_NEW) {
                            local.setStatus(ItemState.STATUS_UNDEFINED); // we need a state that is != NEW
                        }
                    } else {
                        if (local.getOverlayedState() == created) {
                            // underlying state has been permanently created
                            local.pull();
                            local.setStatus(ItemState.STATUS_EXISTING);
                            cache.cache(local);
                        }
                    }
                }
            } catch (NoSuchItemStateException e) {
                /* ignore */
            }
        } else {
            // local state was created
            local = created;
            // just ensure that the newly created state is still cached. it can
            // happen during a restore operation that a state with the same id
            // is deleted and created (JCR-1197)
            if (!cache.isCached(created.getId())) {
                cache.cache(local);
            }
        }
        dispatcher.notifyStateCreated(created);
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Notification handler gets called for both local states that this state manager
     * has created, as well as states that were created by the shared state manager
     * we're listening to.
     */
    public void stateModified(ItemState modified) {
        ItemState local;
        if (modified.getContainer() != this) {
            // shared state was modified
            local = cache.retrieve(modified.getId());
            if (local != null && local.isConnected()) {
                // this instance represents existing state, update it
                local.pull();
            }
        } else {
            // local state was modified
            local = modified;
        }
        if (local != null) {
            dispatcher.notifyStateModified(local);
        } else if (modified.isNode()) {
            // if the state is not ours (and is not cached) it could have
            // vanished from the weak-ref cache due to a gc. but there could
            // still be some listeners (e.g. CachingHierarchyManager) that want
            // to get notified.
            dispatcher.notifyNodeModified((NodeState) modified);
        }
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Notification handler gets called for both local states that this state manager
     * has created, as well as states that were created by the shared state manager
     * we're listening to.
     */
    public void stateDestroyed(ItemState destroyed) {
        ItemState local = null;
        if (destroyed.getContainer() != this) {
            // shared state was destroyed
            local = cache.retrieve(destroyed.getId());
            if (local != null && local.isConnected()) {
                local.setStatus(ItemState.STATUS_EXISTING_REMOVED);
            }
        } else {
            // local state was destroyed
            local = destroyed;
        }
        cache.evict(destroyed.getId());
        if (local != null) {
            dispatcher.notifyStateDestroyed(local);
        }
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Notification handler gets called for both local states that this state manager
     * has created, as well as states that were created by the shared state manager
     * we're listening to.
     */
    public void stateDiscarded(ItemState discarded) {
        ItemState local = null;
        if (discarded.getContainer() != this) {
            // shared state was discarded
            local = cache.retrieve(discarded.getId());
            if (local != null && local.isConnected()) {
                local.setStatus(ItemState.STATUS_UNDEFINED);
            }
        } else {
            // local state was discarded
            local = discarded;
        }
        cache.evict(discarded.getId());
        if (local != null) {
            dispatcher.notifyStateDiscarded(local);
        }
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Optimization: shared state manager we're listening to does not deliver node state changes, therefore the state
     * concerned must be a local state.
     */
    public void nodeAdded(NodeState state, Name name, int index, NodeId id) {
        dispatcher.notifyNodeAdded(state, name, index, id);
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Optimization: shared state manager we're listening to does not deliver node state changes, therefore the state
     * concerned must be a local state.
     */
    public void nodesReplaced(NodeState state) {
        dispatcher.notifyNodesReplaced(state);
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Optimization: shared state manager we're listening to does not deliver node state changes, therefore the state
     * concerned must be a local state.
     */
    public void nodeModified(NodeState state) {
        dispatcher.notifyNodeModified(state);
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Optimization: shared state manager we're listening to does not deliver node state changes, therefore the state
     * concerned must be a local state.
     */
    public void nodeRemoved(NodeState state, Name name, int index, NodeId id) {
        dispatcher.notifyNodeRemoved(state, name, index, id);
    }

    public NodeIdFactory getNodeIdFactory() {
        return sharedStateMgr.getNodeIdFactory();
    }

}
