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

import com.fasterxml.jackson.databind.node.NullNode;

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.facetapi.Facet;
import org.apache.isis.core.metamodel.spec.ObjectSpecification;
import org.apache.isis.core.metamodel.spec.feature.ObjectMember;
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.LinkFollowSpecs;
import org.apache.isis.viewer.restfulobjects.rendering.RendererContext;
import org.apache.isis.viewer.restfulobjects.rendering.ReprRendererAbstract;

public abstract class AbstractObjectMemberReprRenderer<R extends ReprRendererAbstract<R, ObjectAndMember<T>>, T extends ObjectMember> extends ReprRendererAbstract<R, ObjectAndMember<T>> {


    protected enum Mode {
        INLINE, FOLLOWED, STANDALONE, MUTATED, ARGUMENTS, EVENT_SERIALIZATION;

        public boolean isInline() {
            return this == INLINE;
        }

        public boolean isFollowed() {
            return this == FOLLOWED;
        }

        public boolean isStandalone() {
            return this == STANDALONE;
        }

        public boolean isMutated() {
            return this == MUTATED;
        }

        public boolean isArguments() {
            return this == ARGUMENTS;
        }
        
        public boolean isEventSerialization() {
            return this == EVENT_SERIALIZATION;
        }
    }

    protected ObjectAdapterLinkTo linkTo;

    protected ObjectAdapter objectAdapter;
    protected Mode mode = Mode.INLINE; // unless we determine otherwise
    /**
     * Derived from {@link #objectMember} using {@link org.apache.isis.viewer.restfulobjects.rendering.domainobjects.MemberType#determineFrom(org.apache.isis.core.metamodel.spec.feature.ObjectFeature)}
     */
    protected MemberType objectMemberType;
    protected T objectMember;

    /**
     * Not for rendering, but is the key that the representation being rendered will be held under.
     * 
     * <p>
     * Used to determine whether to follow links; only populated for {@link Mode#INLINE inline} Mode.
     */
    private String memberId;
    private final Where where;

    public AbstractObjectMemberReprRenderer(
            final RendererContext rendererContext,
            final LinkFollowSpecs linkFollower,
            final String memberId,
            final RepresentationType representationType,
            final JsonRepresentation representation,
            final Where where) {
        super(rendererContext, linkFollower, representationType, representation);
        this.memberId = memberId;
        this.where = where;
    }
    
    protected String getMemberId() {
        return memberId;
    }


    @Override
    public R with(final ObjectAndMember<T> objectAndMember) {
        this.objectAdapter = objectAndMember.getObjectAdapter();
        this.objectMember = objectAndMember.getMember();
        this.objectMemberType = MemberType.determineFrom(objectMember);
        this.memberId = objectMember.getId();
        usingLinkTo(new DomainObjectLinkTo());

        return cast(this);
    }

    /**
     * Must be called after {@link #with(ObjectAndMember)} (which provides the
     * {@link #objectAdapter}).
     */
    public R usingLinkTo(final ObjectAdapterLinkTo linkTo) {
        this.linkTo = linkTo.usingUrlBase(rendererContext).with(objectAdapter);
        return cast(this);
    }

    /**
     * Indicate that this is a standalone representation.
     */
    public R asStandalone() {
        mode = Mode.STANDALONE;
        return cast(this);
    }

    public R asEventSerialization() {
        mode = Mode.EVENT_SERIALIZATION;
        return cast(this);
    }

    /**
     * Indicate that this is a representation to include as the result of a
     * followed link.
     */
    public R asFollowed() {
        mode = Mode.FOLLOWED;
        return cast(this);
    }

    /**
     * Indicates that the representation was produced as the result of a
     * resource that mutated the state.
     * 
     * <p>
     * The effect of this is to suppress the link to self.
     */
    public R asMutated() {
        mode = Mode.MUTATED;
        return cast(this);
    }

    public R asArguments() {
        mode = Mode.ARGUMENTS;
        return cast(this);
    }

    /**
     * For subclasses to call from their {@link #render()} method.
     */
    protected void renderMemberContent() {

        if(!rendererContext.suppressMemberId()) {
            representation.mapPut("id", objectMember.getId());
        }

        if(!mode.isArguments()) {
            representation.mapPut("memberType", objectMemberType.getName());
        }

        if (mode.isInline() && !rendererContext.suppressMemberLinks()) {
            addDetailsLinkIfPersistent();
        }

        if (mode.isStandalone()) {
            addLinkToSelf();
        }

        if (mode.isStandalone() || mode.isMutated()) {
            addLinkToUp();
        }

        if (mode.isFollowed() || mode.isStandalone() || mode.isMutated()) {
            addMutatorLinksIfEnabled();

            if(!mode.isInline() || !rendererContext.suppressUpdateLink()) {
                putExtensionsIsisProprietary();
            }
            addLinksToFormalDomainModel();
            addLinksIsisProprietary();
        }
    }

