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

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;

import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;

import org.apache.jackrabbit.commons.json.JsonHandler;
import org.apache.jackrabbit.spi.ChildInfo;
import org.apache.jackrabbit.spi.IdFactory;
import org.apache.jackrabbit.spi.ItemInfo;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.NodeId;
import org.apache.jackrabbit.spi.NodeInfo;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.PathFactory;
import org.apache.jackrabbit.spi.PropertyId;
import org.apache.jackrabbit.spi.QValue;
import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.apache.jackrabbit.spi.commons.util.StringCache;
import org.apache.jackrabbit.util.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <code>ItemInfoJSONHandler</code>...
 */
class ItemInfoJsonHandler implements JsonHandler {

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

    private static final String LEAF_NODE_HINT = "::NodeIteratorSize";

    private final List<ItemInfo> itemInfos;
    private final NamePathResolver resolver;
    private final String rootURI;

    private final QValueFactoryImpl vFactory;
    private final PathFactory pFactory;
    private final IdFactory idFactory;

    private boolean expectingHintValue = false;

    private Name name;
    private int index = Path.INDEX_DEFAULT;

    // temp. property state
    private int propertyType;
    private boolean multiValuedProperty = false;
    private List<QValue> propValues = new ArrayList<QValue>();

    private Stack<NodeInfo> nodeInfos = new Stack<NodeInfo>();

    private Stack<List<PropertyInfoImpl>> propInfoLists = new Stack<List<PropertyInfoImpl>>();

    ItemInfoJsonHandler(NamePathResolver resolver, NodeInfo nInfo,
                        String rootURI,
                        QValueFactoryImpl vFactory,
                        PathFactory pFactory,
                        IdFactory idFactory) {
        this.resolver = resolver;
        this.rootURI = rootURI;

        this.vFactory = vFactory;
        this.pFactory = pFactory;
        this.idFactory = idFactory;

        itemInfos = new ArrayList<ItemInfo>();
        itemInfos.add(nInfo);
        nodeInfos.push(nInfo);
        propInfoLists.push(new ArrayList<PropertyInfoImpl>(8));
    }

    public void object() throws IOException {
        if (name != null) {
            try {
                NodeInfo current = getCurrentNodeInfo();
                Path relPath = pFactory.create(name, index);
                NodeId id = idFactory.createNodeId(current.getId(), relPath);
                Path currentPath = current.getPath();
                Path p = pFactory.create(currentPath, relPath, true);
                NodeInfo nInfo = new NodeInfoImpl(id, p);
                nodeInfos.push(nInfo);
                propInfoLists.push(new ArrayList<PropertyInfoImpl>(8));
            } catch (RepositoryException e) {
                throw new IOException(e.getMessage());
            }
        }
    }

    public void endObject() throws IOException {
        try {
            NodeInfoImpl nInfo = (NodeInfoImpl) nodeInfos.pop();
            List<PropertyInfoImpl> props = propInfoLists.pop();
            // all required information to create a node info should now be gathered
            nInfo.setPropertyInfos(props.toArray(new PropertyInfoImpl[props.size()]), idFactory);
            NodeInfo parent = getCurrentNodeInfo();
            if (parent != null) {
                if (nInfo.getPath().getAncestor(1).equals(parent.getPath())) {
                    ChildInfo ci = new ChildInfoImpl(nInfo.getName(), nInfo.getUniqueID(), nInfo.getIndex());
                    ((NodeInfoImpl) parent).addChildInfo(ci);
                } else {
                    log.debug("NodeInfo '"+ nInfo.getPath() + "' out of hierarchy. Parent path = " + parent.getPath());
                }
            }
            if (nInfo.isCompleted()) {
                itemInfos.addAll(props);
                itemInfos.add(nInfo);
            } else {
                log.debug("Incomplete NodeInfo '"+ nInfo.getPath() + "' -> Only present as ChildInfo with its parent.");
            }
        } catch (RepositoryException e) {
            throw new IOException(e.getMessage());
        } finally {
            // reset all node-related handler state
            name = null;
            index = Path.INDEX_DEFAULT;
        }
    }

    public void array() throws IOException {
        multiValuedProperty = true;
        propValues.clear();
    }

    public void endArray() throws IOException {
        try {
            if (propertyType == PropertyType.UNDEFINED) {
                if (propValues.isEmpty()) {
                    // make sure that type is set for mv-properties with empty value array.
                    propertyType = vFactory.retrieveType(getValueURI());
                } else {
                    propertyType = propValues.get(0).getType();
                }
            }
            // create multi-valued property info
            NodeInfoImpl parent = getCurrentNodeInfo();
            Path p = pFactory.create(parent.getPath(), name, true);
            PropertyId id = idFactory.createPropertyId(parent.getId(), name);
            PropertyInfoImpl propInfo = new PropertyInfoImpl(id, p, propertyType, propValues.toArray(new QValue[propValues.size()]));
            propInfo.checkCompleted();
            getCurrentPropInfos().add(propInfo);
        } catch (RepositoryException e) {
            throw new IOException(e.getMessage());
        } finally {
            // reset property-related handler state
            propertyType = PropertyType.UNDEFINED;
            multiValuedProperty = false;
            propValues.clear();
            name = null;
        }
    }

