/*
 * 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.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Iterator;
import java.util.List;

import javax.jcr.AccessDeniedException;
import javax.jcr.InvalidItemStateException;
import javax.jcr.ItemExistsException;
import javax.jcr.PropertyType;
import javax.jcr.ReferentialIntegrityException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.ValueFormatException;
import javax.jcr.lock.LockException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.NoSuchNodeTypeException;
import javax.jcr.version.VersionException;

import org.apache.jackrabbit.jcr2spi.SessionImpl;
import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry;
import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry;
import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeType;
import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeTypeProvider;
import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider;
import org.apache.jackrabbit.jcr2spi.operation.AddNode;
import org.apache.jackrabbit.jcr2spi.operation.AddProperty;
import org.apache.jackrabbit.jcr2spi.operation.Move;
import org.apache.jackrabbit.jcr2spi.operation.Operation;
import org.apache.jackrabbit.jcr2spi.operation.OperationVisitor;
import org.apache.jackrabbit.jcr2spi.operation.Remove;
import org.apache.jackrabbit.jcr2spi.operation.ReorderNodes;
import org.apache.jackrabbit.jcr2spi.operation.SetMixin;
import org.apache.jackrabbit.jcr2spi.operation.SetPrimaryType;
import org.apache.jackrabbit.jcr2spi.operation.SetPropertyValue;
import org.apache.jackrabbit.jcr2spi.operation.TransientOperationVisitor;
import org.apache.jackrabbit.jcr2spi.util.ReferenceChangeTracker;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.QNodeDefinition;
import org.apache.jackrabbit.spi.QPropertyDefinition;
import org.apache.jackrabbit.spi.QValue;
import org.apache.jackrabbit.spi.QValueFactory;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <code>SessionItemStateManager</code> ...
 */
public class SessionItemStateManager extends TransientOperationVisitor implements UpdatableItemStateManager {

    private static Logger log = LoggerFactory.getLogger(SessionItemStateManager.class);

    /**
     * State manager that allows updates
     */
    private final UpdatableItemStateManager workspaceItemStateMgr;

    /**
     * State manager for the transient items
     */
    private final TransientItemStateManager transientStateMgr;

    private final ItemStateValidator validator;

    private final QValueFactory qValueFactory;

    private final SessionImpl mgrProvider;

    /**
     * Creates a new <code>SessionItemStateManager</code> instance.
     *
     * @param workspaceItemStateMgr
     * @param validator
     * @param qValueFactory
     * @param isf
     * @param mgrProvider
     */
    public SessionItemStateManager(UpdatableItemStateManager workspaceItemStateMgr,
                                   ItemStateValidator validator,
                                   QValueFactory qValueFactory,
                                   ItemStateFactory isf, SessionImpl mgrProvider) {

        this.workspaceItemStateMgr = workspaceItemStateMgr;
        this.transientStateMgr = new TransientItemStateManager();
        isf.addCreationListener(transientStateMgr);

        this.validator = validator;
        this.qValueFactory = qValueFactory;
        this.mgrProvider = mgrProvider;
    }

    /**
     * @return <code>true</code> if this manager has any transient state;
     * <code>false</code> otherwise.
     */
    public boolean hasPendingChanges() {
        return transientStateMgr.hasPendingChanges();
    }

    /**
     * This will save <code>state</code> and all descendants items of
     * <code>state</code> that are transiently modified in a single step. If
     * this operation fails, no item will have been saved.
     *
     * @param state the root state of the update operation
     */
    public void save(ItemState state) throws ReferentialIntegrityException,
            InvalidItemStateException, RepositoryException {
        // shortcut, if no modifications are present
        if (!transientStateMgr.hasPendingChanges()) {
            return;
        }
        // collect the changes to be saved
        ChangeLog changeLog = transientStateMgr.getChangeLog(state, true);
        if (!changeLog.isEmpty()) {
            // only pass changelog if there are transient modifications available
            // for the specified item and its descendants.
            workspaceItemStateMgr.execute(changeLog);
            // remove states and operations just processed from the transient ISM
            transientStateMgr.dispose(changeLog);
            // now its save to clear the changeLog
            changeLog.reset();
        }
    }

