/*
 * ModeShape (http://www.modeshape.org)
 *
 * Licensed 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.modeshape.jcr;

import java.math.BigDecimal;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;
import javax.jcr.AccessDeniedException;
import javax.jcr.InvalidItemStateException;
import javax.jcr.Item;
import javax.jcr.ItemNotFoundException;
import javax.jcr.ItemVisitor;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.ValueFormatException;
import javax.jcr.lock.Lock;
import javax.jcr.lock.LockException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.version.OnParentVersionAction;
import javax.jcr.version.VersionException;
import org.modeshape.common.annotation.Immutable;
import org.modeshape.common.annotation.NotThreadSafe;
import org.modeshape.common.util.CheckArg;
import org.modeshape.jcr.api.value.DateTime;
import org.modeshape.jcr.cache.CachedNode;
import org.modeshape.jcr.cache.MutableCachedNode;
import org.modeshape.jcr.cache.NodeKey;
import org.modeshape.jcr.cache.SessionCache;
import org.modeshape.jcr.value.BinaryValue;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.Path;
import org.modeshape.jcr.value.PropertyFactory;
import org.modeshape.jcr.value.Reference;
import org.modeshape.jcr.value.ValueFactories;
import org.modeshape.jcr.value.ValueFactory;
import org.modeshape.jcr.value.basic.NodeKeyReference;

/**
 * An abstract {@link Property JCR Property} implementation.
 */
@NotThreadSafe
abstract class AbstractJcrProperty extends AbstractJcrItem implements org.modeshape.jcr.api.Property, Comparable<org.modeshape.jcr.api.Property> {

    @Immutable
    private final static class CachedDefinition {
        protected final PropertyDefinitionId propDefnId;
        protected final int nodeTypesVersion;

        protected CachedDefinition( PropertyDefinitionId propDefnId,
                                    int nodeTypesVersion ) {
            this.propDefnId = propDefnId;
            this.nodeTypesVersion = nodeTypesVersion;
        }
    }

    private final AbstractJcrNode node;
    private final Name name;
    private int propertyType;
    private volatile CachedDefinition cachedDefn;

    AbstractJcrProperty( AbstractJcrNode node,
                         Name name,
                         int propertyType ) {
        super(node.session());
        assert node != null;
        assert name != null;
        this.node = node;
        this.name = name;
        this.propertyType = propertyType;
    }

    final void setPropertyDefinitionId( PropertyDefinitionId propDefnId,
                                        int nodeTypesVersion ) {
        this.cachedDefn = new CachedDefinition(propDefnId, nodeTypesVersion);
    }

    final void releasePropertyDefinitionId() {
        this.cachedDefn = null;
    }

    /**
     * Get the property definition ID.
     * 
     * @return the cached property definition ID; never null
     * @throws ItemNotFoundException if the node that contains this property doesn't exist anymore
     * @throws ConstraintViolationException if no valid property definition could be found
     * @throws InvalidItemStateException if the node has been removed in this session's transient state
     */
    final PropertyDefinitionId propertyDefinitionId()
        throws ItemNotFoundException, ConstraintViolationException, InvalidItemStateException {
        CachedDefinition defn = cachedDefn;
        NodeTypes nodeTypes = session.nodeTypes();
        if (defn == null || nodeTypes.getVersion() > defn.nodeTypesVersion) {
            Name primaryType = node.getPrimaryTypeName();
            Set<Name> mixinTypes = node.getMixinTypeNames();
            PropertyDefinitionId id = node.propertyDefinitionFor(property(), primaryType, mixinTypes, nodeTypes).getId();
            setPropertyDefinitionId(id, nodeTypes.getVersion());
            return id;
        }
        return defn.propDefnId;
    }