    public void withMemberMode(MemberReprMode memberMode) {
        if(memberMode == MemberReprMode.WRITE) {
            this.asMutated();
        } else {
            this.asStandalone();
        }
    }

    private void addLinkToSelf() {
        getLinks().arrayAdd(linkTo.memberBuilder(Rel.SELF, objectMemberType, objectMember).build());
    }

    private void addLinkToUp() {
        getLinks().arrayAdd(linkTo.builder(Rel.UP).build());
    }

    protected abstract void addMutatorLinksIfEnabled();

    /**
     * For subclasses to call back to when {@link #addMutatorLinksIfEnabled() adding
     * mutators}.
     */
    protected void addLinkFor(final MutatorSpec mutatorSpec) {
        if (!hasMemberFacet(mutatorSpec.mutatorFacetType)) {
            return;
        }
        final JsonRepresentation arguments = mutatorArgs(mutatorSpec);
        final RepresentationType representationType = objectMemberType.getRepresentationType();
        final JsonRepresentation mutatorLink = linkToForMutatorInvoke().memberBuilder(mutatorSpec.rel, objectMemberType, objectMember, representationType, mutatorSpec.suffix).withHttpMethod(mutatorSpec.httpMethod).withArguments(arguments).build();
        getLinks().arrayAdd(mutatorLink);
    }

    /**
     * Hook to allow actions to render invoke links that point to the
     * contributing service.
     */
    protected ObjectAdapterLinkTo linkToForMutatorInvoke() {
        return linkTo;
    }

    /**
     * Default implementation (common to properties and collections) that can be
     * overridden (ie by actions) if required.
     */
    protected JsonRepresentation mutatorArgs(final MutatorSpec mutatorSpec) {
        if (mutatorSpec.arguments.isNone()) {
            return null;
        }
        if (mutatorSpec.arguments.isOne()) {
            final JsonRepresentation repr = JsonRepresentation.newMap();
            repr.mapPut("value", NullNode.getInstance()); // force a null into
                                                          // the map
            return repr;
        }
        // overridden by actions
        throw new UnsupportedOperationException("override mutatorArgs() to populate for many arguments");
    }

    private void addDetailsLinkIfPersistent() {
        if (!objectAdapter.representsPersistent()) {
            return;
        }
        final JsonRepresentation link = linkTo.memberBuilder(Rel.DETAILS, objectMemberType, objectMember).build();
        getLinks().arrayAdd(link);

        final LinkFollowSpecs membersLinkFollower = getLinkFollowSpecs();
        final LinkFollowSpecs detailsLinkFollower = membersLinkFollower.follow("links");
        
        // create a temporary map that looks the same as the member map we'll be following
        final JsonRepresentation memberMap = JsonRepresentation.newMap();
        memberMap.mapPut(getMemberId(), representation);
        if (membersLinkFollower.matches(memberMap) && detailsLinkFollower.matches(link)) {
            followDetailsLink(link);
        }
        return;
    }

    protected abstract void followDetailsLink(JsonRepresentation detailsLink);

    protected final void putDisabledReasonIfDisabled() {
        if(rendererContext.suppressMemberDisabledReason()) {
            return;
        }
        final String disabledReasonRep = usability().getReason();
        representation.mapPut("disabledReason", disabledReasonRep);
    }

    protected abstract void putExtensionsIsisProprietary();

    protected abstract void addLinksToFormalDomainModel();

    protected abstract void addLinksIsisProprietary();

    /**
     * Convenience method.
     */
    public boolean isMemberVisible() {
        return visibility().isAllowed();
    }

    /**
     * Convenience method.
     */
    protected <F extends Facet> F getMemberSpecFacet(final Class<F> facetType) {
        final ObjectSpecification objetMemberSpec = objectMember.getSpecification();
        return objetMemberSpec.getFacet(facetType);
    }

    protected boolean hasMemberFacet(final Class<? extends Facet> facetType) {
        return objectMember.getFacet(facetType) != null;
    }

    protected Consent usability() {
        return objectMember.isUsable(objectAdapter, getInteractionInitiatedBy(), where);
    }

    protected Consent visibility() {
        return objectMember.isVisible(objectAdapter, getInteractionInitiatedBy(), where);
    }

}