    /**
     * This will undo all changes made to <code>state</code> and descendant
     * items of <code>state</code> inside this item state manager.
     *
     * @param itemState the root state of the cancel operation.
     * @throws ConstraintViolationException
     * @throws RepositoryException if undoing changes made to <code>state</code>
     * and descendant items is not a closed set of changes. That is, at least
     * another item needs to be canceled as well in another sub-tree.
     */
    public void undo(ItemState itemState) throws ConstraintViolationException, RepositoryException {
        // short cut
        if (!transientStateMgr.hasPendingChanges()) {
            return;
        }
        ChangeLog changeLog = transientStateMgr.getChangeLog(itemState, false);
        if (!changeLog.isEmpty()) {
            // let changelog revert all changes
            changeLog.undo();
            // remove transient states and related operations from the t-statemanager
            transientStateMgr.dispose(changeLog);
            changeLog.reset();
        }
    }

    /**
     * Adjust references at the end of a successful
     * {@link Session#importXML(String, InputStream, int) XML import}.
     *
     * @param refTracker
     * @throws ConstraintViolationException
     * @throws RepositoryException
     */
    public void adjustReferences(ReferenceChangeTracker refTracker) throws ConstraintViolationException, RepositoryException {
        Iterator<PropertyState> it = refTracker.getReferences();
        while (it.hasNext()) {
            PropertyState propState = it.next();
            boolean modified = false;
            QValue[] values = propState.getValues();
            QValue[] newVals = new QValue[values.length];
            for (int i = 0; i < values.length; i++) {
                QValue val = values[i];
                QValue adjusted = refTracker.getMappedReference(val, qValueFactory);
                if (adjusted != null) {
                    newVals[i] = adjusted;
                    modified = true;
                } else {
                    // reference doesn't need adjusting, just copy old value
                    newVals[i] = val;
                }
            }
            if (modified) {
                int options = ItemStateValidator.CHECK_LOCK |
                    ItemStateValidator.CHECK_VERSIONING |
                    ItemStateValidator.CHECK_CONSTRAINTS;
                setPropertyStateValue(propState, newVals, PropertyType.REFERENCE, options);
            }
        }
        // make sure all entries are removed
        refTracker.clear();
    }

    //------------------------------------------< UpdatableItemStateManager >---
    /**
     * {@inheritDoc}
     * @see UpdatableItemStateManager#execute(Operation)
     */
    public void execute(Operation operation) throws RepositoryException {
        operation.accept(this);
    }

    /**
     * {@inheritDoc}
     * @see UpdatableItemStateManager#execute(ChangeLog)
     */
    public void execute(ChangeLog changes) throws RepositoryException {
        throw new UnsupportedOperationException("Not implemented for SessionItemStateManager");
    }

    /**
     * {@inheritDoc}
     * @see UpdatableItemStateManager#dispose()
     */
    public void dispose() {
        // discard all transient changes
        transientStateMgr.dispose();
        // dispose our (i.e. 'local') state manager
        workspaceItemStateMgr.dispose();
    }

    //---------------------------------------------------< OperationVisitor >---
    /**
     * @see OperationVisitor#visit(AddNode)
     */
    public void visit(AddNode operation) throws LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException, RepositoryException {
        NodeState parent = operation.getParentState();
        ItemDefinitionProvider defProvider = mgrProvider.getItemDefinitionProvider();
        QNodeDefinition def = defProvider.getQNodeDefinition(parent.getAllNodeTypeNames(), operation.getNodeName(), operation.getNodeTypeName());
        List<ItemState> newStates = addNodeState(parent, operation.getNodeName(), operation.getNodeTypeName(), operation.getUuid(), def, operation.getOptions());
        operation.addedState(newStates);

        transientStateMgr.addOperation(operation);
    }

    /**
     * @see OperationVisitor#visit(AddProperty)
     */
    public void visit(AddProperty operation) throws ValueFormatException, LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException {
        NodeState parent = operation.getParentState();
        Name propertyName = operation.getPropertyName();
        QPropertyDefinition pDef = operation.getDefinition();
        int targetType = pDef.getRequiredType();
        if (targetType == PropertyType.UNDEFINED) {
            targetType = operation.getPropertyType();
            if (targetType == PropertyType.UNDEFINED) {
                targetType = PropertyType.STRING;
            }
        }

        addPropertyState(parent, propertyName, targetType, operation.getValues(), pDef, operation.getOptions());

        transientStateMgr.addOperation(operation);
    }

