/*
 *  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.ui.components.attributes;

import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes.EventPropagation;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.repeater.RepeatingView;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

import org.apache.causeway.applib.services.render.PlaceholderRenderService.PlaceholderLiteral;
import org.apache.causeway.core.metamodel.interactions.managed.InteractionVeto;
import org.apache.causeway.viewer.commons.model.components.UiString;
import org.apache.causeway.viewer.wicket.model.models.ActionModel;
import org.apache.causeway.viewer.wicket.model.models.InlinePromptContext;
import org.apache.causeway.viewer.wicket.model.models.UiAttributeWkt;
import org.apache.causeway.viewer.wicket.ui.components.attributes.AttributeFragmentFactory.CompactFragment;
import org.apache.causeway.viewer.wicket.ui.components.attributes.AttributeFragmentFactory.FieldFragment;
import org.apache.causeway.viewer.wicket.ui.components.attributes.AttributeFragmentFactory.FieldFrame;
import org.apache.causeway.viewer.wicket.ui.components.attributes.AttributeFragmentFactory.PromptFragment;
import org.apache.causeway.viewer.wicket.ui.components.widgets.actionlink.ActionLink;
import org.apache.causeway.viewer.wicket.ui.exec.FormExecutorDefault;
import org.apache.causeway.viewer.wicket.ui.util.Wkt;
import org.apache.causeway.viewer.wicket.ui.util.WktTooltips;

import lombok.AccessLevel;
import lombok.Getter;

import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.fileinput.BootstrapFileInputField;

/**
 *  Adds inline prompt logic on top of the underling {@link AttributePanel}.
 */
