/*
 * 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.sequencer.teiid.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import org.modeshape.common.collection.Multimap;
import org.modeshape.common.logging.Logger;
import org.modeshape.common.util.CheckArg;
import org.modeshape.jcr.api.JcrConstants;
import org.modeshape.jcr.api.sequencer.Sequencer.Context;
import org.modeshape.sequencer.teiid.TeiidI18n;
import org.modeshape.sequencer.teiid.VdbModel;
import org.modeshape.sequencer.teiid.lexicon.CoreLexicon;
import org.modeshape.sequencer.teiid.lexicon.DiagramLexicon;
import org.modeshape.sequencer.teiid.lexicon.JdbcLexicon;
import org.modeshape.sequencer.teiid.lexicon.RelationalLexicon;
import org.modeshape.sequencer.teiid.lexicon.TransformLexicon;
import org.modeshape.sequencer.teiid.model.ReferenceResolver.UnresolvedProperty;
import org.modeshape.sequencer.teiid.model.ReferenceResolver.UnresolvedReference;
import org.modeshape.sequencer.teiid.xmi.XmiElement;

/**
 * Writes the JCR node structure for a model.
 */
public final class ModelNodeWriter {

    private static final Logger LOGGER = Logger.getLogger(ModelNodeWriter.class);

    private final Context context;
    private final Map<String, ModelObjectHandler> handlers = new HashMap<String, ModelObjectHandler>();
    private final Node outputNode;
    private final ModelReader reader;
    private final Map<String, Class<? extends ModelObjectHandler>> registry = new HashMap<String, Class<? extends ModelObjectHandler>>();
    private final ReferenceResolver resolver;
    private final VdbModel vdbModel;
    private final ModelExtensionDefinitionHelper medHelper;

    /**
     * @param modelNode the model node where the output should be written to (cannot be <code>null</code>)
     * @param reader the model reader (cannot be <code>null</code>)
     * @param resolver the reference resolver (cannot be <code>null</code>)
     * @param vdbModel the VDB model (can be <code>null</code> if model did not come from a VDB)
     * @param context the sequencer context (cannot be <code>null</code>)
     * @throws Exception if the model node does not have a model primary node type
     */
    ModelNodeWriter( final Node modelNode,
                     final ModelReader reader,
                     final ReferenceResolver resolver,
                     final VdbModel vdbModel,
                     final Context context ) throws Exception {
        CheckArg.isNotNull(modelNode, "modelNode");
        CheckArg.isNotNull(reader, "reader");
        CheckArg.isNotNull(resolver, "resolver");
        CheckArg.isNotNull(context, "context");

        if (!modelNode.isNodeType(CoreLexicon.JcrId.MODEL)) {
            throw new RuntimeException(TeiidI18n.invalidModelNodeType.text(modelNode.getPath()));
        }

        this.outputNode = modelNode;
        this.resolver = resolver;
        this.reader = reader;
        this.vdbModel = vdbModel;
        this.context = context;
        this.medHelper = new ModelExtensionDefinitionHelper(this);
    }

    Context getContext() {
        return this.context;
    }

    Node getModelNode() {
        return this.outputNode;
    }

    ReferenceResolver getResolver() {
        return this.resolver;
    }

    ModelObjectHandler getHandler( final String namespaceUri ) throws Exception {
        CheckArg.isNotEmpty(namespaceUri, "namespaceUri");

        // see if handler has already been constructed
        ModelObjectHandler handler = this.handlers.get(namespaceUri);

        // construct if necessary
        if ((handler == null) && this.registry.containsKey(namespaceUri)) {
            final Class<? extends ModelObjectHandler> handlerClass = this.registry.get(namespaceUri);
            handler = handlerClass.newInstance();
            this.handlers.put(namespaceUri, handler);

            // set handler properties
            handler.setContext(this.context);
            handler.setReader(this.reader);
            handler.setResolver(this.resolver);
            handler.setVdbModel(this.vdbModel);
            handler.setModelExtensionDefinitionHelper(this.medHelper);
        }

        return handler;
    }

