/*
 *  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.causeway.viewer.restfulobjects.rendering.domainobjects;

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

import org.apache.causeway.applib.annotation.Where;
import org.apache.causeway.commons.internal.base._Casts;
import org.apache.causeway.core.metamodel.consent.Consent;
import org.apache.causeway.core.metamodel.facetapi.Facet;
import org.apache.causeway.core.metamodel.interactions.managed.ManagedMember;
import org.apache.causeway.core.metamodel.object.ManagedObject;
import org.apache.causeway.core.metamodel.object.ManagedObjects;
import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
import org.apache.causeway.core.metamodel.spec.feature.ObjectMember;
import org.apache.causeway.viewer.restfulobjects.applib.JsonRepresentation;
import org.apache.causeway.viewer.restfulobjects.applib.Rel;
import org.apache.causeway.viewer.restfulobjects.applib.RepresentationType;
import org.apache.causeway.viewer.restfulobjects.rendering.IResourceContext;
import org.apache.causeway.viewer.restfulobjects.rendering.LinkFollowSpecs;
import org.apache.causeway.viewer.restfulobjects.rendering.ReprRendererAbstract;

import org.jspecify.annotations.NonNull;

public abstract class AbstractObjectMemberReprRenderer<T extends ObjectMember>
extends ReprRendererAbstract<ManagedMember> {

    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 ManagedObject objectAdapter;
    protected Mode mode = Mode.INLINE; // unless we determine otherwise
    /**
     * Derived from {@link #objectMember} using {@link org.apache.causeway.viewer.restfulobjects.rendering.domainobjects.MemberType#determineFrom(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;
    protected final Where where;

    public AbstractObjectMemberReprRenderer(
            final IResourceContext resourceContext,
            final LinkFollowSpecs linkFollower,
            final String memberId,
            final RepresentationType representationType,
            final JsonRepresentation representation,
            final Where where) {
        super(resourceContext, linkFollower, representationType, representation);
        this.memberId = memberId;
        this.where = where;
    }

    protected String getMemberId() {
        return memberId;
    }

    @Override
    public AbstractObjectMemberReprRenderer<T> with(final ManagedMember objectAndMember) {
        this.objectAdapter = objectAndMember.getOwner();
        this.objectMember = _Casts.uncheckedCast(objectAndMember.getMetaModel());
        this.objectMemberType = MemberType.determineFrom(objectMember);
        this.memberId = objectMember.getId();
        usingLinkTo(new DomainObjectLinkTo());

        return this;
    }

    /**
     * Must be called after {@link #with(ManagedMember)} (which provides the
     * {@link #objectAdapter}).
     */
    public AbstractObjectMemberReprRenderer<T> usingLinkTo(final ObjectAdapterLinkTo linkTo) {
        this.linkTo = linkTo.usingUrlBase(resourceContext).with(objectAdapter);
        return this;
    }

    /**
     * Indicate that this is a standalone representation.
     */
    public AbstractObjectMemberReprRenderer<T> asStandalone() {
        mode = Mode.STANDALONE;
        return this;
    }

    public AbstractObjectMemberReprRenderer<T> asEventSerialization() {
        mode = Mode.EVENT_SERIALIZATION;
        return this;
    }

    /**
     * Indicate that this is a representation to include as the result of a
     * followed link.
     */
    public AbstractObjectMemberReprRenderer<T> asFollowed() {
        mode = Mode.FOLLOWED;
        return 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 AbstractObjectMemberReprRenderer<T> asMutated() {
        mode = Mode.MUTATED;
        return this;
    }

    public AbstractObjectMemberReprRenderer<T> asArguments() {
        mode = Mode.ARGUMENTS;
        return this;
    }

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

        if(!resourceContext.config().isSuppressMemberId()) {
            representation.mapPutString("id", objectMember.getId());
        }

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

        if (mode.isInline() && !resourceContext.config().isSuppressMemberLinks()) {
            addDetailsLinkIfPersistent();
        }

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

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

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

            if(!mode.isInline() || !resourceContext.config().isSuppressUpdateLink()) {
                putExtensionsCausewayProprietary();
            }
            addLinksToFormalDomainModel();
        }
    }

    public void withMemberMode(final ManagedMember.RepresentationMode memberMode) {
        if(memberMode.isWrite()) {
            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 @NonNull MutatorSpec mutatorSpec) {
        if (!mutatorSpec.appliesTo(objectMember)) {
            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.mapPutJsonNode("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 (!ManagedObjects.isIdentifiable(objectAdapter)) {
            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.mapPutJsonRepresentation(getMemberId(), representation);
        if (membersLinkFollower.matches(memberMap) && detailsLinkFollower.matches(link)) {
            followDetailsLink(link);
        }
        return;
    }

    protected abstract void followDetailsLink(JsonRepresentation detailsLink);

    protected final void putDisabledReasonIfDisabled() {
        if(resourceContext.config().isSuppressMemberDisabledReason()) {
            return;
        }
        final String disabledReasonRep = usability().getReasonAsString().orElse(null);
        representation.mapPutString("disabledReason", disabledReasonRep);
    }

    protected abstract void putExtensionsCausewayProprietary();

    protected abstract void addLinksToFormalDomainModel();

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

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

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

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

}
