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

import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;

import javax.jcr.InvalidItemStateException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.RepositoryException;

import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyEntry;
import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry;
import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider;
import org.apache.jackrabbit.spi.ItemId;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.util.WeakIdentityCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <code>ItemState</code> represents the state of an <code>Item</code>.
 */
public abstract class ItemState {

    /**
     * Logger instance
     */
    private static Logger log = LoggerFactory.getLogger(ItemState.class);

    /**
     * the internal status of this item state
     */
    private int status;

    /**
     * The hierarchy entry this state belongs to.
     */
    private final HierarchyEntry hierarchyEntry;

    /**
     * Listeners (weak references)
     */
    @SuppressWarnings("unchecked")
    private final transient Collection<ItemStateLifeCycleListener> listeners = new WeakIdentityCollection(5);

    /**
     * The <code>ItemStateFactory</code> which is used to create new
     * <code>ItemState</code> instances.
     */
    final ItemStateFactory isf;

    final ItemDefinitionProvider definitionProvider;

    /**
     * Constructs an item state
     *
     * @param entry
     * @param isf
     * @param definitionProvider
     */
    protected ItemState(HierarchyEntry entry, ItemStateFactory isf,
                        ItemDefinitionProvider definitionProvider) {
        this(getInitialStatus(entry.getParent()), entry, isf, definitionProvider);
    }

    /**
     * Constructs an item state
     *
     * @param entry
     * @param isf
     * @param definitionProvider
     */
    protected ItemState(int initialStatus, HierarchyEntry entry,
                        ItemStateFactory isf,
                        ItemDefinitionProvider definitionProvider) {
        if (entry == null) {
            throw new IllegalArgumentException("Cannot build ItemState from 'null' HierarchyEntry");
        }
        switch (initialStatus) {
            case Status.EXISTING:
            case Status.NEW:
            case Status.EXISTING_REMOVED:
                status = initialStatus;
                break;
            default:
                String msg = "illegal status: " + initialStatus;
                log.debug(msg);
                throw new IllegalArgumentException(msg);
        }
        this.hierarchyEntry = entry;
        this.isf = isf;
        this.definitionProvider = definitionProvider;
    }

    /**
     *
     * @param parent
     * @return
     */
    private static int getInitialStatus(NodeEntry parent) {
        int status = Status.EXISTING;
        // walk up hierarchy and check if any of the parents is transiently
        // removed, in which case the status must be set to EXISTING_REMOVED.
        while (parent != null) {
            if (parent.getStatus() == Status.EXISTING_REMOVED) {
                status = Status.EXISTING_REMOVED;
                break;
            }
            parent = parent.getParent();
        }
        return status;
    }

    //----------------------------------------------------------< ItemState >---
    /**
     * The <code>HierarchyEntry</code> corresponding to this <code>ItemState</code>.
     *
     * @return The <code>HierarchyEntry</code> corresponding to this <code>ItemState</code>.
     */
    public HierarchyEntry getHierarchyEntry() {
        return hierarchyEntry;
    }

    /**
     * Returns <code>true</code> if this item state is valid and can be accessed.
     * @return
     * @see Status#isValid(int)
     * @see Status#isStale(int)
     */
    public boolean isValid() {
        return Status.isValid(getStatus()) || Status.isStale(getStatus());
    }

    /**
     * Utility method:
     * Determines if this item state represents a node.
     *
     * @return true if this item state represents a node, otherwise false.
     */
    public abstract boolean isNode();

    /**
     * Utility method:
     * Returns the name of this state. Shortcut for calling 'getName' on the
     * {@link ItemState#getHierarchyEntry() hierarchy entry}.
     *
     * @return name of this state
     */
    public Name getName() {
        return getHierarchyEntry().getName();
    }

    /**
     * Utility method:
     * Returns the identifier of this item state. Shortcut for calling 'getId'
     * on the {@link ItemState#getHierarchyEntry() hierarchy entry}.
     *
     * @return the identifier of this item state..
     */
    public abstract ItemId getId() throws RepositoryException;

    /**
     * Utility method:
     * Returns the identifier of this item state. Shortcut for calling 'getWorkspaceId'
     * on the NodeEntry or PropertyEntry respectively.
     *
     * @return the identifier of this item state..
     */
    public abstract ItemId getWorkspaceId() throws RepositoryException;

    /**
     * Utility method:
     * Returns the path of this item state. Shortcut for calling
     * 'getPath' on the {@link ItemState#getHierarchyEntry() hierarchy entry}.
     *
     * @return
     * @throws RepositoryException if an error occurs
     */
    public Path getPath() throws RepositoryException {
        return getHierarchyEntry().getPath();
    }

    /**
     * Utility method: Shortcut for calling
     * 'getParent().getNodeState()' on the {@link ItemState#getHierarchyEntry()
     * hierarchy entry}.
     *
     * @return
     * @throws ItemNotFoundException
     * @throws RepositoryException
     */
    public NodeState getParent() throws ItemNotFoundException, RepositoryException {
        return getHierarchyEntry().getParent().getNodeState();
    }