    /**
     * @see OperationVisitor#visit(Move)
     */
    public void visit(Move operation) throws LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException {

        // retrieve states and assert they are modifiable
        NodeState srcState = operation.getSourceState();
        NodeState srcParent = operation.getSourceParentState();
        NodeState destParent = operation.getDestinationParentState();

        // state validation: move-Source can be removed from old/added to new parent
        validator.checkRemoveItem(srcState, operation.getOptions());
        validator.checkAddNode(destParent, operation.getDestinationName(),
            srcState.getNodeTypeName(), operation.getOptions());

        // retrieve applicable definition at the new place
        ItemDefinitionProvider defProvider = mgrProvider.getItemDefinitionProvider();
        QNodeDefinition newDefinition = defProvider.getQNodeDefinition(destParent.getAllNodeTypeNames(), operation.getDestinationName(), srcState.getNodeTypeName());

        // perform the move (modifying states)
        srcParent.moveChildNodeEntry(destParent, srcState, operation.getDestinationName(), newDefinition);

        // remember operation
        transientStateMgr.addOperation(operation);
    }

    /**
     * @see OperationVisitor#visit(Remove)
     */
    public void visit(Remove operation) throws ConstraintViolationException, AccessDeniedException, UnsupportedRepositoryOperationException, VersionException, RepositoryException {
        ItemState state = operation.getRemoveState();
        removeItemState(state, operation.getOptions());

        transientStateMgr.addOperation(operation);
        operation.getParentState().markModified();
    }

    /**
     * @see OperationVisitor#visit(SetMixin)
     */
    public void visit(SetMixin operation) throws ConstraintViolationException, AccessDeniedException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException, RepositoryException {
        // NOTE: nodestate is only modified upon save of the changes!
        Name[] mixinNames = operation.getMixinNames();
        NodeState nState = operation.getNodeState();
        NodeEntry nEntry = nState.getNodeEntry();

        // assert the existence of the property entry and set the array of
        // mixinNames to be set on the corresponding property state
        PropertyEntry mixinEntry = nEntry.getPropertyEntry(NameConstants.JCR_MIXINTYPES);
        if (mixinNames.length > 0) {
            // update/create corresponding property state
            if (mixinEntry != null) {
                // execute value of existing property
                PropertyState pState = mixinEntry.getPropertyState();
                setPropertyStateValue(pState, getQValues(mixinNames, qValueFactory), PropertyType.NAME, operation.getOptions());
            } else {
                // create new jcr:mixinTypes property
                ItemDefinitionProvider defProvider = mgrProvider.getItemDefinitionProvider();
                QPropertyDefinition pd = defProvider.getQPropertyDefinition(nState.getAllNodeTypeNames(), NameConstants.JCR_MIXINTYPES, PropertyType.NAME, true);
                QValue[] mixinValue = getQValues(mixinNames, qValueFactory);
                addPropertyState(nState, pd.getName(), pd.getRequiredType(), mixinValue, pd, operation.getOptions());
            }
            nState.markModified();
            transientStateMgr.addOperation(operation);
        } else if (mixinEntry != null) {
            // remove the jcr:mixinTypes property state if already present
            PropertyState pState = mixinEntry.getPropertyState();
            removeItemState(pState, operation.getOptions());

            nState.markModified();
            transientStateMgr.addOperation(operation);
        } // else: empty Name array and no mixin-prop-entry (should not occur)
    }

    /**
     * @see OperationVisitor#visit(SetPrimaryType)
     */
    public void visit(SetPrimaryType operation) throws ConstraintViolationException, RepositoryException {
        // NOTE: nodestate is only modified upon save of the changes!
        Name primaryName = operation.getPrimaryTypeName();
        NodeState nState = operation.getNodeState();
        NodeEntry nEntry = nState.getNodeEntry();

        // detect obvious node type conflicts

        EffectiveNodeTypeProvider entProvider = mgrProvider.getEffectiveNodeTypeProvider();

        // try to build new effective node type (will throw in case of conflicts)
        Name[] mixins = nState.getMixinTypeNames();
        List<Name> all = new ArrayList<Name>(Arrays.asList(mixins));
        all.add(primaryName);
        // retrieve effective to assert validity of arguments
        entProvider.getEffectiveNodeType(all.toArray(new Name[all.size()]));

        // modify the value of the jcr:primaryType property entry without
        // changing the node state itself
        PropertyEntry pEntry = nEntry.getPropertyEntry(NameConstants.JCR_PRIMARYTYPE);
        PropertyState pState = pEntry.getPropertyState();
        setPropertyStateValue(pState, getQValues(new Name[] {primaryName}, qValueFactory), PropertyType.NAME, operation.getOptions());

        // mark the affected node state modified and remember the operation
        nState.markModified();
        transientStateMgr.addOperation(operation);
    }

