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

import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.jcr.PropertyType;

import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.id.PropertyId;
import org.apache.jackrabbit.core.persistence.PersistenceManager;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.core.state.PropertyState;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This Class provides a simple structure to hold the nodestate and related
 * propertystate data.
 */
public class NodePropBundle {

    /**
     * default logger
     */
    private static Logger log = LoggerFactory.getLogger(NodePropBundle.class);

    /**
     * the node id
     */
    private final NodeId id;

    /**
     * the parent node id
     */
    private NodeId parentId;

    /**
     * the nodetype name
     */
    private Name nodeTypeName;

    /**
     * the mixintype names
     */
    private Set<Name> mixinTypeNames;

    /**
     * the child node entries
     */
    private LinkedList<NodePropBundle.ChildNodeEntry> childNodeEntries = new LinkedList<NodePropBundle.ChildNodeEntry>();

    /**
     * the properties
     */
    private HashMap<Name, PropertyEntry> properties = new HashMap<Name, PropertyEntry>();

    /**
     * flag that indicates if this bundle is new
     */
    private boolean isNew = true;

    /**
     * flag that indicates if this bundle is referenceable
     */
    private boolean isReferenceable;

    /**
     * the mod count
     */
    private short modCount;

    /**
     * the size
     */
    private long size;
    
    /**
     * Shared set, consisting of the parent ids of this shareable node. This
     * entry is <code>null</code> if this node is not shareable.
     */
    private Set<NodeId> sharedSet;

    /**
     * Creates a "new" bundle with the given id
     *
     * @param id the node id
     */
    public NodePropBundle(NodeId id) {
        this.id = id;
    }

    /**
     * Creates a bundle from the given state
     *
     * @param state the node state
     */
    public NodePropBundle(NodeState state) {
        this(state.getNodeId());
        update(state);
    }

    /**
     * Updates this bundle with values from the given state.
     * @param state the node state
     */
    public void update(NodeState state) {
        if (!id.equals(state.getNodeId())) {
            // sanity check
            throw new IllegalArgumentException("Not allowed to update foreign state.");
        }
        parentId = state.getParentId();
        nodeTypeName = state.getNodeTypeName();
        mixinTypeNames = state.getMixinTypeNames();
        isReferenceable = state.hasPropertyName(NameConstants.JCR_UUID);
        modCount = state.getModCount();
        List<org.apache.jackrabbit.core.state.ChildNodeEntry> list = state.getChildNodeEntries();
        childNodeEntries.clear();
        for (org.apache.jackrabbit.core.state.ChildNodeEntry cne : list) {
            addChildNodeEntry(cne.getName(), cne.getId());
        }
        sharedSet = state.getSharedSet();
    }

    /**
     * Creates a node state from the values of this bundle
     * @param pMgr the persistence manager
     * @return the new nodestate
     */
    public NodeState createNodeState(PersistenceManager pMgr) {
        NodeState state = pMgr.createNew(id);
        state.setParentId(parentId);
        state.setNodeTypeName(nodeTypeName);
        state.setMixinTypeNames(mixinTypeNames);
        state.setModCount(modCount);
        for (ChildNodeEntry e : childNodeEntries) {
            state.addChildNodeEntry(e.getName(), e.getId());
        }
        state.setPropertyNames(properties.keySet());

        // add fake property entries
        state.addPropertyName(NameConstants.JCR_PRIMARYTYPE);
        if (mixinTypeNames.size() > 0) {
            state.addPropertyName(NameConstants.JCR_MIXINTYPES);
        }
        // uuid is special...only if 'referenceable'
        if (isReferenceable) {
            state.addPropertyName(NameConstants.JCR_UUID);
        }
        for (NodeId nodeId : sharedSet) {
            state.addShare(nodeId);
        }
        return state;
    }

    /**
     * Checks if this bundle is new.
     * @return <code>true</code> if this bundle is new;
     *         <code>false</code> otherwise.
     */
    public boolean isNew() {
        return isNew;
    }

    /**
     * Marks this bundle as 'not new'.
     */
    public void markOld() {
        isNew = false;
    }

    /**
     * Returns the node id of this bundle
     * @return the node id of this bundle
     */
    public NodeId getId() {
        return id;
    }

