/*******************************************************************************
 * Copyright (c) 2014, 2015 Oracle. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *      Dmitry Kornilov - Initial implementation
 ******************************************************************************/
package org.eclipse.persistence.jpa.rs.util;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.internal.weaving.PersistenceWeavedRest;
import org.eclipse.persistence.jaxb.ObjectGraph;
import org.eclipse.persistence.jaxb.Subgraph;
import org.eclipse.persistence.jpa.rs.PersistenceContext;
import org.eclipse.persistence.jpa.rs.features.fieldsfiltering.FieldsFilter;
import org.eclipse.persistence.jpa.rs.features.fieldsfiltering.FieldsFilterType;
import org.eclipse.persistence.jpa.rs.util.list.PageableCollection;
import org.eclipse.persistence.jpa.rs.util.list.ReadAllQueryResultCollection;
import org.eclipse.persistence.jpa.rs.util.list.ReportQueryResultCollection;
import org.eclipse.persistence.jpa.rs.util.list.SingleResultQueryResult;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.ForeignReferenceMapping;

import javax.xml.bind.JAXBElement;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Builds object graph for JPARS 2.0. Object graph defines the structure of the returned data. It hides some
 * fields which are not suppose to be returned to the client.
 *
 * @author Dmitry Kornilov
 * @since EclipseLink 2.6.0
 */
public class ObjectGraphBuilder {
    private final PersistenceContext context;
    private final List<String> RESERVED_WORDS = Arrays.asList("hasMore", "limit", "offset", "links", "count", "items", "_persistence_links");

    /**
     * Creates an object graph builder.
     *
     * @param context the persistence context
     */
    public ObjectGraphBuilder(PersistenceContext context) {
        this.context = context;
    }

    /**
     * Builds object graph for specified object using given filter.
     *
     * @param object the object to build object graph for. Mandatory.
     * @param filter the filter (included or excluded fields) to use. Optional.
     * @return constructed object graph.
     */
    public ObjectGraph createObjectGraph(Object object, FieldsFilter filter) {
        final Node root = new Node();

        if (PersistenceWeavedRest.class.isAssignableFrom(object.getClass())) {
            createNodeForEntity(object, root);
        } else if (object instanceof SingleResultQueryResult) {
            root.addAttributeNode("links");

            final SingleResultQueryResult singleResultQueryResult = (SingleResultQueryResult)object;
            processFieldsList(root.addSubNode("fields"), singleResultQueryResult.getFields());
        } else if (object instanceof ReadAllQueryResultCollection) {
            createNodeForPageableCollection((PageableCollection) object, root);
        } else {
            return null;
        }

        ObjectGraph objectGraph = context.getJAXBContext().createObjectGraph(object.getClass());
        fillObjectGraphFromNode(objectGraph, root, filter);

        return objectGraph;
    }

    private void createNodeForPageableCollection(PageableCollection collection, Node node) {
        node.addAttributeNode("hasMore");
        node.addAttributeNode("count");
        node.addAttributeNode("offset");
        node.addAttributeNode("limit");
        node.addAttributeNode("links");
        if (collection.getItems() != null && !collection.getItems().isEmpty()) {
            final Node subNode = node.addSubNode("items");
            if (collection instanceof ReportQueryResultCollection) {
                final ReportQueryResultCollection reportQueryResultCollection = (ReportQueryResultCollection)collection;
                processFieldsList(subNode.addSubNode("fields"), reportQueryResultCollection.getItems().get(0).getFields());
            } else {
                createNodeForEntity(collection.getItems().get(0), subNode);
            }
        }
    }

    private void processFieldsList(Node fieldsNode, List<JAXBElement> elements) {
        for (JAXBElement field : elements) {
            if (field.getValue() instanceof PersistenceWeavedRest) {
                final Node subNode = fieldsNode.addSubNode(field.getName().toString());
                subNode.addAttributeNode("_persistence_links");
            } else {
                fieldsNode.addAttributeNode(field.getName().toString());
            }
        }
    }

    private void createNodeForEntity(final Object object, final Node node) {
        final ClassDescriptor classDescriptor = context.getServerSession().getProject().getDescriptors().get(object.getClass());
        if (classDescriptor == null) {
            return;
        }
        node.addAttributeNode("_persistence_links");

        for (final DatabaseMapping mapping : classDescriptor.getMappings()) {
            if (ForeignReferenceMapping.class.isAssignableFrom(mapping.getClass())) {
                final Node subNode = node.addSubNode(mapping.getAttributeName());
                if (mapping.isCollectionMapping()) {
                    // Add CollectionWrapper links and items properties
                    subNode.addAttributeNode("links");
                    final Node itemsSubNode = subNode.addSubNode("items");
                    itemsSubNode.addAttributeNode("_persistence_links");
                } else {
                    // PersistenceWeavedRest._persistence_links
                    subNode.addAttributeNode("_persistence_links");
                }
            } else {
                node.addAttributeNode(mapping.getAttributeName());
            }
        }
    }

    private void fillObjectGraphFromNode(ObjectGraph objectGraph, Node node, FieldsFilter filter) {
        for (final String attribute : node.getNodesMap().keySet()) {
            if (filter != null) {
                if (filter.getType() == FieldsFilterType.INCLUDE) {
                    if (!(RESERVED_WORDS.contains(attribute) || filter.getFields().contains(attribute))) {
                        continue;
                    }
                } else {
                    if (filter.getFields().contains(attribute)) {
                        continue;
                    }
                }
            }

            final Node value = node.get(attribute);
            if (value != null) {
                final Subgraph subgraph = objectGraph.addSubgraph(attribute);
                fillSubgraphFromNode(subgraph, value, filter);
            } else {
                objectGraph.addAttributeNodes(attribute);
            }
        }
    }

    private void fillSubgraphFromNode(Subgraph subgraph, Node node, FieldsFilter filter) {
        for (final String attribute : node.getNodesMap().keySet()) {
            final Node value = node.get(attribute);
            if (filter != null) {
                if (filter.getType() == FieldsFilterType.INCLUDE) {
                    if (!(RESERVED_WORDS.contains(attribute) || filter.getFields().contains(attribute))) {
                        continue;
                    }
                } else {
                    if (filter.getFields().contains(attribute)) {
                        continue;
                    }
                }
            }

            if (value != null) {
                final Subgraph childSubgraph = subgraph.addSubgraph(attribute);
                fillSubgraphFromNode(childSubgraph, value, filter);
            } else {
                subgraph.addAttributeNodes(attribute);
            }
        }
    }

    /**
     * Internal object graph node.
     */
    private static class Node {
        private final Map<String, Node> nodesMap = new HashMap<String, Node>();

        public void addAttributeNode(final String attribute) {
            nodesMap.put(attribute, null);
        }

        public Node addSubNode(final String attribute) {
            final Node subNode = new Node();
            nodesMap.put(attribute, subNode);
            return subNode;
        }

        public Map<String, Node> getNodesMap() {
            return nodesMap;
        }

        public Node get(final String key) {
            return nodesMap.get(key);
        }

        @Override
        public String toString() {
            return "Node{" +
                    "nodesMap=" + nodesMap +
                    '}';
        }
    }
}
