/*
 *  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.wicket.model.models;

import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;

import org.apache.wicket.model.IModel;
import org.apache.wicket.util.convert.IConverter;
import org.jspecify.annotations.NonNull;

import org.apache.causeway.applib.annotation.PromptStyle;
import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.functional.Either;
import org.apache.causeway.core.metamodel.commons.ViewOrEditMode;
import org.apache.causeway.core.metamodel.interactions.managed.ManagedValue;
import org.apache.causeway.core.metamodel.object.ManagedObject;
import org.apache.causeway.core.metamodel.object.ManagedObjects;
import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
import org.apache.causeway.core.metamodel.util.Facets;
import org.apache.causeway.viewer.commons.model.attrib.UiAttribute;
import org.apache.causeway.viewer.commons.model.hints.HasRenderingHints;
import org.apache.causeway.viewer.commons.model.hints.RenderingHint;
import org.apache.causeway.viewer.wicket.model.value.ConverterBasedOnValueSemantics;

import lombok.Getter;
import lombok.Setter;

/**
 * We refer to both method parameters and instance fields collectively
 * as "attributes" of a class or method.
 * <p>
 * Represents an attribute of a domain object, either a PROPERTY or
 * a PARAMETER. Potentially plural in the latter case.
 * <p>
 * Is also the backing model to each of the fields that appear in forms (for entities
 * or action dialogs).
 *
 * @implSpec
 * <pre>
 * UiAttributeWkt --chained-to--> UiObjectWkt
 * UiAttributeWkt --provides--> ManagedObject <--provides-- ManagedValue
 * </pre>
 * @see UiAttribute
 */