    /**
     * Get the definition for this property.
     * 
     * @return the cached property definition ID; never null
     * @throws ItemNotFoundException if the node that contains this property doesn't exist anymore
     * @throws ConstraintViolationException if no valid property definition could be found
     * @throws InvalidItemStateException if the node has been removed in this session's transient state
     */
    final JcrPropertyDefinition propertyDefinition()
        throws ItemNotFoundException, ConstraintViolationException, InvalidItemStateException {
        CachedDefinition defn = cachedDefn;
        NodeTypes nodeTypes = session.nodeTypes();
        if (defn == null || nodeTypes.getVersion() > defn.nodeTypesVersion) {
            Name primaryType = node.getPrimaryTypeName();
            Set<Name> mixinTypes = node.getMixinTypeNames();
            JcrPropertyDefinition propDefn = node.propertyDefinitionFor(property(), primaryType, mixinTypes, nodeTypes);
            PropertyDefinitionId id = propDefn.getId();
            setPropertyDefinitionId(id, nodeTypes.getVersion());
            return propDefn;
        }
        return nodeTypes.getPropertyDefinition(defn.propDefnId);
    }

    final CachedNode cachedNode() throws ItemNotFoundException, InvalidItemStateException {
        return node.node();
    }

    final MutableCachedNode mutable() {
        return node.mutable();
    }

    final SessionCache sessionCache() {
        return node.sessionCache();
    }

    final PropertyFactory propertyFactory() {
        return node.session().propertyFactory();
    }

    final org.modeshape.jcr.value.Property property() throws ItemNotFoundException, InvalidItemStateException {
        return cachedNode().getProperty(name, sessionCache());
    }

    final JcrValue createValue( Object value ) {
        return new JcrValue(session().context().getValueFactories(), this.propertyType, value);
    }

    final JcrValue createValue( Object value,
                                int propertyType ) throws ValueFormatException {
        try {
            return new JcrValue(session().context().getValueFactories(), propertyType, value);
        } catch (org.modeshape.jcr.value.ValueFormatException e) {
            throw new ValueFormatException(e);
        }
    }

    @Override
    public JcrSession getSession() {
        return node.getSession();
    }

    /**
     * Checks that this property's parent node is not already locked by another session. If the parent node is not locked or the
     * parent node is locked but the lock is owned by this {@code Session}, this method completes silently. If the parent node is
     * locked (either directly or as part of a deep lock from an ancestor), this method throws a {@code LockException}.
     * 
     * @throws LockException if the parent node of this property is locked (that is, if {@code getParent().isLocked() == true &&
     *         getParent().getLock().getLockToken() == null}.
     * @throws RepositoryException if any other error occurs
     * @see Node#isLocked()
     * @see Lock#getLockToken()
     */
    protected final void checkForLock() throws LockException, RepositoryException {

        if (getParent().isLockedByAnotherSession()) {
            Lock parentLock = this.getParent().getLock();
            if (parentLock != null && parentLock.getLockToken() == null) {
                throw new LockException(JcrI18n.lockTokenNotHeld.text(node.location()));
            }
        }
    }

    /**
     * Verifies that this node is either not versionable or that it is versionable but checked out.
     * 
     * @throws VersionException if the node is versionable but is checked in and cannot be modified
     * @throws RepositoryException if there is an error accessing the repository
     */
    protected final void checkForCheckedOut() throws VersionException, RepositoryException {
        if (!node.isCheckedOut()) {
            // Node is not checked out, so changing property is only allowed if OPV of property is 'ignore' ...
            JcrPropertyDefinition defn = getDefinition();
            if (defn.getOnParentVersion() != OnParentVersionAction.IGNORE) {
                // Can't change this property ...
                String path = getParent().getPath();
                throw new VersionException(JcrI18n.nodeIsCheckedIn.text(path));
            }
        }
    }
    
    protected final void checkModifyPermission() throws AccessDeniedException {
        session.checkPermission(node, ModeShapePermissions.SET_PROPERTY);
    }

    @Override
    public final void accept( ItemVisitor visitor ) throws RepositoryException {
        CheckArg.isNotNull(visitor, "visitor");
        checkSession();
        visitor.visit(this);
    }

    final Name name() {
        return name;
    }

    @Override
    Path path() throws RepositoryException {
        return session().pathFactory().create(node.path(), name);
    }

    @Override
    public int getType() throws RepositoryException {
        checkSession();
        return propertyType;
    }