    /**
     * Returns the parent id of this bundle
     * @return the parent id of this bundle
     */
    public NodeId getParentId() {
        return parentId;
    }

    /**
     * Sets the parent id
     * @param parentId the parent id
     */
    public void setParentId(NodeId parentId) {
        this.parentId = parentId;
    }

    /**
     * Returns the nodetype name of this bundle
     * @return the nodetype name of this bundle
     */
    public Name getNodeTypeName() {
        return nodeTypeName;
    }

    /**
     * Sets the node type name
     * @param nodeTypeName the nodetype name
     */
    public void setNodeTypeName(Name nodeTypeName) {
        this.nodeTypeName = nodeTypeName;
    }

    /**
     * Returns the mixin type names of this bundle.
     * @return the mixin type names of this bundle.
     */
    public Set<Name> getMixinTypeNames() {
        return mixinTypeNames;
    }

    /**
     * Sets the mixin type names
     * @param mixinTypeNames the mixin type names
     */
    public void setMixinTypeNames(Set<Name> mixinTypeNames) {
        this.mixinTypeNames = mixinTypeNames;
    }

    /**
     * Checks if this bundle is referenceable.
     * @return <code>true</code> if this bundle is referenceable;
     *         <code>false</code> otherwise.
     */
    public boolean isReferenceable() {
        return isReferenceable;
    }

    /**
     * Sets the is referenceable flag on this bundle
     * @param referenceable the ref. flag
     */
    public void setReferenceable(boolean referenceable) {
        isReferenceable = referenceable;
    }

    /**
     * Returns the mod count.
     *
     * @return the mod count.
     */
    public short getModCount() {
        return modCount;
    }

    /**
     * Sets the mod count
     * 
     * @param modCount the mod count
     */
    public void setModCount(short modCount) {
        this.modCount = modCount;
    }

    /**
     * Returns the list of the child node entries.
     * @return the list of the child node entries.
     */
    public List<NodePropBundle.ChildNodeEntry> getChildNodeEntries() {
        return childNodeEntries;
    }

    /**
     * Adds a child node entry.
     * @param name the name of the entry.
     * @param id the id of the entry
     */
    public void addChildNodeEntry(Name name, NodeId id) {
        childNodeEntries.add(new ChildNodeEntry(name, id));
    }

    /**
     * Adds a new property entry
     * @param entry the enrty to add
     */
    public void addProperty(PropertyEntry entry) {
        assert !NameConstants.JCR_PRIMARYTYPE.equals(entry.getName());
        assert !NameConstants.JCR_MIXINTYPES.equals(entry.getName());
        assert !NameConstants.JCR_UUID.equals(entry.getName());
        properties.put(entry.getName(), entry);
    }

    /**
     * Creates a property entry from the given state and adds it.
     *
     * @param state the property state
     * @param blobStore BLOB store from where to delete previous property value
     */
    public void addProperty(PropertyState state, BLOBStore blobStore) {
        PropertyEntry old =
            properties.put(state.getName(), new PropertyEntry(state));
        if (old != null) {
            old.destroy(blobStore);
        }
    }

    /**
     * Checks if this bundle has a property
     * @param name the name of the property
     * @return <code>true</code> if the property exists;
     *         <code>false</code> otherwise.
     */
    public boolean hasProperty(Name name) {
        return properties.containsKey(name);
    }

    /**
     * Returns a set of the property names.
     * @return a set of the property names.
     */
    public Set<Name> getPropertyNames() {
        return properties.keySet();
    }

    /**
     * Returns a collection of property entries.
     * @return a collection of property entries.
     */
    public Collection<PropertyEntry> getPropertyEntries() {
        return properties.values();
    }

    /**
     * Returns the property entry with the given name.
     * @param name the name of the property entry
     * @return the desired property entry or <code>null</code>
     */
    public PropertyEntry getPropertyEntry(Name name) {
        return properties.get(name);
    }

    /**
     * Removes all property entries
     *
     * @param blobStore BLOB store from where to delete property values
     */
    public void removeAllProperties(BLOBStore blobStore) {
        for (Name name : new HashSet<Name>(properties.keySet())) {
            removeProperty(name, blobStore);
        }
    }