    /**
     * Returns the status of this item.
     *
     * @return the status of this item.
     */
    public final int getStatus() {
        // Call calculateStatus to apply a possible pending invalidation
        // in the entry hierarchy.
        getHierarchyEntry().calculateStatus();
        return status;
    }

    /**
     * Sets the new status of this item.
     *
     * @param newStatus the new status
     */
    public void setStatus(int newStatus) {
        int oldStatus = status;
        if (oldStatus == newStatus) {
            return;
        }

        if (oldStatus == Status.REMOVED) {
            throw new IllegalStateException("State is already in terminal status " + Status.getName(oldStatus));
        }
        if (Status.isValidStatusChange(oldStatus, newStatus)) {
            status = Status.getNewStatus(oldStatus, newStatus);
        } else {
            throw new IllegalArgumentException("Invalid new status " + Status.getName(newStatus) + " for state with status " + Status.getName(oldStatus));
        }
        // Notify listeners about status change
        // copy listeners to array to avoid ConcurrentModificationException
        ItemStateLifeCycleListener[] la;
        synchronized (listeners) {
            la = listeners.toArray(new ItemStateLifeCycleListener[listeners.size()]);
        }
        for (int i = 0; i < la.length; i++) {
            if (la[i] != null) {
                la[i].statusChanged(this, oldStatus);
            }
        }
        if (status == Status.MODIFIED) {
            /*
            change back tmp MODIFIED status, that is used as marker only
            inform listeners about (external) changes.
            */
            status = Status.EXISTING;
        }
    }

    /**
     * Merge all data from the given state into this state. If
     * '<code>keepChanges</code>' is true, transient modifications present on
     * this state are not touched. Otherwise this state is completely reset
     * according to the given other state.
     *
     * @param another
     * @param keepChanges
     * @return a MergeResult instance which represent the result of the merge operation
     */
    public abstract MergeResult merge(ItemState another, boolean keepChanges);

    /**
     * Revert all transient modifications made to this ItemState.
     *
     * @return true if this state has been modified i.e. if there was anything
     * to revert.
     */
    public abstract boolean revert();

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

    /**
     * Remove an <code>ItemStateLifeCycleListener</code>
     *
     * @param listener an existing listener
     */
    public void removeListener(ItemStateLifeCycleListener listener) {
        synchronized (listeners) {
            listeners.remove(listener);
        }
    }

    /**
     * Unmodifiable iterator over the listeners present on this item state.
     *
     * @return iterator over <code>ItemStateLifeCycleListener</code>s.
     */
    public Iterator<ItemStateLifeCycleListener> getListeners() {
        return Collections.unmodifiableCollection(listeners).iterator();
    }

    /**
     * Invalidates this state: set its {@link Status} to {@link Status#INVALIDATED}
     * if the current status is {@link Status#EXISTING}. Does nothing otherwise.
     */
    public void invalidate() {
        if (status == Status.EXISTING) {
            setStatus(Status.INVALIDATED);
        } else {
            log.debug("Skip invalidation for item {} with status {}", getName(), Status.getName(status));
        }
    }

    /**
     * Marks this item state as modified.
     */
    void markModified() throws InvalidItemStateException {
        switch (status) {
            case Status.EXISTING:
                setStatus(Status.EXISTING_MODIFIED);
                break;
            case Status.EXISTING_MODIFIED:
                // already modified, do nothing
                break;
            case Status.NEW:
                // still new, do nothing
                break;
            case Status.STALE_DESTROYED:
            case Status.STALE_MODIFIED:
                // should actually not get here because item should check before
                // it modifies an item state.
                throw new InvalidItemStateException("Cannot mark stale state modified.");

            case Status.EXISTING_REMOVED:
            default:
                String msg = "Cannot mark item state with status '" + Status.getName(status) + "' modified.";
                throw new InvalidItemStateException(msg);
        }
    }

    // -----------------------------------------------------< MergeResult >---

    /**
     * A MergeResult represents the result of a {@link ItemState#merge(ItemState, boolean)}
     * operation.
     */
    public interface MergeResult {

        /**
         * @return  true iff the target state of {@link ItemState#merge(ItemState, boolean)}
         * was modified.
         */
        public boolean modified();

        /**
         * Dispose this MergeResult and release all internal resources that
         * are not needed any more.
         */
        public void dispose();
    }

    /**
     * A SimpleMergeResult is just a holder for a modification status.
     * The {@link #modified()} method just returns the modification status passed
     * to the constructor.
     */
    protected class SimpleMergeResult implements MergeResult {
        private final boolean modified;

        /**
         * @param modified  modification status
         */
        public SimpleMergeResult(boolean modified) {
            this.modified = modified;
        }

        public boolean modified() {
            return modified;
        }

        public void dispose() {
            // nothing to do.
        }
    }

}
