/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2012, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.gatein.api.management;

import org.gatein.api.common.i18n.LocalizedString;
import org.gatein.api.navigation.Navigation;
import org.gatein.api.navigation.Node;
import org.gatein.api.navigation.NodePath;
import org.gatein.api.navigation.NodeVisitor;
import org.gatein.api.navigation.Nodes;
import org.gatein.api.navigation.PublicationDate;
import org.gatein.api.navigation.Visibility;
import org.gatein.api.page.PageId;
import org.gatein.api.site.SiteId;
import org.gatein.api.site.SiteType;
import org.gatein.management.api.PathAddress;
import org.gatein.management.api.annotations.Managed;
import org.gatein.management.api.annotations.ManagedContext;
import org.gatein.management.api.annotations.ManagedOperation;
import org.gatein.management.api.annotations.ManagedRole;
import org.gatein.management.api.annotations.MappedAttribute;
import org.gatein.management.api.annotations.MappedPath;
import org.gatein.management.api.model.Model;
import org.gatein.management.api.model.ModelList;
import org.gatein.management.api.model.ModelNumber;
import org.gatein.management.api.model.ModelObject;
import org.gatein.management.api.model.ModelProvider;
import org.gatein.management.api.model.ModelReference;
import org.gatein.management.api.model.ModelString;
import org.gatein.management.api.model.ModelValue;
import org.gatein.management.api.operation.OperationContext;
import org.gatein.management.api.operation.OperationNames;

import java.util.Date;
import java.util.Locale;

import static org.gatein.api.management.GateInApiManagementResource.*;
import static org.gatein.api.management.Utils.*;

/**
 * @author <a href="mailto:nscavell@redhat.com">Nick Scavelli</a>
 */
@Managed
@SuppressWarnings("unused")
public class NavigationManagementResource {
    private final Navigation navigation;
    private final ModelProvider modelProvider;

    public NavigationManagementResource(Navigation navigation, ModelProvider modelProvider) {
        this.navigation = navigation;
        this.modelProvider = modelProvider;
    }

    @Managed
    public ModelObject getNavigation(@ManagedContext OperationContext context,
                                     @MappedAttribute("scope") String scopeAttribute,
                                     @MappedAttribute("showAll") String showAllAttribute) {
        NodeVisitor visitor = Nodes.visitChildren();
        int scope = 0;
        if (scopeAttribute != null) {
            scope = Integer.parseInt(scopeAttribute);
            visitor = Nodes.visitNodes(scope);
        }

        Node node = getNode(NodePath.root(), true, visitor);
        boolean showAll = showAllAttribute != null && Boolean.parseBoolean(showAllAttribute);
        if (!showAll || !context.getExternalContext().isUserInRole("administrators")) {
            node = node.filter().showDefault();
        }

        return populateNavigationModel(node, scope, context);
    }

    @Managed
    @ManagedOperation(name = OperationNames.UPDATE_RESOURCE, description = "Updates the navigation")
    public ModelObject updateNavigation(@ManagedContext OperationContext context, @ManagedContext ModelObject navModel) {
        ModelNumber priority = get(navModel, ModelNumber.class, "priority");
        if (priority.isDefined()) {
            navigation.setPriority(priority.getInt());
        }

        Node node = getNode(NodePath.root(), true, Nodes.visitChildren());
        return populateNavigationModel(node, 0, context);
    }

    @Managed("{path: .*}")
    public ModelObject getNode(@MappedPath("path") String path, @MappedAttribute("scope") String scopeAttribute,
                               @MappedAttribute("showAll") String showAllAttribute, @ManagedContext OperationContext context) {
        NodeVisitor visitor = Nodes.visitChildren();
        int scope = 0;
        if (scopeAttribute != null) {
            scope = Integer.parseInt(scopeAttribute);
            visitor = Nodes.visitNodes(scope);
        }
        Node node = getNode(path, true, visitor);
        boolean showAll = showAllAttribute != null && Boolean.parseBoolean(showAllAttribute);
        if (showAll && context.getExternalContext().isUserInRole("administrators")) {
            node = node.filter().showDefault();
        } else {
            node = node.filter().showDefault();
        }

        // Populate the model
        ModelObject model = modelProvider.newModel(ModelObject.class);
        populateNode(node, scope, model, context.getAddress());

        return model;
    }

    @Managed("{path: .*}")
    @ManagedRole("administrators")
    @ManagedOperation(name = OperationNames.REMOVE_RESOURCE, description = "Removes the navigation node")
    public void removeNode(@MappedPath("path") String path) {
        Node node = getNode(path, true);

        Node parent = node.getParent();
        parent.removeChild(node.getName());
        navigation.saveNode(parent);
    }