public abstract class AttributePanelWithInlinePrompt
extends AttributePanel {

    private static final long serialVersionUID = 1L;

    // -- FIELD FRAME

    @Getter(AccessLevel.PROTECTED)
    protected MarkupContainer fieldFrame;

    // -- INLINE PROMPT LINK

    protected WebMarkupContainer inlinePromptLink;

    // -- CONSTRUCTION

    protected AttributePanelWithInlinePrompt(final String id, final UiAttributeWkt attributeModel) {
        super(id, attributeModel);
    }

    @Override
    protected final void setupInlinePrompt() {

        var attributeModel = attributeModel();
        var regularFrame = getRegularFrame();
        var fieldFrame = getFieldFrame();
        var scalarFrameContainer = getScalarFrameContainer();

        // even if this particular attributeModel (property) is not configured for inline edits,
        // it's possible that one of the associated actions is.  Thus we set the prompt context
        attributeModel.setInlinePromptContext(
                new InlinePromptContext(
                        attributeModel,
                        scalarFrameContainer,
                        regularFrame,
                        getFormFrame()));

        FieldFragment.matching(fieldFrame)
        .ifPresent(fieldFragment ->{

            switch (fieldFragment) {
            case LINK_TO_PROMT: {

                fieldFrame
                    .addOrReplace(inlinePromptLink = createInlinePromptLink());

                // needs InlinePromptContext to properly initialize
                addOnClickBehaviorTo(inlinePromptLink);

                var additionalButtonContainer = fieldFragment.createButtonContainer(inlinePromptLink);
                addAdditionalButtonsTo(additionalButtonContainer, fieldFragment);
                return;
            }
            case NO_LINK_VIEWING:
            case NO_LINK_EDITING: {

                var additionalButtonContainer = fieldFragment.createButtonContainer(fieldFrame);
                addAdditionalButtonsTo(additionalButtonContainer, fieldFragment);

                return;
            }
            default:
                break;
            }
        });

        //XXX support for legacy panels, remove eventually
        {
            if(fieldFrame!=null
                && fieldFrame.get(ID_SCALAR_VALUE)==null) {
                Wkt.labelAdd(fieldFrame, ID_SCALAR_VALUE, "∅");
            }
        }
    }

    /**
     * Builds the component to render the model when in COMPACT frame,
     * or when in REGULAR frame rendering the OUTPUT-FORMAT.
     * <p>
     * The (textual) default implementation uses a {@link Label}.
     * However, it may be overridden if required.
     */
    protected Component createComponentForOutput(final String id) {
        if(isUsingTextarea()) {
            return PromptFragment.TEXTAREA
                    .createFragment(id, this, scalarValueId->{
                        var textArea = Wkt.textAreaNoTab(scalarValueId, this::outputFormatAsString);
                        var attributeModel = attributeModel();
                        Wkt.setFormComponentAttributes(textArea,
                                attributeModel::multilineNumberOfLines,
                                attributeModel::maxLength,
                                attributeModel::typicalLength);
                        return textArea;
                    });
        }
        return CompactFragment.LABEL
                    .createFragment(id, this, scalarValueId->
                        Wkt.labelWithDynamicEscaping(scalarValueId, this::obtainOutputFormat));
    }

    // -- SEMANTICS

    private boolean isUsingTextarea() {
        if(getRenderScenario().isCompact()
                || getFormatModifiers().contains(FormatModifier.MARKUP)
                || !getFormatModifiers().contains(FormatModifier.MULTILINE)) {
            return false;
        }
        // only render a text-area if it has content
        return !attributeModel().isEmpty();
    }

    /**
     * @see #obtainOutputFormat()
     */
    protected final String outputFormatAsString() {
        return obtainOutputFormat().string();
    }

    /**
     * Output format (usually HTML) as String, for any non editing scenario.
     * <p>
     * Usually HTML, except for (non-empty) text-areas or badges (that are already modeled in HTML).
     */
    protected UiString obtainOutputFormat() {
        var proposedValue = attributeModel().proposedValue();
        if(!proposedValue.isPresent()) {
            return UiString.markup(
                    getPlaceholderRenderService()
                    .asHtml(PlaceholderLiteral.NULL_REPRESENTATION));
        }
        return isUsingTextarea()
                || getFormatModifiers().contains(FormatModifier.TEXT_ONLY)
                        ? UiString.text(proposedValue.getValueAsTitle().getValue())
                        : UiString.markup(proposedValue.getValueAsHtml().getValue());
    }

    /**
     * Optional hook.
     */
    protected void onSwitchFormForInlinePrompt(
            final WebMarkupContainer inlinePromptForm,
            final AjaxRequestTarget target) {
    }

    protected void configureInlinePromptLink(final WebMarkupContainer inlinePromptLink) {
        Wkt.cssAppend(inlinePromptLink, obtainInlinePromptLinkCssIfAny());
    }

    protected String obtainInlinePromptLinkCssIfAny() {
        return "form-control form-control-sm";
    }

    @Override
    protected void onMakeNotEditable(final String disableReason) {
        this.setupInlinePrompt(); // recreate additional buttons
    }

    @Override
    protected void onMakeEditable() {
        this.setupInlinePrompt(); // recreate additional buttons
    }

    // -- HELPER

    private void addOnClickBehaviorTo(
            final @Nullable MarkupContainer clickReceiver) {

        if(clickReceiver==null) return;

        var attributeModel = attributeModel();

        if (_Util.canPropertyEnterInlineEditDirectly(attributeModel)) {

            _Util.lookupMixinForCompositeValueUpdate(attributeModel)
            .ifPresentOrElse((final ActionModel mixinForCompositeValueEdit)->{
                // composite value type support
                var actionLink = ActionLink.create(mixinForCompositeValueEdit);
                Wkt.behaviorAddOnClick(clickReceiver, actionLink::onClick);
            },()->{
                // we configure the prompt link if _this_ property is configured for inline edits...
                Wkt.behaviorAddOnClick(clickReceiver, this::onPropertyInlineEditClick);
            });

        } else {

            _Util.lookupPropertyActionForInlineEdit(attributeModel)
            .ifPresent((final ActionModel actionLinkInlineAsIfEdit)->{
                var actionLink = ActionLink.create(actionLinkInlineAsIfEdit);
                Wkt.behaviorAddOnClick(clickReceiver, actionLink::onClick);
            });
        }
    }

    private void addAdditionalButtonsTo(
            final @NonNull RepeatingView buttonContainer, final FieldFragment fieldFragment) {

        for(var additionalButton : AttributePanelAdditionalButton.values()) {
            if(additionalButton.isVisible(attributeModel(), getRenderScenario(), fieldFragment)) {
                switch (additionalButton) {
                case COPY_TO_CLIPBOARD:
                    //XXX Future extension
                    break;
                case DISABLED_REASON:
                    addDisabledReasonIcon(buttonContainer, "fa-solid fa-ban veto-reason-icon", "");
                    break;
                case DISABLED_REASON_PROTOTYPING:
                    addDisabledReasonIcon(buttonContainer, "fa-solid fa-text-slash veto-reason-icon prototyping",
                            "\nNote: This icon only appears in prototyping mode "
                            + "(unless disabled via config option "
                            + "causeway.viewer.wicket.disable-reason-explanation-in-prototyping-mode-enabled).");
                    break;
                case CLEAR_FIELD:
                    addClearFieldButton(buttonContainer);
                    break;
                default:
                    break;
                }
            }
        }
    }

    private void addDisabledReasonIcon(
            final @NonNull RepeatingView buttonContainer,
            final @NonNull String faClass,
            final @NonNull String reasonSuffix) {
        var disableReasonButton = Wkt.linkAddWithBody(buttonContainer,
                Wkt.faIcon(faClass), ajaxTarget->{/*no-op*/});

        var disabledReason = attributeModel().disabledReason()
                .flatMap(InteractionVeto::getReasonAsString)
                .orElse("framework bug: should provide a reason");

        WktTooltips.addTooltip(disableReasonButton, translate(disabledReason) + translate(reasonSuffix));
        Wkt.noTabbing(disableReasonButton);

        if(attributeModel().isParameter()) {
            // allow the client-side popover cleaner to kick in
            disableReasonButton.setEventPropagation(EventPropagation.BUBBLE);
        } // properties otherwise do recreate the entire page anyway
    }

    private void addClearFieldButton(final @NonNull RepeatingView buttonContainer) {
        var clearFieldButton = Wkt.linkAddWithBody(buttonContainer,
                Wkt.faIcon("fa-regular fa-trash-can"), this::onClearFieldButtonClick);

        Wkt.cssAppend(clearFieldButton, "btn-warning");
        WktTooltips.addTooltip(clearFieldButton, translate("Click to clear the field"));

        if(attributeModel().isParameter()) {
            // allow the client-side popover cleaner to kick in
            clearFieldButton.setEventPropagation(EventPropagation.BUBBLE);
        } // properties otherwise do recreate the entire page anyway
    }

    private WebMarkupContainer createInlinePromptLink() {
        final WebMarkupContainer inlinePromptLink =
                FieldFrame.SCALAR_VALUE_INLINE_PROMPT_LINK
                    .createComponent(WebMarkupContainer::new);

        inlinePromptLink.setOutputMarkupId(true);
        inlinePromptLink.setOutputMarkupPlaceholderTag(true);
        configureInlinePromptLink(inlinePromptLink);

        Wkt.add(inlinePromptLink, FieldFrame.SCALAR_VALUE_CONTAINER
                .createComponent(id->createComponentForOutput(id)));

        return inlinePromptLink;
    }

    private void onPropertyInlineEditClick(final AjaxRequestTarget target) {
        attributeModel().toEditingMode();

        switchRegularFrameToFormFrame();
        onSwitchFormForInlinePrompt(getFormFrame(), target);

        target.add(getScalarFrameContainer());

        Wkt.focusOnMarkerAttribute(getFormFrame(), target); // not sure this works...
        Wkt.javaScriptAdd(target, Wkt.EventTopic.FOCUS_FIRST_PARAMETER, getMarkupId());  // .. javascript equivalent.

    }

    private void onClearFieldButtonClick(final AjaxRequestTarget target) {
        attributeModel().proposedValue().clear();
        attributeModel().getSpecialization().accept(
                param->{
                    clearBootstrapFileInputField();
                    this.setupInlinePrompt(); // recreate the param field
                    target.add(this);
                },
                prop->{
                    FormExecutorDefault.forProperty(prop)
                        .executeAndProcessResults(target, null, prop);
                });
    }

    /**
     * Workaround {@link BootstrapFileInputField} not reacting to clearing of underlying model.
     * @implNote recreates the entire {@link FormComponent}.
     */
    private void clearBootstrapFileInputField() {
        if(this instanceof AttributePanelWithFormField) {
            var formContainer = ((AttributePanelWithFormField<?>)this);
            var formComponent = formContainer.getFormComponent();
            if(formComponent instanceof BootstrapFileInputField) {
                // recreate from scratch
                var replacement = formContainer.createFormComponent(formComponent.getId(), attributeModel());
                formComponent.replaceWith(replacement);
            }
        }
    }

}
