package com.thinkbiganalytics.metadata.modeshape.extension;

/*-
 * #%L
 * thinkbig-metadata-modeshape
 * %%
 * Copyright (C) 2017 ThinkBig Analytics
 * %%
 * 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.
 * #L%
 */

import com.thinkbiganalytics.metadata.api.extension.ExtensibleType;
import com.thinkbiganalytics.metadata.api.extension.ExtensibleType.ID;
import com.thinkbiganalytics.metadata.api.extension.ExtensibleTypeBuilder;
import com.thinkbiganalytics.metadata.api.extension.ExtensibleTypeProvider;
import com.thinkbiganalytics.metadata.api.extension.FieldDescriptor;
import com.thinkbiganalytics.metadata.api.extension.FieldDescriptor.Type;
import com.thinkbiganalytics.metadata.api.extension.FieldDescriptorBuilder;
import com.thinkbiganalytics.metadata.modeshape.JcrMetadataAccess;
import com.thinkbiganalytics.metadata.modeshape.MetadataRepositoryException;
import com.thinkbiganalytics.metadata.modeshape.TypeAlreadyExistsException;
import com.thinkbiganalytics.metadata.modeshape.support.JcrUtil;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.nodetype.NoSuchNodeTypeException;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.NodeTypeIterator;
import javax.jcr.nodetype.NodeTypeManager;
import javax.jcr.nodetype.NodeTypeTemplate;
import javax.jcr.nodetype.PropertyDefinition;
import javax.jcr.nodetype.PropertyDefinitionTemplate;

public class JcrExtensibleTypeProvider implements ExtensibleTypeProvider {

    @Override
    public ID resolve(final Serializable ser) {
        return (ser instanceof ID) ? (ID) ser : new JcrExtensibleType.TypeId(ser);
    }

    @Override
    public ExtensibleType getType(final ID id) {
        final JcrExtensibleType.TypeId typeId = (JcrExtensibleType.TypeId) id;
        final Session session = getSession();
        try {
            final Node typeNode = session.getNodeByIdentifier(typeId.getIdValue());
            final NodeType nodeType = session.getWorkspace().getNodeTypeManager().getNodeType(typeNode.getName());
            return new JcrExtensibleType(typeNode, nodeType);
        } catch (ItemNotFoundException e) {
            return null;
        } catch (NoSuchNodeTypeException e) {
            return null;
        } catch (RepositoryException e) {
            throw new MetadataRepositoryException("Failure retriving exstenible type with ID: " + id, e);
        }
    }

    @Override
    public ExtensibleType getType(final String name) {
        final Session session = getSession();
        final String typeName = ensureTypeName(name);
        try {
            final NodeType nodeType = session.getWorkspace().getNodeTypeManager().getNodeType(typeName);
            final Node typeNode = session.getRootNode().getNode(ExtensionsConstants.TYPES + "/" + typeName);
            return new JcrExtensibleType(typeNode, nodeType);
        } catch (NoSuchNodeTypeException e) {
            return null;
        } catch (RepositoryException e) {
            throw new MetadataRepositoryException("Failed to lookup extensible type: " + name, e);
        }
    }

    @Override
    public List<ExtensibleType> getTypes() {
        return getTypesList("tba:extensibleEntity");
    }

    @Override
    public List<ExtensibleType> getTypes(final ExtensibleType type) {
        return getTypesList(type.getName());
    }

    @Override
    public boolean deleteType(final ID id) {
        final Session session = getSession();
        final JcrExtensibleType.TypeId typeId = (JcrExtensibleType.TypeId) id;
        try {
            final Node typeNode = session.getNodeByIdentifier(typeId.getIdValue());
            session.getWorkspace().getNodeTypeManager().unregisterNodeType(typeNode.getName());
            session.getRootNode().getNode(ExtensionsConstants.TYPES + "/" + typeNode.getName()).remove();
            return true;
        } catch (ItemNotFoundException | NoSuchNodeTypeException | NullPointerException e) {  // KYLO-8: Ignore NPE caused by unregistering a node type
            return true;
        } catch (UnsupportedRepositoryOperationException e) {
            return false;
        } catch (RepositoryException e) {
            throw new MetadataRepositoryException("Unable to retrieve extensible type: " + id, e);
        }
    }

    /**
     * Return the Property names and types for a given NodeType (i.e. pass in tba:feed).
     */
    @Override
    public Set<FieldDescriptor> getPropertyDescriptors(final String nodeType) {
        return getType(nodeType).getFieldDescriptors();
    }

    @Nonnull
    @Override
    public ExtensibleTypeBuilder buildType(@Nonnull final String name) {
        return new TypeBuilder(ensureTypeName(name), null);
    }