    @Managed("{path: .*}")
    @ManagedRole("administrators")
    @ManagedOperation(name = OperationNames.ADD_RESOURCE, description = "Adds the navigation node")
    public ModelObject addNode(@MappedPath("path") String path, @ManagedContext PathAddress address) {
        NodePath nodePath = NodePath.fromString(path);
        Node parent = getNode(nodePath.parent(), true, Nodes.visitChildren());
        String name = nodePath.getLastSegment();

        if (parent.hasChild(name)) {
            throw alreadyExists("Cannot add node", navigation.getSiteId(), nodePath);
        }

        // Add child and save
        Node child = parent.addChild(name);
        navigation.saveNode(parent);

        // Populate model
        ModelObject model = modelProvider.newModel(ModelObject.class);
        populateNode(child, 0, model, address);

        return model;
    }

    @Managed("{path: .*}")
    @ManagedRole("administrators")
    @ManagedOperation(name = OperationNames.UPDATE_RESOURCE, description = "Updates the navigation node")
    public ModelObject updateNode(@MappedPath("path") String path, @ManagedContext ModelObject nodeModel, @ManagedContext PathAddress address) {

        // Update node from model
        Node node = getNode(path, true);
        updateNodeFromModel(node, nodeModel);

        // Save node
        navigation.saveNode(node);

        // Populate model for update node
        ModelObject updateNodeModel = modelProvider.newModel(ModelObject.class);
        populateNode(node, 0, updateNodeModel, address);

        return updateNodeModel;
    }

    private Node getNode(String pathString, boolean require) {
        return getNode(pathString, require, Nodes.visitNone());
    }

    private Node getNode(String pathString, boolean require, NodeVisitor visitor) {
        return getNode(NodePath.fromString(pathString), require, visitor);
    }

    private Node getNode(NodePath path, boolean require) {
        return getNode(path, require, Nodes.visitNone());
    }

    private Node getNode(NodePath path, boolean require, NodeVisitor visitor) {
        Node node = navigation.getNode(path, visitor);
        if (node == null && require)
            throw notFound("Cannot retrieve node", navigation.getSiteId(), path);

        return node;
    }

    private ModelObject populateNavigationModel(Node rootNode, int scope, OperationContext context) {
        ModelObject model = modelProvider.newModel(ModelObject.class);

        PathAddress address = context.getAddress();

        // Populate navigation fields
        model.set("priority", navigation.getPriority());
        model.set("siteType", navigation.getSiteId().getType().getName());
        model.set("siteName", navigation.getSiteId().getName());
        ModelList nodesModel = model.get("nodes").setEmptyList();
        if (rootNode.isChildrenLoaded()) {
            for (Node child : rootNode) {
                Model childModel = nodesModel.add();
                PathAddress childAddress = address.append(child.getName());
                if (scope > 0 || scope < 0) // Continue populating nodes in response
                {
                    populateNode(child, scope - 1, childModel.setEmptyObject(), childAddress);
                } else { // Populate node reference which can be followed
                    ModelReference nodeRef = childModel.set(childAddress);
                    nodeRef.set("name", child.getName());
                }
            }
        }

        return model;
    }

    private void populateNode(Node node, int scope, ModelObject model, PathAddress address) {
        model.set("name", node.getName());
        set("uri", node.getURI(), model);
        model.set("isVisible", node.isVisible());
        populateVisibility(node.getVisibility(), model.get("visibility", ModelObject.class));
        model.set("iconName", node.getIconName());

        // Display name
        model.set("displayName", node.getDisplayName());
        populate("displayNames", node.getDisplayNames(), model);

        // Children nodes
        ModelList children = model.get("children", ModelList.class);
        if (node.isChildrenLoaded()) {
            for (Node child : node) {
                Model childModel = children.add();
                PathAddress childAddress = address.append(child.getName());
                if (scope > 0 || scope < 0) // Continue populating nodes in response
                {
                    populateNode(child, scope - 1, childModel.setEmptyObject(), childAddress);
                } else { // Populate node reference which can be followed
                    ModelReference nodeRef = childModel.set(childAddress);
                    nodeRef.set("name", child.getName());
                }
            }
        }
        // Page reference
        ModelReference pageRef = model.get("page").asValue(ModelReference.class);
        if (node.getPageId() != null) {
            PageId pageId = node.getPageId();
            pageRef.set("pageName", pageId.getPageName());
            pageRef.set("siteName", pageId.getSiteId().getName());
            pageRef.set("siteType", pageId.getSiteId().getType().getName());

            // Set the address for the ref
            PathAddress pageAddress = getPagesAddress(pageId.getSiteId()).append(pageId.getPageName());
            pageRef.set(pageAddress);
        }
    }

    private void populateVisibility(Visibility visibility, ModelObject model) {
        if (visibility != null) {
            set("status", visibility.getStatus(), model);
            if (visibility.getPublicationDate() != null) {
                ModelObject pubDateModel = model.get("publication-date", ModelObject.class);
                Date start = visibility.getPublicationDate().getStart();
                Date end = visibility.getPublicationDate().getEnd();
                set("start", start, pubDateModel);
                set("end", end, pubDateModel);
            }
        }
    }