    @Override
    public final JcrPropertyDefinition getDefinition() throws RepositoryException {
        checkSession();
        return propertyDefinition();
    }

    @Override
    public final String getName() {
        return name.getString(namespaces());
    }

    @Override
    public final AbstractJcrNode getParent() {
        return node;
    }

    @Override
    public final String getPath() throws RepositoryException {
        return path().getString(namespaces());
    }

    @Override
    public String getLocalName() {
        return name.getLocalName();
    }

    @Override
    public String getNamespaceURI() {
        return name.getNamespaceUri();
    }

    @Override
    public final boolean isModified() {
        try {
            checkSession();
            CachedNode node = cachedNode();
            return node instanceof MutableCachedNode && ((MutableCachedNode)node).isPropertyModified(sessionCache(), name);
        } catch (RepositoryException re) {
            throw new IllegalStateException(re);
        }
    }

    @Override
    public final boolean isNew() {
        try {
            checkSession();
            CachedNode node = cachedNode();
            return node instanceof MutableCachedNode && ((MutableCachedNode)node).isPropertyNew(sessionCache(), name);
        } catch (RepositoryException re) {
            throw new IllegalStateException(re);
        }
    }

    @Override
    public final boolean isNode() {
        return false;
    }

    @Override
    public final boolean isSame( Item otherItem ) throws RepositoryException {
        checkSession();
        if (otherItem instanceof Property) {
            Property otherProperty = (Property)otherItem;
            // The nodes that own the properties must be the same ...
            if (!getParent().isSame(otherProperty.getParent())) return false;
            // The properties must have the same name ...
            return getName().equals(otherProperty.getName());
        }
        return false;
    }

    @Override
    public void refresh( boolean keepChanges ) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void remove() throws VersionException, LockException, ConstraintViolationException, RepositoryException {
        checkSession();
        checkNotProtected();
        checkForLock();
        checkForCheckedOut();
        session.checkPermission(this, ModeShapePermissions.REMOVE);
        AbstractJcrNode parentNode = getParent();
        if (parentNode.isLockedByAnotherSession()) {
            Lock parentLock = parentNode.getLock();
            if (parentLock != null && !parentLock.isLockOwningSession()) {
                throw new LockException(JcrI18n.lockTokenNotHeld.text(getPath()));
            }
        }

        if (!parentNode.isCheckedOut()) {
            throw new VersionException(JcrI18n.nodeIsCheckedIn.text(getPath()));
        }

        node.removeProperty(this);
    }

    private void checkNotProtected() throws RepositoryException {
        if (this.getDefinition().isProtected()) {
            throw new ConstraintViolationException(JcrI18n.propertyIsProtected.text(getPath()));
        }
    }

    @Override
    public abstract JcrValue[] getValues() throws ValueFormatException, RepositoryException;

    @Override
    public abstract JcrValue getValue() throws ValueFormatException, RepositoryException;

    @SuppressWarnings( "deprecation" )
    @Override
    public void save() throws RepositoryException {
        checkSession();
        // This is not a correct implementation, but it's good enough to work around some TCK requirements for version tests
        // Plus, Item.save() has been removed from the JCR 2.0 spec (and deprecated in JCR 2.0's Java API).
        getParent().save();
    }

    @Override
    public int compareTo( org.modeshape.jcr.api.Property that ) {
        if (that == this) return 0;
        try {
            return this.getName().compareTo(that.getName());
        } catch (RepositoryException e) {
            throw new RuntimeException(e);
        }
    }