    /**
     * @see OperationVisitor#visit(SetPropertyValue)
     */
    public void visit(SetPropertyValue operation) throws ValueFormatException, LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException {
        PropertyState pState = operation.getPropertyState();
        setPropertyStateValue(pState, operation.getValues(), operation.getValueType(), operation.getOptions());
        transientStateMgr.addOperation(operation);
    }

    /**
     * @see OperationVisitor#visit(ReorderNodes)
     */
    public void visit(ReorderNodes operation) throws ConstraintViolationException, AccessDeniedException, UnsupportedRepositoryOperationException, VersionException, RepositoryException {
        NodeState parent = operation.getParentState();
        // modify the parent node state
        parent.reorderChildNodeEntries(operation.getInsertNode(), operation.getBeforeNode());
        // remember the operation
        transientStateMgr.addOperation(operation);
    }

    //--------------------------------------------< Internal State Handling >---
    /**
     *
     * @param parent
     * @param propertyName
     * @param propertyType
     * @param values
     * @param pDef
     * @param options int used to validate the given params. Note, that the options
     * differ depending if the 'addProperty' is called regularly or to create
     * auto-created (or protected) properties.
     * @throws LockException
     * @throws ConstraintViolationException
     * @throws AccessDeniedException
     * @throws ItemExistsException
     * @throws NoSuchNodeTypeException
     * @throws UnsupportedRepositoryOperationException
     * @throws VersionException
     * @throws RepositoryException
     */
    private PropertyState addPropertyState(NodeState parent, Name propertyName,
                                  int propertyType, QValue[] values,
                                  QPropertyDefinition pDef, int options)
            throws LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException, RepositoryException {

        validator.checkAddProperty(parent, propertyName, pDef, options);
        // create property state
        return transientStateMgr.createNewPropertyState(propertyName, parent, pDef, values, propertyType);
    }

    private List<ItemState> addNodeState(NodeState parent, Name nodeName, Name nodeTypeName,
                              String uuid, QNodeDefinition definition, int options)
            throws RepositoryException, ConstraintViolationException, AccessDeniedException,
            UnsupportedRepositoryOperationException, NoSuchNodeTypeException,
            ItemExistsException, VersionException {

        // check if add node is possible. note, that the options differ if
        // the 'addNode' is called from inside a regular add-node to create
        // autocreated child nodes that may be 'protected'.
        validator.checkAddNode(parent, nodeName, nodeTypeName, options);
        // a new NodeState doesn't have mixins defined yet -> ent is ent of primarytype
        EffectiveNodeType ent = mgrProvider.getEffectiveNodeTypeProvider().getEffectiveNodeType(nodeTypeName);

        if (nodeTypeName == null) {
            // no primary node type specified,
            // try default primary type from definition
            nodeTypeName = definition.getDefaultPrimaryType();
            if (nodeTypeName == null) {
                String msg = "No applicable node type could be determined for " + nodeName;
                log.debug(msg);
                throw new ConstraintViolationException(msg);
            }
        }

        List<ItemState> addedStates = new ArrayList<ItemState>();

        // create new nodeState. NOTE, that the uniqueID is not added to the
        // state for consistency between 'addNode' and importXML
        NodeState nodeState = transientStateMgr.createNewNodeState(nodeName, null, nodeTypeName, definition, parent);
        addedStates.add(nodeState);
        if (uuid != null) {
            QValue[] value = getQValues(uuid, qValueFactory);
            ItemDefinitionProvider defProvider = mgrProvider.getItemDefinitionProvider();
            QPropertyDefinition pDef = defProvider.getQPropertyDefinition(NameConstants.MIX_REFERENCEABLE, NameConstants.JCR_UUID, PropertyType.STRING, false);
            addedStates.add(addPropertyState(nodeState, NameConstants.JCR_UUID, PropertyType.STRING, value, pDef, 0));
        }

        // add 'auto-create' properties defined in node type
        for (QPropertyDefinition pd : ent.getAutoCreateQPropertyDefinitions()) {
            if (!nodeState.hasPropertyName(pd.getName())) {
                QValue[] autoValue = computeSystemGeneratedPropertyValues(nodeState, pd);
                if (autoValue != null) {
                    int propOptions = ItemStateValidator.CHECK_NONE;
                    // execute 'addProperty' without adding operation.
                    addedStates.add(addPropertyState(nodeState, pd.getName(), pd.getRequiredType(), autoValue, pd, propOptions));
                }
            }
        }

        // recursively add 'auto-create' child nodes defined in node type
        for (QNodeDefinition nd : ent.getAutoCreateQNodeDefinitions()) {
            // execute 'addNode' without adding the operation.
            int opt = ItemStateValidator.CHECK_LOCK | ItemStateValidator.CHECK_COLLISION;
            addedStates.addAll(addNodeState(nodeState, nd.getName(), nd.getDefaultPrimaryType(), null, nd, opt));
        }
        return addedStates;
    }