    /**
     * Removes the proprty with the given name from this bundle.
     *
     * @param name the name of the property
     * @param blobStore BLOB store from where to delete the property value
     */
    public void removeProperty(Name name, BLOBStore blobStore) {
        PropertyEntry pe = properties.remove(name);
        if (pe != null) {
            pe.destroy(blobStore);
        }
    }
    
    /**
     * Sets the shared set of this bundle.
     * @return the shared set of this bundle.
     */
    public Set<NodeId> getSharedSet() {
        return sharedSet;
    }

    /**
     * Sets the shared set.
     * @param sharedSet shared set
     */
    public void setSharedSet(Set<NodeId> sharedSet) {
        this.sharedSet = sharedSet;
    }

    /**
     * Returns the approx. size of this bundle.
     * @return the approx. size of this bundle.
     */
    public long getSize() {
        // add some internal memory
        //  + shallow size: 64
        //  + properties
        //    + shallow size: 40
        //    + N * property entry: 218 + values + blobids
        //  + childnodes
        //    + shallow size: 24
        //    + N * 24 + 160 + 44 + name.length
        //  + mixintypes names
        //    + shallow size: 16
        //    + N * QNames
        //  + nodetype name:
        //    + shallow size: 24
        //      + string: 20 + length
        //  + parentId: 160
        //  + id: 160
        return 500 + size + 300 * (childNodeEntries.size() + properties.size() + 3);
    }

    /**
     * Sets the data size of this bundle
     * @param size the data size
     */
    public void setSize(long size) {
        this.size = size;
    }