    @Nonnull
    @Override
    public ExtensibleTypeBuilder updateType(@Nonnull final ID id) {
        try {
            final JcrExtensibleType.TypeId typeId = (JcrExtensibleType.TypeId) id;
            final Node typeNode = getSession().getNodeByIdentifier(typeId.getIdValue());
            return new TypeBuilder(typeNode.getName(), typeNode);
        } catch (RepositoryException e) {
            throw new MetadataRepositoryException("Unable to retrieve extensible type: " + id, e);
        }
    }

    private List<ExtensibleType> getTypesList(final String typeName) {
        final Session session = getSession();

        try {
            final List<ExtensibleType> list = new ArrayList<>();
            final NodeTypeManager typeMgr = session.getWorkspace().getNodeTypeManager();
            final NodeTypeIterator typeItr = typeMgr.getPrimaryNodeTypes();
            final NodeType extensibleType = typeMgr.getNodeType(typeName);

            while (typeItr.hasNext()) {
                final NodeType nodeType = (NodeType) typeItr.next();

                if (nodeType.isNodeType(extensibleType.getName()) && !nodeType.equals(extensibleType)) {
                    final Node typeNode = session.getRootNode().getNode(ExtensionsConstants.TYPES + "/" + nodeType.getName());
                    list.add(new JcrExtensibleType(typeNode, nodeType));
                }
            }

            return list;
        } catch (RepositoryException e) {
            throw new MetadataRepositoryException("Failed to lookup all extensible types", e);
        }
    }

    public int asCode(final Type type) {
        switch (type) {
            case BOOLEAN:
                return PropertyType.BOOLEAN;
            case DOUBLE:
                return PropertyType.DOUBLE;
            case INTEGER:
                return PropertyType.LONG;
            case LONG:
                return PropertyType.LONG;
            case STRING:
                return PropertyType.STRING;
            case ENTITY:
                return PropertyType.REFERENCE;
            case WEAK_REFERENCE:
                return PropertyType.WEAKREFERENCE;
            default:
                return PropertyType.STRING;
        }
    }

    private Session getSession() {
        return JcrMetadataAccess.getActiveSession();
    }

    /**
     * Builds the type defined by the specified builder.
     *
     * @param builder the builder for the type
     * @return the type
     * @throws MetadataRepositoryException if the repository is unavailable
     * @throws TypeAlreadyExistsException  if a type with the same name already exists
     */
    @Nonnull
    private ExtensibleType buildType(@Nonnull final TypeBuilder builder) {
        try {
            // Remove existing type and subgraph
            if (builder.node != null) {
                builder.node.remove();
            }

            // Get or create the type node
            final Session session = getSession();
            final Node typeNode;

            if (!session.nodeExists(JcrUtil.path(session.getRootNode().getPath(), ExtensionsConstants.TYPES + "/" + builder.name).toString())) {
                typeNode = session.getRootNode().addNode(ExtensionsConstants.TYPES + "/" + builder.name, ExtensionsConstants.TYPE_DESCRIPTOR_TYPE);
            } else {
                throw new TypeAlreadyExistsException(builder.name);
            }

            // Update type metadata
            if (builder.displayName != null) {
                typeNode.setProperty(JcrExtensibleType.NAME, builder.displayName);
            }
            if (builder.description != null) {
                typeNode.setProperty(JcrExtensibleType.DESCRIPTION, builder.description);
            }

            // Update type definition
            final NodeTypeManager typeMgr = session.getWorkspace().getNodeTypeManager();
            final NodeTypeTemplate nodeTemplate = typeMgr.createNodeTypeTemplate();
            nodeTemplate.setName(builder.name);

            if (builder.supertype != null) {
                final JcrExtensibleType superImpl = (JcrExtensibleType) builder.supertype;
                final String supername = superImpl.getJcrName();
                nodeTemplate.setDeclaredSuperTypeNames(new String[]{ExtensionsConstants.EXTENSIBLE_ENTITY_TYPE, supername});
            } else {
                nodeTemplate.setDeclaredSuperTypeNames(new String[]{ExtensionsConstants.EXTENSIBLE_ENTITY_TYPE});
            }

            // Update field definitions
            @SuppressWarnings("unchecked")
            final List<PropertyDefinitionTemplate> propertyDefinitionTemplates = nodeTemplate.getPropertyDefinitionTemplates();

            for (final FieldBuilder bldr : builder.fieldBuilders) {
                // Create field
                PropertyDefinitionTemplate propDef = typeMgr.createPropertyDefinitionTemplate();
                propDef.setName(bldr.name);
                propDef.setRequiredType(asCode(bldr.type));
                propDef.setMandatory(bldr.required);
                propDef.setMultiple(bldr.collection);
                propertyDefinitionTemplates.add(propDef);

                // Set field metadata
                Node fieldNode = typeNode.addNode(bldr.name, ExtensionsConstants.FIELD_DESCRIPTOR_TYPE);
                for (final Map.Entry<String, String> entry : bldr.metadata.entrySet()) {
                    fieldNode.setProperty(entry.getKey(), entry.getValue());
                }
            }

            NodeType nodeType = typeMgr.registerNodeType(nodeTemplate, true);
            return new JcrExtensibleType(typeNode, nodeType);
        } catch (RepositoryException e) {
            throw new MetadataRepositoryException("Failed to create type: " + builder.name, e);
        }
    }