    public boolean isAcceptedPrimaryMetamodel( final String uri ) {
        return RelationalLexicon.Namespace.URI.equals(uri);
    }

    private void loadRegistry() {
        this.registry.put(CoreLexicon.Namespace.URI, CoreModelObjectHandler.class);
        this.registry.put(DiagramLexicon.Namespace.URI, DiagramModelObjectHandler.class);
        this.registry.put(JdbcLexicon.Namespace.URI, JdbcModelObjectHandler.class);
        this.registry.put(RelationalLexicon.Namespace.URI, RelationalModelObjectHandler.class);
        this.registry.put(TransformLexicon.Namespace.URI, TransformationModelObjectHandler.class);
    }

    public boolean write() throws Exception {
        long startTime = System.currentTimeMillis();
        boolean result = true;

        // use primary metamodel URI to determine if we should continue sequencing
        final String primaryMetamodelUri = this.reader.getPrimaryMetamodelUri();

        if ((primaryMetamodelUri == null) || !isAcceptedPrimaryMetamodel(primaryMetamodelUri)) {
            result = false;
        } else {
            // load model object handler registry
            loadRegistry();

            if (!writeModelObjects()) {
                result = false;
            } else {
                result = writeUnresolvedReferences();

                // add MED mixins to node
                this.medHelper.assignModelNodeChildrenMedMixins(this.outputNode);
            }
        }

        LOGGER.debug("model write time={0}\n\n", (System.currentTimeMillis() - startTime));

        return result;
    }

    private boolean writeModelObjects() throws Exception {
        LOGGER.debug("[begin writeModelObjects()]");

        for (final XmiElement element : this.reader.getElements()) {
            final String nsUri = element.getNamespaceUri();
            final ModelObjectHandler handler = getHandler(nsUri);

            if (handler == null) {
                LOGGER.debug("ModelObjectHandler for namespace {0} cannot be found", nsUri);
                continue;
            }

            handler.process(element, this.outputNode);
        }

        LOGGER.debug("[end writeModelObjects()]\n\n");
        return true;
    }