    //--------------------------------------------------------------< Object >

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append(id);
        builder.append("(");
        builder.append(parentId);
        builder.append(",");
        builder.append(nodeTypeName);
        for (Name mixin : mixinTypeNames) {
            builder.append(",");
            builder.append(mixin);
        }
        if (isReferenceable) {
            builder.append(",referenceable");
        }
        builder.append(") = ");
        if (!sharedSet.isEmpty()) {
            builder.append(sharedSet);
            builder.append(" ");
        }
        builder.append(properties.values());
        builder.append(" ");
        builder.append(childNodeEntries);
        return builder.toString();
    }

    @Override
    public boolean equals(Object object) {
        if (object instanceof NodePropBundle) {
            NodePropBundle that = (NodePropBundle) object;
            return equalNullSafe(id, that.id)
                && equalNullSafe(parentId, that.parentId)
                && equalNullSafe(nodeTypeName, that.nodeTypeName)
                && equalNullSafe(mixinTypeNames, that.mixinTypeNames)
                && isReferenceable == that.isReferenceable
                && equalNullSafe(sharedSet, that.sharedSet)
                && equalNullSafe(properties, that.properties)
                && equalNullSafe(childNodeEntries, that.childNodeEntries);
        }
        return false;
    }
    
    private static boolean equalNullSafe(Object a, Object b) {
        if (a == null || b == null) {
            return a == b;
        }
        return a.equals(b);
    }

    //-----------------------------------------------------< ChildNodeEntry >---

    /**
     * Helper class for a child node entry
     */
    public static class ChildNodeEntry {

        /**
         * the name of the entry
         */
        private final Name name;

        /**
         * the id of the entry
         */
        private final NodeId id;

        /**
         * Creates a new entry with the given name and id
         * @param name the name
         * @param id the id
         */
        public ChildNodeEntry(Name name, NodeId id) {
            this.name = name;
            this.id = id;
        }

        /**
         * Returns the name.
         * @return the name.
         */
        public Name getName() {
            return name;
        }

        /**
         * Returns the id.
         * @return the id.
         */
        public NodeId getId() {
            return id;
        }

        //----------------------------------------------------------< Object >

        public String toString() {
            return name + " => " + id;
        }

        public boolean equals(Object object) {
            if (object instanceof ChildNodeEntry) {
                ChildNodeEntry that = (ChildNodeEntry) object;
                return name.equals(that.name) && id.equals(that.id);
            } else {
                return false;
            }
        }

    }

    //------------------------------------------------------< PropertyEntry >---

    /**
     * Helper class for a property enrty
     */
    public static class PropertyEntry {

        /**
         * The property id
         */
        private final PropertyId id;

        /**
         * the internal value
         */
        private InternalValue[] values;

        /**
         * the property type
         */
        private int type;

        /**
         * the multivalued flag
         */
        private boolean multiValued;

        /**
         * the blob ids
         */
        private String[] blobIds;

        /**
         * the mod count
         */
        private short modCount;

        /**
         * Creates a new property entry with the given id.
         * @param id the id
         */
        public PropertyEntry(PropertyId id) {
            this.id = id;
        }

        /**
         * Creates a new property entry and initialized it with values from
         * the given property state.
         * @param state the source property state.
         */
        public PropertyEntry(PropertyState state) {
            this((PropertyId) state.getId());
            values = state.getValues();
            type = state.getType();
            multiValued = state.isMultiValued();
            if (!multiValued && values.length != 1) {
                throw new IllegalArgumentException("Non-multi-valued property with values.length " + values.length);
            }
            modCount = state.getModCount();
            if (type == PropertyType.BINARY) {
                blobIds = new String[values.length];
            }
        }

        /**
         * Returns the property id.
         * @return the property id.
         */
        public PropertyId getId() {
            return id;
        }

        /**
         * Returns the property name
         * @return the property name
         */
        public Name getName() {
            return id.getName();
        }

        /**
         * Retruns the internal values
         * @return the internal values
         */
        public InternalValue[] getValues() {
            return values;
        }

        /**
         * Sets the internal values.
         * @param values the internal values.
         */
        public void setValues(InternalValue[] values) {
            this.values = values;
        }

        /**
         * Returns the type.
         * @return the type.
         */
        public int getType() {
            return type;
        }

        /**
         * Sets the type
         * @param type the type
         */
        public void setType(int type) {
            this.type = type;
        }

        /**
         * Returns the multivalued flag.
         * @return the multivalued flag.
         */
        public boolean isMultiValued() {
            return multiValued;
        }

        /**
         * Sets the multivalued flag.
         * @param multiValued the multivalued flag
         */
        public void setMultiValued(boolean multiValued) {
            this.multiValued = multiValued;
        }

        /**
         * Returns the n<sup>th</sup> blob id.
         * @param n the index of the blob id
         * @return the blob id
         */
        public String getBlobId(int n) {
            return blobIds[n];
        }

        /**
         * Sets the blob ids
         * @param blobIds the blobids
         */
        public void setBlobIds(String[] blobIds) {
            this.blobIds = blobIds;
        }

        /**
         * Sets the n<sup>th</sup> blobid
         * @param blobId the blob id
         * @param n the index of the blob id
         */
        public void setBlobId(String blobId, int n) {
            blobIds[n] = blobId;
        }

        /**
         * Returns the mod count.
         * @return the mod count.
         */
        public short getModCount() {
            return modCount;
        }

        /**
         * Sets the mod count
         * @param modCount the mod count
         */
        public void setModCount(short modCount) {
            this.modCount = modCount;
        }

        /**
         * Destroys this property state and deletes temporary blob file values.
         * @param blobStore the blobstore that will destroy the blobs
         */
        private void destroy(BLOBStore blobStore) {
            // delete blobs if needed
            for (int i = 0; blobIds != null && i < blobIds.length; i++) {
                if (blobIds[i] != null) {
                    try {
                        blobStore.remove(blobIds[i]);
                        log.debug("removed blob {}", blobIds[i]);
                    } catch (Exception e) {
                        log.error("Ignoring error while removing blob " + blobIds[i], e);
                    }
                }
            }
        }

        //----------------------------------------------------------< Object >

        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append(id);
            builder.append("(");
            builder.append(PropertyType.nameFromValue(type));
            if (multiValued) {
                builder.append(",multiple");
            }
            builder.append(") = ");
            builder.append(Arrays.toString(values));
            return builder.toString();
        }

        public boolean equals(Object object) {
            if (object instanceof PropertyEntry) {
                PropertyEntry that = (PropertyEntry) object;
                return id.equals(that.id)
                    && type == that.type
                    && multiValued == that.multiValued
                    && Arrays.equals(values, that.values);
            } else {
                return false;
            }
        }

    }

}