//@Slf4j
public abstract class UiAttributeWkt
implements IModel<ManagedObject>, HasRenderingHints, UiAttribute, FormExecutorContext {

    private static final long serialVersionUID = 1L;

    private final UiObjectWkt parentEntityModel;

    @Getter(onMethod_={@Override})
    @Setter(onMethod_={@Override})
    private ViewOrEditMode viewOrEditMode;

    @Getter(onMethod_={@Override})
    private RenderingHint renderingHint;

    /**
     * Creates a model representing an action parameter of an action of a parent
     * object, with the {@link #getObject() value of this model} to be default
     * value (if any) of that action parameter.
     */
    protected UiAttributeWkt(
            final UiObjectWkt parentUiObject,
            final ViewOrEditMode viewOrEdit) {
        this(parentUiObject, viewOrEdit, RenderingHint.REGULAR);
    }

    /**
     * Creates a model representing a property of a parent object, with the
     * {@link #getObject() value of this model} to be current value of the
     * property.
     */
    protected UiAttributeWkt(
            final @NonNull UiObjectWkt parentEntityModel,
            final @NonNull ViewOrEditMode viewOrEdit,
            final @NonNull RenderingHint renderingHint) {
        this.parentEntityModel = parentEntityModel;
        this.renderingHint = renderingHint;
        this.viewOrEditMode = viewOrEdit;
    }

    /**
     * This instance is either a {@link ParameterModel} or a {@link PropertyModel}.
     */
    public final Either<ParameterModel, PropertyModel> getSpecialization() {
        return this.isParameter()
                ? Either.left((ParameterModel) this)
                : Either.right((PropertyModel) this);
    }

    public <T> T unwrapAs(final Class<T> type) {
        return new ScalarUnwrapper<T>(type, this).getObject();
    }

    public <T> IModel<T> unwrapped(final Class<T> type) {
        return new IModel<T>() {
            private static final long serialVersionUID = 1L;
            @Override public T getObject() {
                return new ScalarUnwrapper<T>(type, UiAttributeWkt.this).getObject();
            }
            @Override public void setObject(final T t) {
                new ScalarUnwrapper<T>(type, UiAttributeWkt.this).setObject(t);
            }
        };
    }

    /**
     * Gets the proposed value as ManagedObject.
     * (override, so we don't return the target model, we are chained to)
     */
    @Override
    public final ManagedObject getObject() {
        var proposedValue = proposedValue();
        return ManagedObjects.nullToEmpty(
                proposedValue.getElementType(),
                proposedValue.getValue().getValue());
    }

    /**
     * Sets given ManagedObject as new proposed value.
     * (override, so we don't return the target model, we are chained to)
     * <p>
     * This happens during Wicket's {@code formComponent.updateModel()},
     * which updates the models first, before later calling {@code onUpdate(target)}
     * to repaint those components, that have a changed/dirty model.
     * <p>
     * in other words: changed components get a chance to participate in the partial page update
     * based on whether their models have changed
     */
    @Override
    public final void setObject(final ManagedObject newValue) {
        proposedValue().update(oldValue->{
            _Xray.onSclarModelUpdate(this, oldValue, newValue);
            return newValue;
        });
    }

    @Override
    public final UiObjectWkt getParentUiModel() {
        return parentEntityModel;
    }

    public final boolean isEmpty() {
        return ManagedObjects.isNullOrUnspecifiedOrEmpty(proposedValue().getValue().getValue());
    }

    public abstract ManagedValue proposedValue();

    public abstract String validate(@NonNull ManagedObject proposedAdapter);

    @Override
    public final PromptStyle getPromptStyle() {
        return Facets.promptStyleOrElse(getMetaModel(), PromptStyle.INLINE);
    }

    // -- CONVERSION

    public final <T> Optional<IConverter<T>> getConverter(final Class<T> requiredType) {
        final ObjectFeature propOrParam = getMetaModel();
        return Optional.of(
                new ConverterBasedOnValueSemantics<>(requiredType, propOrParam, getViewOrEditMode()));
    }

    // -- PREDICATES

    @Override
    public final boolean isInlinePrompt() {
        return hasAssociatedActionWithInlineAsIfEdit()
                || (getPromptStyle().isInline()
                        && isViewingMode()
                        && !disabledReason().isPresent());
    }

    /**
     * Whether or not to display some indication that a form field is mandatory.
     * Eg. a star in the UI.
     */
    public final boolean isShowMandatoryIndicator() {
        return isRequired()
                && !disabledReason().isPresent();
    }

    // -- INLINE PROMPT

    private InlinePromptContext inlinePromptContext;

    /**
     * Further hint, to support inline prompts...
     */
    @Override
    public InlinePromptContext getInlinePromptContext() {
        return inlinePromptContext;
    }

    public void setInlinePromptContext(final InlinePromptContext inlinePromptContext) {
        if (this.inlinePromptContext != null) {
            // otherwise the components created for an property edit inline prompt will overwrite the original
            // components on the underlying page (which we go back to if the prompt is cancelled).
            return;
        }
        this.inlinePromptContext = inlinePromptContext;
    }

    // -- ASSOCIATED ACTIONS

    public final AssociatedActions getAssociatedActions() {
        if (associatedActions == null) {
            associatedActions = AssociatedActions.of(calcAssociatedActions());
        }
        return associatedActions;
    }

    public final boolean hasAssociatedActionWithInlineAsIfEdit() {
        return getAssociatedActions().firstAssociatedWithInlineAsIfEdit().isPresent();
    }

    protected transient AssociatedActions associatedActions;

    public record AssociatedActions(
            Optional<ObjectAction> firstAssociatedWithInlineAsIfEdit,
            List<ObjectAction> remainingAssociated) {

        static AssociatedActions of(final Can<ObjectAction> allAssociated) {
            Optional<ObjectAction> firstAssociatedWithInlineAsIfEdit = firstAssociatedActionWithInlineAsIfEdit(allAssociated);
            return new AssociatedActions(
                    firstAssociatedWithInlineAsIfEdit,
                    firstAssociatedWithInlineAsIfEdit.isPresent()
                        ? allAssociated.remove(firstAssociatedWithInlineAsIfEdit.get()).toList()
                        : allAssociated.toList());
        }

        private static Optional<ObjectAction> firstAssociatedActionWithInlineAsIfEdit(
                final Can<ObjectAction> objectActions) {
            return objectActions.stream()
                .filter(act->ObjectAction.Util.promptStyleFor(act).isInlineAsIfEdit())
                .findFirst();
        }
    }

    protected abstract Can<ObjectAction> calcAssociatedActions();

    // --

    public final OptionalInt multilineNumberOfLines() {
        return Facets.multilineNumberOfLines(getMetaModel());
    }

    public final OptionalInt maxLength() {
        return Facets.maxLength(getElementType());
    }

    public final OptionalInt typicalLength() {
        return Facets.typicalLength(getElementType(), maxLength());
    }

    // -- TO STRING

    @Override
    public String toString() {
        return toStringOf();
    }

    protected abstract String toStringOf();

}