    protected Node valueToNode( Object value ) throws RepositoryException {
        ValueFactories factories = context().getValueFactories();
        try {
            if (value instanceof Reference) {
                NodeKey key = null;
                if (value instanceof NodeKeyReference) {
                    // REFERENCE and WEAKREFERENCE values are node keys ...
                    key = ((NodeKeyReference)value).getNodeKey();
                } else {
                    throw new IllegalArgumentException("Unknown reference type: " + value.getClass().getSimpleName());
                }
                return session().node(key, null);
            } else if (value instanceof String ) {
                String valueString = (String) value;
                // see MODE-2609 - some reference properties may have been incorrectly stored as strings, so try to resolve them
                if (NodeKey.isValidFormat(valueString)) {
                    return session().node(new NodeKey(valueString), null);
                }                 
            }
            // STRING, PATH and NAME values will be convertable to a Path object ...
            Path path = factories.getPathFactory().create(value);
            return path.isAbsolute() ? session().node(path) : session().node(getParent().node(), path);
        } catch (org.modeshape.jcr.value.ValueFormatException e) {
            throw new ValueFormatException(e.getMessage(), e);
        }
        catch (PathNotFoundException pathNotFound) {
            //expected by the TCK
            throw new ItemNotFoundException(pathNotFound.getMessage(), pathNotFound);
        }
    }

    @Override
    public String toString() {
        ValueFactory<String> stringFactory = session().context().getValueFactories().getStringFactory();
        StringBuilder sb = new StringBuilder();
        try {
            org.modeshape.jcr.value.Property property = cachedNode().getProperty(name, sessionCache());
            sb.append(getName()).append('=');
            if (isMultiple()) {
                sb.append('[');
                Iterator<?> iter = property.iterator();
                while (iter.hasNext()) {
                    Object value = iter.next();
                    appendValueToString(stringFactory, sb, value);
                    if (iter.hasNext()) sb.append(',');
                }
                sb.append(']');
            } else {
                Object value = property.getFirstValue();
                appendValueToString(stringFactory, sb, value);
            }
        } catch (RepositoryException e) {
            // The node likely does not exist ...
            sb.append(" on deleted node ").append(node.key());
        }
        return sb.toString();
    }

    private void appendValueToString( ValueFactory<String> stringFactory,
                                      StringBuilder sb,
                                      Object value ) {
        if (value instanceof javax.jcr.Binary) {
            sb.append("**binary-value-not-shown**");
        } else {
            sb.append(stringFactory.create(value));
        }
    }

    @Override
    public <T> T getAs( Class<T> type,
                        int index ) throws IndexOutOfBoundsException, ValueFormatException, RepositoryException {
        checkSession();
        Object value = property().getValue(index);
        Object convertedValue = null;
        try {
            if (String.class.equals(type)) {
                convertedValue = context().getValueFactories().getStringFactory().create(value);
            } else if (Long.class.equals(type)) {
                convertedValue = context().getValueFactories().getLongFactory().create(value);
            } else if (Boolean.class.equals(type)) {
                convertedValue = context().getValueFactories().getBooleanFactory().create(value);
            } else if (Date.class.equals(type)) {
                Calendar calendar = context().getValueFactories().getDateFactory().create(value).toCalendar();
                convertedValue = calendar.getTime();
            } else if (Calendar.class.equals(type)) {
                convertedValue = context().getValueFactories().getDateFactory().create(value).toCalendar();
            } else if (DateTime.class.equals(type)) {
                convertedValue = context().getValueFactories().getDateFactory().create(value);
            } else if (Double.class.equals(type)) {
                convertedValue = context().getValueFactories().getDoubleFactory().create(value);
            } else if (BigDecimal.class.equals(type)) {
                convertedValue = context().getValueFactories().getDecimalFactory().create(value);
            } else if (java.io.InputStream.class.equals(type)) {
                BinaryValue binary = context().getValueFactories().getBinaryFactory().create(value);
                convertedValue = binary.getStream();
            } else if (javax.jcr.Binary.class.isAssignableFrom(type)) {
                convertedValue = context().getValueFactories().getBinaryFactory().create(value);
            } else if (Node.class.equals(type)) {
                convertedValue = valueToNode(value);
            } else if (NodeIterator.class.equals(type)) {
                convertedValue = new JcrSingleNodeIterator((AbstractJcrNode)getNode());
            } else {
                throw new ValueFormatException(JcrI18n.unableToConvertPropertyValueAtIndexToType.text(getPath(), index, type));
            }
        } catch (org.modeshape.jcr.value.ValueFormatException e) {
            throw new ValueFormatException(e);
        }
        return type.cast(convertedValue);
    }
}
