/*
 *  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;

import java.util.List;
import java.util.Map;

import javax.ws.rs.core.MediaType;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
import org.apache.isis.core.metamodel.deployment.DeploymentCategory;
import org.apache.isis.core.metamodel.spec.ObjectSpecification;
import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
import org.apache.isis.viewer.restfulobjects.applib.Rel;
import org.apache.isis.viewer.restfulobjects.applib.RepresentationType;
import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.DomainObjectReprRenderer;
import org.apache.isis.viewer.restfulobjects.rendering.domaintypes.DomainTypeReprRenderer;
import org.apache.isis.viewer.restfulobjects.rendering.service.RepresentationService;

public abstract class ReprRendererAbstract<R extends ReprRendererAbstract<R, T>, T> implements ReprRenderer<R, T> {

    protected final RendererContext rendererContext;
    private final LinkFollowSpecs linkFollower;
    private final RepresentationType representationType;
    protected final JsonRepresentation representation;
    private final Map<String,String> mediaTypeParams = Maps.newLinkedHashMap();

    private final DeploymentCategory deploymentCategory;
    private final InteractionInitiatedBy interactionInitiatedBy;

    protected boolean includesSelf;

    public ReprRendererAbstract(
            final RendererContext rendererContext,
            final LinkFollowSpecs linkFollower,
            final RepresentationType representationType,
            final JsonRepresentation representation) {
        this.rendererContext = rendererContext;
        this.linkFollower = asProvidedElseCreate(linkFollower);
        this.representationType = representationType;
        this.representation = representation;

        this.deploymentCategory = determineDeploymentCategoryFrom(this.rendererContext);
        this.interactionInitiatedBy = determineInteractionInitiatedByFrom(this.rendererContext);
    }

    private static DeploymentCategory determineDeploymentCategoryFrom(final RendererContext rendererContext) {
        if(rendererContext instanceof RendererContext3) {
            return ((RendererContext3) rendererContext).getDeploymentCategory();
        } else {
            return DeploymentCategory.PRODUCTION; // fallback
        }
    }

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

    protected DeploymentCategory getDeploymentCategory() {
        return deploymentCategory;
    }

    protected InteractionInitiatedBy getInteractionInitiatedBy() {
        return interactionInitiatedBy;
    }


    public RendererContext getRendererContext() {
        return rendererContext;
    }

    public LinkFollowSpecs getLinkFollowSpecs() {
        return linkFollower;
    }

    private LinkFollowSpecs asProvidedElseCreate(final LinkFollowSpecs linkFollower) {
        if (linkFollower != null) {
            return linkFollower;
        }
        return LinkFollowSpecs.create(rendererContext.getFollowLinks());
    }

    @Override
    public MediaType getMediaType() {
        return representationType.getMediaType(mediaTypeParams);
    }

    protected void addMediaTypeParams(String param, String paramValue) {
        mediaTypeParams.put(param, paramValue);
    }

    @SuppressWarnings("unchecked")
    public R includesSelf() {
        this.includesSelf = true;
        return (R) this;
    }

    public R withLink(final Rel rel, final String href) {
        if (href != null) {
            getLinks().arrayAdd(LinkBuilder.newBuilder(rendererContext, rel.getName(), representationType, href).build());
        }
        return cast(this);
    }

    public R withLink(final Rel rel, final JsonRepresentation link) {
        final String relStr = link.getString("rel");
        if (relStr == null || !relStr.equals(rel.getName())) {
            throw new IllegalArgumentException("Provided link does not have a 'rel' of '" + rel.getName() + "'; was: " + link);
        }
        if (link != null) {
            getLinks().arrayAdd(link);
        }
        return cast(this);
    }


    /**
     * Will lazily create links array as required
     */
    protected JsonRepresentation getLinks() {
        JsonRepresentation links = representation.getArray("links");
        if (links == null) {
            links = JsonRepresentation.newArray();
            representation.mapPut("links", links);
        }
        return links;
    }

    protected void addLink(final Rel rel, final ObjectSpecification objectSpec) {
        if (objectSpec == null) {
            return;
        }
        final LinkBuilder linkBuilder = DomainTypeReprRenderer.newLinkToBuilder(getRendererContext(), rel, objectSpec);
        JsonRepresentation link = linkBuilder.build();
        getLinks().arrayAdd(link);
        
        final LinkFollowSpecs linkFollower = getLinkFollowSpecs().follow("links");
        if (linkFollower.matches(link)) {
            final DomainTypeReprRenderer renderer = new DomainTypeReprRenderer(getRendererContext(), linkFollower, JsonRepresentation.newMap())
                .with(objectSpec);
            link.mapPut("value", renderer.render());
        }

    }

    /**
     * Will lazily create extensions map as required
     */
    protected JsonRepresentation getExtensions() {
        JsonRepresentation extensions = representation.getMap("extensions");
        if (extensions == null) {
            extensions = JsonRepresentation.newMap();
            representation.mapPut("extensions", extensions);
        }
        return extensions;
    }

    public R withExtensions(final JsonRepresentation extensions) {
        if (!extensions.isMap()) {
            throw new IllegalArgumentException("extensions must be a map");
        }
        representation.mapPut("extensions", extensions);
        return cast(this);
    }

    @SuppressWarnings("unchecked")
    protected static <R extends ReprRendererAbstract<R, T>, T> R cast(final ReprRendererAbstract<R, T> builder) {
        return (R) builder;
    }

    @Override
    public abstract JsonRepresentation render();

    /**
     * Convenience for representations that are returned from objects that
     * mutate state.
     */
    protected final void addExtensionsIsisProprietaryChangedObjects() {

        // TODO: have removed UpdateNotifier, plan is to re-introduce using the IsisTransaction enlisted objects
        // (which would also allow newly-created objects to be shown)
        final List<ObjectAdapter> changedObjects = Lists.newArrayList(); // updateNotifier.getChangedObjects();
        final List<ObjectAdapter> disposedObjects = Lists.newArrayList(); // updateNotifier.getDisposedObjects();

        addToExtensions("changed", changedObjects);
        addToExtensions("disposed", disposedObjects);
    }

    private void addToExtensions(final String key, final List<ObjectAdapter> adapters) {
        if(adapters == null || adapters.isEmpty()) {
            return;
        }
        final JsonRepresentation adapterList = JsonRepresentation.newArray();
        getExtensions().mapPut(key, adapterList);
        for (final ObjectAdapter adapter : adapters) {
            adapterList.arrayAdd(DomainObjectReprRenderer.newLinkToBuilder(getRendererContext(), Rel.VALUE, adapter).build());
        }
    }

    protected List<ObjectAdapter> getServiceAdapters() {
        return rendererContext.getPersistenceSession().getServices();
    }

}