    public boolean writeUnresolvedReferences() throws Exception {
        LOGGER.debug("[begin writeUnresolvedReferences()]");

        // keep track of the unresolved references that have been resolved so that they can be marked as resolved later
        List<UnresolvedReference> resolvedReferences = new ArrayList<ReferenceResolver.UnresolvedReference>();

        for (final Entry<String, UnresolvedReference> entry : this.resolver.getUnresolved().entrySet()) {
            final Node resolved = this.resolver.getNode(entry.getKey());

            if (resolved == null) {
                LOGGER.debug("**** uuid {0} is still unresolved during last phase of writing model", entry.getKey());
                continue;
            }

            final UnresolvedReference unresolved = entry.getValue();
            final ValueFactory valueFactory = resolved.getSession().getValueFactory();

            // add mixins
            for (final String mixin : unresolved.getMixins()) {
                resolved.addMixin(mixin);
                LOGGER.debug("adding mixin {0} to resolved node {1}", mixin, resolved.getName());
            }

            { // add properties
                for (final String propName : unresolved.getProperties().keySet()) {
                    UnresolvedProperty property = unresolved.getProperties().get(propName);
                    assert (property != null);

                    boolean multiValued = property.isMulti();

                    if (multiValued) {
                        Value[] propertyValues = new Value[property.getValues().size()];
                        int i = 0;

                        for (String value : property.getValues()) {
                            propertyValues[i++] = valueFactory.createValue(value);
                        }
                    } else {
                        // single valued
                        final String mappedName = this.medHelper.getMappedPropertyName(propName);
                        resolved.setProperty(mappedName, property.getValue());
                        LOGGER.debug("setting property '{0}' with value '{1}' to resolved node {2}",
                                     propName,
                                     property.getValue(),
                                     resolved.getName());
                    }
                }
            }

            { // add weakreferences
                for (final String propertyName : unresolved.getReferences().keySet()) {
                    final Collection<String> refs = unresolved.getReferences().get(propertyName);

                    if ((refs == null) || refs.isEmpty()) {
                        continue;
                    }

                    boolean multiValued = false;

                    if (resolved.hasProperty(propertyName)) {
                        multiValued = resolved.getProperty(propertyName).isMultiple();

                        if (multiValued) {
                            Value[] values = new Value[refs.size()];
                            int i = 0;

                            for (final String value : refs) {
                                Node referencedNode = this.resolver.getNode(value);

                                if (referencedNode == null) {
                                    this.resolver.addUnresolvedReference(value);
                                } else {
                                    values[i++] = valueFactory.createValue(referencedNode, true);
                                }
                            }

                            resolved.setProperty(propertyName, values);
                        } else {
                            // single valued property so just use first value
                            resolved.setProperty(propertyName, refs.iterator().next());
                        }
                    } else {
                        LOGGER.debug("**** resolved property does not have property '{0}'. The value has {1}  reference(s) and first reference is '{2}'",
                                     propertyName,
                                     refs.size(),
                                     refs.iterator().next());
                    }
                }
            }

            { // add referenced node name to referencer property
                for (final String propertyName : unresolved.getReferenceNames().keySet()) {
                    final Collection<String> referencerUuids = unresolved.getReferenceNames().get(propertyName);

                    if ((referencerUuids == null) || referencerUuids.isEmpty()) {
                        continue;
                    }

                    for (final String uuid : referencerUuids) {
                        Node referencerNode = this.resolver.getNode(uuid);

                        if (referencerNode == null) {
                            // referencer node is unresolved
                            UnresolvedReference unresolvedReferencer = this.resolver.addUnresolvedReference(uuid);
                            unresolvedReferencer.addProperty(propertyName, resolved.getName(), true);
                        } else {
                            referencerNode.setProperty(propertyName,
                                                       new Value[] { this.context.valueFactory().createValue(
                                                               resolved.getName()) });
                        }
                    }
                }
            }

            { // referencer references
                Multimap<String, String> refRefs = unresolved.getReferencerReferences();

                for (final String propertyName : refRefs.keySet()) {
                    if (!resolved.isNodeType(JcrConstants.MIX_REFERENCEABLE)) {
                        resolved.addMixin(JcrConstants.MIX_REFERENCEABLE);
                    }

                    Value weakRef = valueFactory.createValue(resolved, true);

                    // property needs to get set with the weak reference of the resolved node
                    for (final String referencerUuuid : refRefs.get(propertyName)) {
                        Node referencer = this.resolver.getNode(referencerUuuid);

                        if (referencer == null) {
                            UnresolvedReference unresolvedReferencer = this.resolver.addUnresolvedReference(referencerUuuid);
                            unresolvedReferencer.addReference(propertyName, entry.getKey());
                        } else {
                            if (referencer.hasProperty(propertyName)) {
                                Property prop = referencer.getProperty(propertyName);

                                if (prop.isMultiple()) {
                                    Value[] currentValues = prop.getValues();
                                    final Value[] newValues = new Value[currentValues.length + 1];
                                    System.arraycopy(currentValues, 0, newValues, 0, currentValues.length);
                                    newValues[currentValues.length] = weakRef;
                                    referencer.setProperty(propertyName, newValues);
                                } else {
                                    referencer.setProperty(propertyName, weakRef);
                                }
                            } else {
                                LOGGER.debug("**** weak reference property could be multi-value here");
                            }
                        }
                    }
                }
            }
        }

        // let resolver know the references were resolved
        if (!resolvedReferences.isEmpty()) {
            for (UnresolvedReference unresolved : resolvedReferences) {
                this.resolver.resolved(unresolved);
            }
        }

        LOGGER.debug("number unresolved at end={0}\n[end writeUnresolvedReferences()]\n\n", this.resolver.getUnresolved().size());

        return true;
    }
}