    public void key(String key) throws IOException {
        expectingHintValue = false;
        try {
            if (key.equals(LEAF_NODE_HINT)) {
                expectingHintValue = true;
                // TODO: remember name of hint if there will be additional types of hints
                name = null;
            } else if (key.startsWith(":")) {
                expectingHintValue = true;
                // either
                //   :<nameOfProperty> : "PropertyTypeName"
                // or
                //   :<nameOfBinaryProperty> : <lengthOfBinaryProperty>
                //name = resolver.getQName(key.substring(1));
                name = resolver.getQName(StringCache.fromCacheOrNew(key.substring(1)));
                index = Path.INDEX_DEFAULT;
            } else if (key.endsWith("]")) {
                // sns-node name
                int pos = key.lastIndexOf('[');
                //name = resolver.getQName(key.substring(0, pos));
                name = resolver.getQName(StringCache.fromCacheOrNew(key.substring(0, pos)));
                propertyType = PropertyType.UNDEFINED;
                index = Integer.parseInt(key.substring(pos + 1, key.length() - 1));
            } else {
                // either node or property
                name = resolver.getQName(StringCache.cache(key));
                index = Path.INDEX_DEFAULT;
            }
        } catch (RepositoryException e) {
            throw new IOException(e.getMessage());
        }
    }

    /**
     * there is currently one special string-value hint:
     *
     *   :<nameOfProperty> : "PropertyTypeName"
     *
     * @param value The value.
     * @throws IOException
     */
    public void value(String value) throws IOException {
        if (expectingHintValue) {
            // :<nameOfProperty> : "PropertyTypeName"
            propertyType = PropertyType.valueFromName(value);
            return;
        }
        try {
            QValue v;
            switch (propertyType) {
                case PropertyType.UNDEFINED:
                    if (!NameConstants.JCR_UUID.equals(name)) {
                        value = StringCache.cache(value);
                    }
                    v = vFactory.create(value, PropertyType.STRING);
                    break;
                case PropertyType.NAME:
                    v = vFactory.create(resolver.getQName(value));
                    break;
                case PropertyType.PATH:
                    v = vFactory.create(resolver.getQPath(value));
                    break;
                default:
                    v = vFactory.create(value, propertyType);
            }
            value(v);
        } catch (RepositoryException e) {
            throw new IOException(e.getMessage());
        }
    }

    public void value(boolean value) throws IOException {
        if (expectingHintValue) {
            // there are currently no special boolean value hints:
            return;
        }
        try {
            value(vFactory.create(value));
        } catch (RepositoryException e) {
            throw new IOException(e.getMessage());
        }
    }

    /**
     * there are currently 2 types of special long value hints:
     *
     * a) ::NodeIteratorSize : 0
     *    ==> denotes the current node as leaf node
     *
     * b) :<nameOfBinaryProperty> : <lengthOfBinaryProperty>
     *
     * @param value The value.
     * @throws IOException
     */
    public void value(long value) throws IOException {
        if (expectingHintValue) {
            if (name == null) {
                // ::NodeIteratorSize : 0
                NodeInfoImpl parent = getCurrentNodeInfo();
                if (parent != null) {
                    parent.markAsLeafNode();
                }
            } else {
                // :<nameOfBinaryProperty> : <lengthOfBinaryProperty>
                propertyType = PropertyType.BINARY;
                try {
                    int indx = (!multiValuedProperty) ? -1 : propValues.size();
                    value(vFactory.create(value, getValueURI(), indx));
                } catch (RepositoryException e) {
                    throw new IOException(e.getMessage());
                }
            }
            return;
        }
        try {
            value(vFactory.create(value));
        } catch (RepositoryException e) {
            throw new IOException(e.getMessage());
        }
    }

    public void value(double value) throws IOException {
        if (expectingHintValue) {
            // currently no special double value pair -> ignore
            return;
        }
        try {
            value(vFactory.create(value));
        } catch (RepositoryException e) {
            throw new IOException(e.getMessage());
        }
    }

    //--------------------------------------------------------------------------
    /**
     *
     * @param value
     * @throws RepositoryException
     */
    private void value(QValue value) throws RepositoryException {
        if (!multiValuedProperty) {
            try {
                if (propertyType == PropertyType.UNDEFINED) {
                    propertyType = value.getType();
                }
                // create single-valued property info
                NodeInfoImpl parent = getCurrentNodeInfo();
                Path p = pFactory.create(parent.getPath(), name, true);
                PropertyId id = idFactory.createPropertyId(parent.getId(), name);
                PropertyInfoImpl propInfo = new PropertyInfoImpl(id, p, propertyType, value);
                propInfo.checkCompleted();
                // add property info to current list, will be processed on endObject() event
                getCurrentPropInfos().add(propInfo);
            } finally {
                // reset property-related handler state
                propertyType = PropertyType.UNDEFINED;
                multiValuedProperty = false;
                propValues.clear();
                name = null;
                expectingHintValue = false;
            }
        } else {
            // multi-valued property
            // add value to current list, will be processed on endArray() event
            propValues.add(value);
        }
    }

    Iterator<? extends ItemInfo> getItemInfos() {
        return Collections.unmodifiableList(itemInfos).iterator();
    }

    private NodeInfoImpl getCurrentNodeInfo() {
        return (nodeInfos.isEmpty()) ? null : (NodeInfoImpl) nodeInfos.peek();
    }

    private List<PropertyInfoImpl> getCurrentPropInfos() {
        return (propInfoLists.isEmpty()) ? null : propInfoLists.peek();
    }

    private String getValueURI() throws RepositoryException {
        Path propertyPath = pFactory.create(getCurrentNodeInfo().getPath(), name, true);
        StringBuffer sb = new StringBuffer(rootURI);
        sb.append(Text.escapePath(resolver.getJCRPath(propertyPath)));
        return sb.toString();
    }
}