    private void updateNodeFromModel(Node node, ModelObject nodeModel) {

        // Update name
        if (nodeModel.has("name")) {
            node.setName(nonNullString(nodeModel, "name"));
        }

        // Update visibility
        if (nodeModel.has("visibility")) {
            Visibility visibility = getVisibility(nodeModel, node.getVisibility());
            if (visibility != null) {
                node.setVisibility(visibility);
            }
        }

        // Update iconName
        if (nodeModel.has("iconName")) {
            String iconName = get(nodeModel, ModelString.class, "iconName").getValue();
            node.setIconName(iconName);
        }

        // Update pageId
        if (nodeModel.has("page")) {
            node.setPageId(getPageId(nodeModel));
        }

        //TODO: Support adding and not just overwriting. i.e. one locale/value pair is added to rest.
        if (nodeModel.has("displayNames")) {
            LocalizedString displayName = getDisplayNames(nodeModel);
            node.setDisplayNames(displayName);
        } else if (nodeModel.has("displayName")) {
            String displayName = get(nodeModel, ModelString.class, "displayName").getValue();
            if (displayName == null) {
                throw invalidValue(null, "displayName");
            }
            node.setDisplayName(displayName);
        }
    }

    private static LocalizedString getDisplayNames(ModelObject nodeModel) {
        ModelList list = get(nodeModel, ModelList.class, "displayNames");
        if (!list.isDefined()) {
            throw invalidValue(null, "displayNames");
        }
        LocalizedString displayName = null;
        int i=0;
        for (ModelValue mv : list) {
            ModelObject displayNameModel = mv.asValue(ModelObject.class);

            // Parse value (required && non-null)
            if (!displayNameModel.has("value")) {
                throw requiredField("displayNames["+i+"].value");
            }
            String value = get(displayNameModel, ModelString.class, "value").getValue();
            if (value == null) {
                throw invalidValue(value, "displayNames[" + i + "].value");
            }

            // Parse lang (not-required but if defined must be non-null)
            if (displayNameModel.has("lang")) {
                ModelString langModel = get(displayNameModel, ModelString.class, "lang");
                String lang = langModel.getValue();
                if (lang == null) {
                    throw invalidValue(lang, "displayNames[" + i + "].lang");
                }
                Locale locale = getLocale(displayNameModel, "lang");
                if (displayName == null) {
                    displayName = new LocalizedString(locale, value);
                } else {
                    displayName.setLocalizedValue(locale, value);
                }
            } else if (displayName == null) {
                displayName = new LocalizedString(value);
            } else {
                throw invalidData("Cannot have multiple non localized values for displayNames");
            }
        }

        return displayName;
    }

    private static PageId getPageId(ModelObject nodeModel) {
        ModelObject pageModel = get(nodeModel, ModelObject.class, "page");
        if (pageModel.isDefined()) {
            String pageName = nonNullString(nodeModel, "page", "pageName");
            String siteName = nonNullString(nodeModel, "page", "siteName");
            String siteTypeString = nonNullString(nodeModel, "page", "siteType");
            SiteType siteType = SiteType.forName(siteTypeString);
            if (siteType == null) {
                throw invalidValue(siteTypeString, "page", "siteType");
            }
            return new PageId(new SiteId(siteType, siteName), pageName);
        } else {
            return null;
        }
    }

    private static Visibility getVisibility(ModelObject nodeModel, Visibility original) {
        Visibility.Status status = getStatus(nodeModel);
        if (status == Visibility.Status.PUBLICATION) {
            ModelObject pubDateModel = get(nodeModel, ModelObject.class, "visibility", "publication-date");
            // If status was set to PUBLICATION however no publication date was specified then throw exception
            if (!pubDateModel.isDefined()) {
                throw requiredFieldWhen("visibility status is " + status, "visibility", "publication-date");
            }
            PublicationDate publicationDate = getPublicationDate(nodeModel, original.getPublicationDate());
            return new Visibility(publicationDate);
        } else {
            return new Visibility(status);
        }
    }

    private static Visibility.Status getStatus(ModelObject nodeModel) {
        String statusString = nonNullString(nodeModel, "visibility", "status");
        Visibility.Status status;
        try {
            status = Visibility.Status.valueOf(statusString.toUpperCase());
        } catch (IllegalArgumentException e) {
            throw invalidValue(statusString, "visibility", "status");
        }
        return status;
    }

    private static PublicationDate getPublicationDate(ModelObject nodeModel, PublicationDate previous) {
        ModelString startModel = get(nodeModel, ModelString.class, "visibility", "publication-date", "start");
        Date start = (previous == null) ? null : previous.getStart();
        if (startModel.isDefined()) {
            start = getDate(nodeModel, "visibility", "publication-date", "start");
        }

        ModelString endModel = get(nodeModel, ModelString.class, "visibility", "publication-date", "end");
        Date end = (previous == null) ? null : previous.getEnd();
        if (endModel.isDefined()) {
            end = getDate(nodeModel, "visibility", "publication-date", "end");
        }

        if (start != null && end != null) {
            return PublicationDate.between(start, end);
        } else if (start == null && end != null) {
            return PublicationDate.endingOn(end);
        } else if (start != null) {
            return PublicationDate.startingOn(start);
        } else {
            throw invalidData("Either 'start' or 'end' is required for visibility.publication-date");
        }
    }
}
