/*
 *  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.isis.viewer.restfulobjects.rendering.service.conneg;

import java.util.Collection;
import java.util.List;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.apache.isis.applib.DomainObjectContainer;
import org.apache.isis.applib.annotation.DomainService;
import org.apache.isis.applib.annotation.NatureOfService;
import org.apache.isis.applib.annotation.Programmatic;
import org.apache.isis.applib.annotation.Where;
import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
import org.apache.isis.core.metamodel.consent.Consent;
import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
import org.apache.isis.core.metamodel.facets.collections.modify.CollectionFacet;
import org.apache.isis.core.metamodel.facets.collections.modify.CollectionFacetUtils;
import org.apache.isis.core.metamodel.spec.ObjectSpecification;
import org.apache.isis.core.metamodel.spec.feature.Contributed;
import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
import org.apache.isis.viewer.restfulobjects.applib.domainobjects.ActionResultRepresentation;
import org.apache.isis.viewer.restfulobjects.rendering.RendererContext;
import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndAction;
import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndActionInvocation;
import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndCollection;
import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndProperty;
import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectPropertyReprRenderer;
import org.apache.isis.viewer.restfulobjects.rendering.service.RepresentationService;

@DomainService(
        nature = NatureOfService.DOMAIN,
        menuOrder = "" + (Integer.MAX_VALUE - 10)
)
public class ContentNegotiationServiceOrgApacheIsisV1 extends ContentNegotiationServiceAbstract {

    /**
     * Unlike RO v1.0, use a single content-type of <code>application/json;profile="urn:org.apache.isis/v1"</code>.
     *
     * <p>
     * The response content types ({@link #CONTENT_TYPE_OAI_V1_OBJECT}, {@link #CONTENT_TYPE_OAI_V1_OBJECT_COLLECTION},
     * {@link #CONTENT_TYPE_OAI_V1_LIST}) append the 'repr-type' parameter.
     * </p>
     */
    public static final String ACCEPT_PROFILE = "urn:org.apache.isis/v1";

    /**
     * Whether to suppress the RO v1.0 '$$ro' node; suppress='false' or suppress='true'
     */
    public static final String ACCEPT_RO = "suppress";

    /**
     * The media type (as a string) used as the content-Type header when a domain object is rendered.
     *
     * @see #ACCEPT_PROFILE for discussion.
     */
    public static final String CONTENT_TYPE_OAI_V1_OBJECT = "application/json;"
            + "profile=\"" + ACCEPT_PROFILE + "\""
            + ";repr-type=\"object\""
            ;
    /**
     * The media type (as a string) used as the content-Type header when a parented collection is rendered.
     *
     * @see #ACCEPT_PROFILE for discussion.
     */
    public static final String CONTENT_TYPE_OAI_V1_OBJECT_COLLECTION = "application/json;"
            + "profile=\"" + ACCEPT_PROFILE + "\""
            + ";repr-type=\"object-collection\""
            ;
    /**
     * The media type (as a string) used as the content-Type header when a standalone collection is rendered.
     *
     * @see #ACCEPT_PROFILE for discussion.
     */
    public static final String CONTENT_TYPE_OAI_V1_LIST = "application/json;"
            + "profile=\"" + ACCEPT_PROFILE + "\""
            + ";repr-type=\"list\""
            ;

    private ContentNegotiationServiceForRestfulObjectsV1_0 restfulObjectsV1_0 = new ContentNegotiationServiceForRestfulObjectsV1_0();

    /**
     * Domain object is returned as a map with the RO 1.0 representation as a special '$$ro' property
     * within that map.
     */
    public Response.ResponseBuilder buildResponse(
            final RepresentationService.Context2 rendererContext,
            final ObjectAdapter objectAdapter) {

        boolean canAccept = canAccept(rendererContext);
        if(!canAccept) {
            return null;
        }
        boolean suppressRO = suppress(rendererContext);

        final JsonRepresentation rootRepresentation = JsonRepresentation.newMap();

        appendObjectTo(rendererContext, objectAdapter, rootRepresentation);

        final JsonRepresentation $$roRepresentation;
        if(!suppressRO) {
            $$roRepresentation = JsonRepresentation.newMap();
            rootRepresentation.mapPut("$$ro", $$roRepresentation);
        } else {
            $$roRepresentation = null;
        }
        final Response.ResponseBuilder responseBuilder =
                restfulObjectsV1_0.buildResponseTo(
                        rendererContext, objectAdapter, $$roRepresentation, rootRepresentation);

        responseBuilder.type(CONTENT_TYPE_OAI_V1_OBJECT);

        return responseBuilder(responseBuilder);
    }

    /**
     * Individual property of an object is not supported.
     */
    @Programmatic
    public Response.ResponseBuilder buildResponse(
            final RepresentationService.Context2 rendererContext,
            final ObjectAndProperty objectAndProperty)  {

        return null;
    }

    /**
     * Individual (parented) collection of an object is returned as a list with the RO representation
     * as an object in the list with a single property named '$$ro'
     */
    @Programmatic
    public Response.ResponseBuilder buildResponse(
            final RepresentationService.Context2 rendererContext,
            final ObjectAndCollection objectAndCollection) {

        if(!canAccept(rendererContext)) {
            return null;
        }
        boolean suppressRO = suppress(rendererContext);

        final JsonRepresentation rootRepresentation = JsonRepresentation.newArray();

        ObjectAdapter objectAdapter = objectAndCollection.getObjectAdapter();
        OneToManyAssociation collection = objectAndCollection.getMember();

        appendCollectionTo(rendererContext, objectAdapter, collection, rootRepresentation);

        final JsonRepresentation $$roRepresentation;
        if(!suppressRO) {
            // $$ro representation will be an object in the list with a single property named "$$ro"
            final JsonRepresentation $$roContainerRepresentation = JsonRepresentation.newMap();
            rootRepresentation.arrayAdd($$roContainerRepresentation);

            $$roRepresentation = JsonRepresentation.newMap();
            $$roContainerRepresentation.mapPut("$$ro", $$roRepresentation);
        } else {
            $$roRepresentation = null;
        }

        final Response.ResponseBuilder responseBuilder =
                restfulObjectsV1_0.buildResponseTo(
                        rendererContext, objectAndCollection, $$roRepresentation, rootRepresentation);

        responseBuilder.type(CONTENT_TYPE_OAI_V1_OBJECT_COLLECTION);

        return responseBuilder(responseBuilder);
    }

    /**
     * Action prompt is not supported.
     */
    @Programmatic
    public Response.ResponseBuilder buildResponse(
            final RepresentationService.Context2 rendererContext,
            final ObjectAndAction objectAndAction)  {
        return null;
    }

    /**
     * Action invocation is supported provided it returns a single domain object or a list of domain objects
     * (ie invocations returning void or scalar value are not supported).
     *
     * Action invocations returning a domain object will be rendered as a map with the RO v1.0 representation as a
     * '$$ro' property within (same as {@link #buildResponse(RepresentationService.Context2, ObjectAdapter)}), while
     * action invocations returning a list will be rendered as a list with the RO v1.0 representation as a map object
     * with a single '$$ro' property (similar to {@link #buildResponse(RepresentationService.Context2, ObjectAndCollection)})
     */
    @Programmatic
    public Response.ResponseBuilder buildResponse(
            final RepresentationService.Context2 rendererContext,
            final ObjectAndActionInvocation objectAndActionInvocation) {

        if(!canAccept(rendererContext)) {
            return null;
        }
        boolean suppressRO = suppress(rendererContext);

        JsonRepresentation rootRepresentation = null;
        final JsonRepresentation $$roRepresentation;
        if(!suppressRO) {
            $$roRepresentation = JsonRepresentation.newMap();
        } else {
            $$roRepresentation = null;
        }

        final ObjectAdapter returnedAdapter = objectAndActionInvocation.getReturnedAdapter();
        final ObjectSpecification returnType = objectAndActionInvocation.getAction().getReturnType();

        if (returnedAdapter == null) {
            return null;
        }

        final ActionResultRepresentation.ResultType resultType = objectAndActionInvocation.determineResultType();
        switch (resultType) {
            case DOMAIN_OBJECT:

                rootRepresentation = JsonRepresentation.newMap();
                appendObjectTo(rendererContext, returnedAdapter, rootRepresentation);

                break;

            case LIST:

                rootRepresentation = JsonRepresentation.newArray();

                final CollectionFacet collectionFacet = returnType.getFacet(CollectionFacet.class);
                final Collection<ObjectAdapter> collectionAdapters = collectionFacet.collection(returnedAdapter);
                appendIterableTo(rendererContext, collectionAdapters, rootRepresentation);

                // $$ro representation will be an object in the list with a single property named "$$ro"
                if(!suppressRO) {
                    JsonRepresentation $$roContainerRepresentation = JsonRepresentation.newMap();
                    rootRepresentation.arrayAdd($$roContainerRepresentation);
                    $$roContainerRepresentation.mapPut("$$ro", $$roRepresentation);
                }

                break;

            case SCALAR_VALUE:
            case VOID:

                // not supported
                return null;
        }

        final Response.ResponseBuilder responseBuilder =
                restfulObjectsV1_0.buildResponseTo(
                        rendererContext, objectAndActionInvocation, $$roRepresentation, rootRepresentation);

        // set appropriate Content-Type
        responseBuilder.type(
                resultType == ActionResultRepresentation.ResultType.DOMAIN_OBJECT
                        ? CONTENT_TYPE_OAI_V1_OBJECT
                        : CONTENT_TYPE_OAI_V1_LIST
        );

        return responseBuilder(responseBuilder);
    }

    /**
     * For easy subclassing to further customize, eg additional headers
     */
    protected Response.ResponseBuilder responseBuilder(final Response.ResponseBuilder responseBuilder) {
        return responseBuilder;
    }

    boolean canAccept(final RepresentationService.Context2 rendererContext) {
        final List<MediaType> acceptableMediaTypes = rendererContext.getAcceptableMediaTypes();
        return mediaTypeParameterMatches(acceptableMediaTypes, "profile", ACCEPT_PROFILE);
    }

    protected boolean suppress(
            final RepresentationService.Context2 rendererContext) {
        final List<MediaType> acceptableMediaTypes = rendererContext.getAcceptableMediaTypes();
        return mediaTypeParameterMatches(acceptableMediaTypes, "suppress", "true");
    }

    private void appendObjectTo(
            final RepresentationService.Context2 rendererContext,
            final ObjectAdapter objectAdapter,
            final JsonRepresentation rootRepresentation) {

        appendPropertiesTo(rendererContext, objectAdapter, rootRepresentation);

        final List<OneToManyAssociation> collections = objectAdapter.getSpecification().getCollections(Contributed.INCLUDED);
        final Where where = rendererContext.getWhere();
        for (final OneToManyAssociation collection : collections) {

            final JsonRepresentation collectionRepresentation = JsonRepresentation.newArray();

            rootRepresentation.mapPut(collection.getId(), collectionRepresentation);

            final InteractionInitiatedBy interactionInitiatedBy = determineInteractionInitiatedByFrom(rendererContext);
            final Consent visibility = collection.isVisible(objectAdapter, interactionInitiatedBy, where);
            if (!visibility.isAllowed()) {
                continue;
            }

            appendCollectionTo(rendererContext, objectAdapter, collection, collectionRepresentation);
        }
    }

    private void appendPropertiesTo(
            final RepresentationService.Context2 rendererContext,
            final ObjectAdapter objectAdapter,
            final JsonRepresentation rootRepresentation) {
        final InteractionInitiatedBy interactionInitiatedBy = determineInteractionInitiatedByFrom(rendererContext);
        final Where where = rendererContext.getWhere();
        List<OneToOneAssociation> properties = objectAdapter.getSpecification().getProperties(Contributed.INCLUDED);
        for (final OneToOneAssociation property : properties) {

            final Consent visibility = property.isVisible(objectAdapter, interactionInitiatedBy, where);
            if (!visibility.isAllowed()) {
                continue;
            }

            final JsonRepresentation propertyRepresentation = JsonRepresentation.newMap();
            final ObjectPropertyReprRenderer renderer =
                    new ObjectPropertyReprRenderer(rendererContext, null, property.getId(), propertyRepresentation)
                        .asStandalone();
            renderer.with(new ObjectAndProperty(objectAdapter, property));

            final JsonRepresentation propertyValueRepresentation = renderer.render();

            final String upHref = propertyValueRepresentation.getString("links[rel=up].href");
            rootRepresentation.mapPut("$$href", upHref);
            final String upTitle = propertyValueRepresentation.getString("links[rel=up].title");
            rootRepresentation.mapPut("$$title", upTitle);
            final String upInstanceId = upHref.substring(upHref.lastIndexOf("/")+1);
            rootRepresentation.mapPut("$$instanceId", upInstanceId);

            final JsonRepresentation value = propertyValueRepresentation.getRepresentation("value");
            rootRepresentation.mapPut(property.getId(), value);
        }
    }

    private void appendCollectionTo(
            final RepresentationService.Context2 rendererContext,
            final ObjectAdapter objectAdapter,
            final OneToManyAssociation collection,
            final JsonRepresentation representation) {

        final InteractionInitiatedBy interactionInitiatedBy = determineInteractionInitiatedByFrom(rendererContext);
        final ObjectAdapter valueAdapter = collection.get(objectAdapter, interactionInitiatedBy);
        if (valueAdapter == null) {
            return;
        }

        final CollectionFacet facet = CollectionFacetUtils.getCollectionFacetFromSpec(valueAdapter);
        final Iterable<ObjectAdapter> iterable = facet.iterable(valueAdapter);
        appendIterableTo(rendererContext, iterable, representation);
    }

    private void appendIterableTo(
            final RepresentationService.Context2 rendererContext,
            final Iterable<ObjectAdapter> iterable,
            final JsonRepresentation collectionRepresentation) {
        for (final ObjectAdapter elementAdapter : iterable) {

            JsonRepresentation elementRepresentation = JsonRepresentation.newMap();
            appendPropertiesTo(rendererContext, elementAdapter, elementRepresentation);

            collectionRepresentation.arrayAdd(elementRepresentation);
        }
    }

    private static InteractionInitiatedBy determineInteractionInitiatedByFrom(
            final RendererContext rendererContext) {
        if (rendererContext instanceof RepresentationService.Context4) {
            return ((RepresentationService.Context4) rendererContext).getInteractionInitiatedBy();
        } else {
            // fallback
            return InteractionInitiatedBy.USER;
        }
    }


    @javax.inject.Inject
    protected DomainObjectContainer container;
}