    private void removeItemState(ItemState itemState, int options) throws RepositoryException {
        validator.checkRemoveItem(itemState, options);
        // recursively remove the given state and all child states.
        boolean success = false;
        try {
            itemState.getHierarchyEntry().transientRemove();
            success = true;
        } finally {
            if (!success) {
                // TODO: TOBEFIXED undo state modifications
            }
        }
    }

    /**
     *
     * @param propState
     * @param iva
     * @param valueType
     * @throws ValueFormatException
     * @throws LockException
     * @throws ConstraintViolationException
     * @throws AccessDeniedException
     * @throws ItemExistsException
     * @throws UnsupportedRepositoryOperationException
     * @throws VersionException
     * @throws RepositoryException
     */
    private void setPropertyStateValue(PropertyState propState, QValue[] iva,
                                       int valueType, int options)
        throws ValueFormatException, LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException {
        // assert that the property can be modified.
        validator.checkSetProperty(propState, options);
        propState.setValues(iva, valueType);
    }

    /**
     * Computes the values of well-known system (i.e. protected) properties
     * as well as auto-created properties which define default value(s)
     *
     * @param parent
     * @param def
     * @return the computed values
     */
    private QValue[] computeSystemGeneratedPropertyValues(NodeState parent,
                                                          QPropertyDefinition def)
            throws RepositoryException {
        QValue[] genValues = null;
        QValue[] qDefaultValues = def.getDefaultValues();
        if (qDefaultValues != null && qDefaultValues.length > 0) {
            genValues = qDefaultValues;
        } else if (def.isAutoCreated()) {
            // handle known predefined nodetypes that declare auto-created
            // properties without default values
            Name declaringNT = def.getDeclaringNodeType();
            Name name = def.getName();

            if (NameConstants.JCR_PRIMARYTYPE.equals(name)) {
                // jcr:primaryType property
                genValues = new QValue[]{qValueFactory.create(parent.getNodeTypeName())};

            } else if (NameConstants.JCR_MIXINTYPES.equals(name)) {
                // jcr:mixinTypes property
                Name[] mixins = parent.getMixinTypeNames();
                genValues = getQValues(mixins, qValueFactory);

            } else if (NameConstants.JCR_CREATED.equals(name)
                    && (NameConstants.MIX_CREATED.equals(declaringNT) ||
                            NameConstants.NT_HIERARCHYNODE.equals(declaringNT))) {

                // jcr:created property of a mix:created or nt:hierarchyNode
                genValues = new QValue[]{qValueFactory.create(Calendar.getInstance())};

            } else if (NameConstants.JCR_CREATEDBY.equals(name)
                    && NameConstants.MIX_CREATED.equals(declaringNT)) {
                // jcr:createdBy property of a mix:created
                genValues = new QValue[]{qValueFactory.create(mgrProvider.getUserID(), PropertyType.STRING)};

            } else if (NameConstants.JCR_LASTMODIFIED.equals(name)
                    && NameConstants.MIX_LASTMODIFIED.equals(declaringNT)) {
                // jcr:lastModified property of a mix:lastModified
                genValues = new QValue[]{qValueFactory.create(Calendar.getInstance())};

            } else if (NameConstants.JCR_LASTMODIFIEDBY.equals(name)
                    && NameConstants.MIX_LASTMODIFIED.equals(declaringNT)) {
                // jcr:lastModifiedBy property of a mix:lastModified
                genValues = new QValue[]{qValueFactory.create(mgrProvider.getUserID(), PropertyType.STRING)};

            } else {
                // ask the SPI implementation for advice
                genValues = qValueFactory.computeAutoValues(def);
            }
        }
        return genValues;
    }

    /**
     * @param qNames
     * @param factory
     * @return An array of QValue objects from the given <code>Name</code>s
     */
    private static QValue[] getQValues(Name[] qNames, QValueFactory factory) throws RepositoryException {
        QValue[] ret = new QValue[qNames.length];
        for (int i = 0; i < qNames.length; i++) {
            ret[i] = factory.create(qNames[i]);
        }
        return ret;
    }

    private static QValue[] getQValues(String uniqueID, QValueFactory factory) throws RepositoryException {
        return new QValue[] {factory.create(uniqueID, PropertyType.STRING)};
    }
}