    /**
     * Ensures that the specified name is a valid type name.
     *
     * <p>The {@code tba:} namespace will be added if the name is not already namespaced.</p>
     *
     * @param name the name to be checked
     * @return a valid type name
     * @throws NullPointerException if the name is {@code null}
     */
    @Nonnull
    private String ensureTypeName(@Nullable final String name) {
        if (name == null) {
            throw new NullPointerException("Type name cannot be null");
        }

        return name.matches("^\\w*:.*") ? name : JcrMetadataAccess.TBA_PREFIX + ":" + name;
    }

    /**
     * An {@link ExtensibleTypeBuilder} that builds JCR {@link NodeType} objects.
     */
    private class TypeBuilder implements ExtensibleTypeBuilder {

        /**
         * Field definitions
         */
        @Nonnull
        private final Set<FieldBuilder> fieldBuilders = new HashSet<>();
        /**
         * Name of the node type
         */
        @Nonnull
        private final String name;
        /**
         * Node containing type definition
         */
        @Nullable
        private final Node node;
        /**
         * Human-readable specification
         */
        @Nullable
        private String description;
        /**
         * Human-readable title
         */
        @Nullable
        private String displayName;
        /**
         * Parent type
         */
        @Nullable
        private ExtensibleType supertype;

        /**
         * Constructs an {@code ExtensibleTypeBuilder} with the specified type name and optional type node.
         *
         * @param name the name of the node type
         * @param node the node containing the type definition, or {@code null} if this is a new type
         */
        TypeBuilder(@Nonnull final String name, @Nullable final Node node) {
            this.name = name;
            this.node = node;
        }

        @Override
        public ExtensibleTypeBuilder supertype(final ExtensibleType type) {
            this.supertype = type;
            return this;
        }

        @Override
        public ExtensibleTypeBuilder displayName(final String dispName) {
            this.displayName = dispName;
            return this;
        }

        @Override
        public ExtensibleTypeBuilder description(String descr) {
            this.description = descr;
            return this;
        }

        @Override
        public ExtensibleTypeBuilder addField(String name, Type type) {
            return new FieldBuilder(this).name(name).type(type).add();
        }

        @Override
        public FieldDescriptorBuilder field(String name) {
            return new FieldBuilder(this).name(name);
        }

        @Nonnull
        @Override
        public ExtensibleType build() {
            return buildType(this);
        }
    }

    /**
     * A {@link FieldDescriptorBuilder} that builds JCR {@link PropertyDefinition} objects.
     */
    private class FieldBuilder implements FieldDescriptorBuilder {

        /**
         * Metadata for this field
         */
        @Nonnull
        private final Map<String, String> metadata = new HashMap<>();
        /**
         * Parent type builder
         */
        @Nonnull
        private final TypeBuilder typeBuilder;
        /**
         * Indicates that multiple values are allowed
         */
        private boolean collection;
        /**
         * Name of this field
         */
        @Nullable
        private String name;
        /**
         * Indicates that a value is required
         */
        private boolean required;
        /**
         * Value type
         */
        private Type type;

        /**
         * Constructs a {@code FieldBuilder} with the specified parent type builder.
         *
         * @param typeBldr the parent type builder
         */
        FieldBuilder(@Nonnull final TypeBuilder typeBldr) {
            this.typeBuilder = typeBldr;
        }

        @Override
        public FieldDescriptorBuilder name(final String name) {
            this.name = name;
            return this;
        }

        @Override
        public FieldDescriptorBuilder type(final Type type) {
            this.type = type;
            return this;
        }

        @Override
        public FieldDescriptorBuilder displayName(final String name) {
            metadata.put(JcrExtensibleType.NAME, name);
            return this;
        }

        @Override
        public FieldDescriptorBuilder description(final String descr) {
            metadata.put(JcrExtensibleType.DESCRIPTION, descr);
            return this;
        }

        @Override
        public FieldDescriptorBuilder collection(final boolean flag) {
            this.collection = flag;
            return this;
        }

        @Override
        public FieldDescriptorBuilder required(final boolean flag) {
            this.required = flag;
            return this;
        }

        @Nonnull
        @Override
        public FieldDescriptorBuilder property(@Nonnull final String name, @Nonnull final String value) {
            metadata.put(name, value);
            return this;
        }

        @Override
        public ExtensibleTypeBuilder add() {
            this.typeBuilder.fieldBuilders.add(this);
            return this.typeBuilder;
        }
    }
}
