/*
 *  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.applib.events.domain;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;

import org.jspecify.annotations.Nullable;

import org.apache.causeway.applib.Identifier;
import org.apache.causeway.applib.events.EventObjectBase;
import org.apache.causeway.applib.services.i18n.TranslatableString;
import org.apache.causeway.applib.util.ObjectContracts;
import org.apache.causeway.applib.util.ToString;
import org.apache.causeway.commons.internal.base._Casts;
import org.apache.causeway.commons.internal.exceptions._Exceptions;

import lombok.Getter;
import org.jspecify.annotations.NonNull;

/**
 * Superclass for all domain events that are raised by the framework when
 * interacting with actions, properties or collections.
 *
 * <p>
 *     The main purpose of the class is to define the protocol by which
 *     subscribers can influence an interaction (eg hide a collection,
 *     disable a property, validate action arguments).
 * </p>
 *
 * <p>
 *     The class also provides a simple mechanism to allow adhoc sharing of
 *     user data between different phases.
 * </p>
 *
 * @see ActionDomainEvent
 * @see PropertyDomainEvent
 * @see CollectionDomainEvent
 *
 * @since 1.x {@index}
 */
public abstract class AbstractDomainEvent<S> extends EventObjectBase<S> {

    /**
     * If used then the framework will set state via (non-API) setters.
     *
     * <p>
     *     Because the {@link EventObjectBase} superclass prohibits a null source,
     *     a dummy value is temporarily used.
     * </p>
     */
    public AbstractDomainEvent() {
        this(null, null);
    }

    /**
     * If used then the framework will set the remaining state via (non-API) setters.
     *
     * <p>
     *     Provided to allow nested non-static implementations, for use in nested non-static mixins.
     * </p>
     */
    public AbstractDomainEvent(final S source) {
        this(source, null);
    }

    public AbstractDomainEvent(
            final S source,
            final Identifier identifier) {
        super(source);
        this.identifier = identifier;
    }

    /**
     * The domain object raising this event.
     *
     * <p>
     * For a "regular" action, property or collection then this will be the
     * target domain object.
     * </p>
     *
     * <p>
     *     But for a "mixin" action, this will be an instance of the mixin
     *     itself.
     * </p>
     */
    @Nullable
    @Override
    public S getSource() {
        return super.getSource();
    }

    /**
     * Populated only for mixins; holds the underlying domain object that the
     * mixin contributes to.
     *
     * <p>
     * For a "regular" action, this will return <code>null</code>.
     * </p>
     */
    @Getter
    private Object mixee;

    /**
     * Not API - set by the framework.
     */
    public final void setMixee(final Object mixee) {
        this.mixee = mixee;
    }

    /**
     * The subject of the event, which will be either the
     * {@link #getSource() source} for a regular action, or the
     * {@link #getMixee() mixed-in} domain object for a mixin.
     */
    public <T> T getSubject() {
        var mixee = getMixee();
        var mixedInElseSource = mixee != null
                ? mixee
                : getSource();
        return _Casts.uncheckedCast(mixedInElseSource);
    }

    /**
     *
     * {@index}
     */
    public enum Phase {
        HIDE,
        DISABLE,
        VALIDATE,
        EXECUTING,
        EXECUTED;

        /**
         * The significance being that at this point the proposed values/arguments are known, and so the event can be
         * fully populated.
         */
        public boolean isValidatingOrLater() {
            return this == VALIDATE || isExecutingOrLater();
        }

        /**
         * When the {@link org.apache.causeway.applib.services.command.Command} is made available on the
         * {@link org.apache.causeway.applib.events.domain.ActionDomainEvent} via {@link org.apache.causeway.applib.services.iactn.Interaction#getCommand()}.
         */
        public boolean isExecutingOrLater() {
            return isExecuting() || isExecuted();
        }

        public boolean isExecuting() {
            return this == EXECUTING;
        }

        public boolean isExecuted() {
            return this == EXECUTED;
        }
    }

    /**
     * Whether the framework is checking visibility, enablement, validity or actually executing (invoking action,
     * updating property or collection).
     */
    @Getter
    private Phase eventPhase;

    /**
     * Not API, set by the framework.
     */
    public final void setEventPhase(final Phase phase) {
        this.eventPhase = phase;
    }

    /**
     * Identifier of the action, property or collection being interacted with.
     *
     */
    @Getter
    private Identifier identifier;

    /**
     * Not API, set by the framework if the no-arg constructor is used.
     */
    public final void setIdentifier(final Identifier identifier) {
        this.identifier = identifier;
    }

    // -- HIDING

    /**
     * Used by the framework to determine if the member should be hidden (not
     * rendered).
     */
    @Getter
    private boolean hidden;

    /**
     * API for subscribers to hide the member.
     *
     * @see #veto(String, Object...)
     */
    public final void hide() {
        this.hidden = true;
    }

    /**
     * Same as ... {@code if(condition) hide(); }
     * @see #hide()
     */
    public void hideIf(final boolean condition) {
        if(condition) {
            hide();
        }
    }

    /**
     * Same as ... {@code if(shouldHide.getAsBoolean()) hide(); }
     * @see #hide()
     */
    public void hideIf(final @NonNull BooleanSupplier shouldHide) {
        if(shouldHide.getAsBoolean()) {
            hide();
        }
    }

    // -- DISABLING

    /**
     * If {@link #isDisabled() disabled}, then either this method returns
     * non-null or {@link #getDisabledReasonTranslatable()} will.
     */
    @Getter
    private String disabledReason;

    /**
     * If {@link #isDisabled() disabled}, then either this method returns non-null or {@link #getDisabledReason()} will.
     */
    @Getter
    private TranslatableString disabledReasonTranslatable;

    public boolean isDisabled() {
        return disabledReason != null || disabledReasonTranslatable != null;
    }

    /**
     * API for subscribers to disable the member, specifying the reason why.
     *
     * @see #disable(org.apache.causeway.applib.services.i18n.TranslatableString)
     * @see #veto(String, Object...)
     */
    public final void disable(final String reason) {
        this.disabledReason = reason;
    }

    /**
     * API for subscribers to disable the member, specifying the reason why as
     * a {@link TranslatableString}.
     *
     * @see #disable(java.lang.String)
     * @see #veto(org.apache.causeway.applib.services.i18n.TranslatableString)
     */
    public final void disable(final TranslatableString reason) {
        this.disabledReasonTranslatable = reason;
    }

    /**
     * Same as ... {@code if(reasonSupplier.get()!=null) disable(reasonSupplier.get()); }
     * @see #disable(String)
     */
    public void disableIfReason(final @NonNull Supplier<String> reasonSupplier) {
        Optional
            .ofNullable(reasonSupplier.get())
            .ifPresent(this::disable);
    }

    /**
     * Same as ... {@code if(reasonSupplier.get()!=null) disable(reasonSupplier.get()); }
     * @see #disable(TranslatableString)
     */
    public void disableIfTranslatableReason(final @NonNull Supplier<TranslatableString> reasonSupplier) {
        Optional
            .ofNullable(reasonSupplier.get())
            .ifPresent(this::disable);
    }

    // -- INVALIDATING

    /**
     * Used by the framework to determine whether the interaction is invalid
     * and should be blocked (eg pressing OK shows message).
     *
     * <p>
     * If {@link #isInvalid() invalid}, then either this method returns
     * non-null or {@link #getInvalidityReasonTranslatable()} will.
     * </p>
     */
    @Getter
    private String invalidityReason;

    /**
     * Used by the framework to determine whether the interaction is invalid
     * and should be blocked (eg pressing OK shows message).
     *
     * <p>
     * If {@link #isInvalid() invalid}, then either this method returns non-null
     * or {@link #getInvalidityReason()} will.
     * </p>
     */
    @Getter
    private TranslatableString invalidityReasonTranslatable;

    public boolean isInvalid() {
        return invalidityReason != null || invalidityReasonTranslatable != null;
    }

    /**
     * API for subscribers to invalidate an interaction, eg invalid arguments
     * to an action.
     *
     * @see #invalidate(org.apache.causeway.applib.services.i18n.TranslatableString)
     * @see #veto(String, Object...)
     */
    public void invalidate(final String reason) {
        this.invalidityReason = reason;
    }

    /**
     * API for subscribers to invalidate an interaction, specifying the reason
     * as a {@link TranslatableString}.
     *
     * @see #invalidate(String)
     * @see #veto(org.apache.causeway.applib.services.i18n.TranslatableString)
     */
    public void invalidate(final TranslatableString reason) {
        this.invalidityReasonTranslatable = reason;
    }

    /**
     * Same as ... {@code if(reasonSupplier.get()!=null) invalidate(reasonSupplier.get()); }
     * @see #invalidate(String)
     */
    public void invalidateIfReason(final @NonNull Supplier<String> reasonSupplier) {
        Optional
            .ofNullable(reasonSupplier.get())
            .ifPresent(this::invalidate);
    }

    /**
     * Same as ... {@code if(reasonSupplier.get()!=null) invalidate(reasonSupplier.get()); }
     * @see #invalidate(TranslatableString)
     */
    public void invalidateIfTranslatableReason(final @NonNull Supplier<TranslatableString> reasonSupplier) {
        Optional
            .ofNullable(reasonSupplier.get())
            .ifPresent(this::invalidate);
    }

    // -- VETOING

    /**
     * Use instead of {@link #hide()}, {@link #disable(String)} and
     * {@link #invalidate(String)}; just delegates to
     * appropriate vetoing method based upon the {@link #getEventPhase() phase}.
     *
     * <p>
     *     If hiding, just pass <tt>null</tt> for the parameter.
     * </p>
     *
     * @param reason - reason why the interaction is being invalidated (ignored
     *              if in {@link org.apache.causeway.applib.events.domain.AbstractDomainEvent.Phase#HIDE hide} phase).
     * @param args
     *
     * @see #veto(org.apache.causeway.applib.services.i18n.TranslatableString)
     */
    public void veto(final String reason, final Object... args) {

        switch (getEventPhase()) {
        case HIDE:
            hide();
            break;
        case DISABLE:
            if(reason == null) {
                throw new IllegalArgumentException("Reason must be non-null");
            }
            disable(String.format(reason, args));
            break;
        case VALIDATE:
            if(reason == null) {
                throw new IllegalArgumentException("Reason must be non-null");
            }
            invalidate(String.format(reason, args));
            break;
        case EXECUTED:
        case EXECUTING:
            break;
        default:
            throw _Exceptions.unmatchedCase(getEventPhase());
        }
    }

    /**
     * Use instead of {@link #hide()}, {@link #disable(org.apache.causeway.applib.services.i18n.TranslatableString)} and {@link #invalidate(org.apache.causeway.applib.services.i18n.TranslatableString)}; just delegates to
     * appropriate vetoing method based upon the {@link #getEventPhase() phase}.
     *
     * <p>
     *     If hiding, just pass <tt>null</tt> for the parameter.
     * </p>
     *
     * @param translatableReason - reason why the interaction is being invalidated (ignored if in {@link org.apache.causeway.applib.events.domain.AbstractDomainEvent.Phase#HIDE hide} phase).
     *
     * @see #veto(String, Object...)
     */
    public void veto(final TranslatableString translatableReason) {

        switch (getEventPhase()) {
        case HIDE:
            hide();
            break;
        case DISABLE:
            disable(translatableReason);
            break;
        case VALIDATE:
            invalidate(translatableReason);
            break;
        case EXECUTED:
        case EXECUTING:
            break;
        default:
            throw _Exceptions.unmatchedCase(getEventPhase());
        }
    }

    // -- USER DATA

    /**
     * Provides a mechanism to pass data to the next {@link #getEventPhase() phase}.
     */
    private final Map<Object, Object> userData = new HashMap<>();

    /**
     * Obtain user-data, as set by a previous {@link #getEventPhase() phase}.
     */
    public Object get(final Object key) {
        return userData.get(key);
    }

    /**
     * Mechanism to allow subscribers to share arbitrary information between
     * phases. One event instance is used for both the hide and disable phases,
     * and a different event instance is shared between validate/pre-execute/post-execute.
     *
     * Set user-data, for the use of a subsequent {@link #getEventPhase() phase}.
     */
    public final void put(final Object key, final Object value) {
        userData.put(key, value);
    }

    private static final ToString<AbstractDomainEvent<?>> toString =
            ObjectContracts.<AbstractDomainEvent<?>>
    toString("source", AbstractDomainEvent::getSource)
    .thenToString("identifier", AbstractDomainEvent::getIdentifier)
    .thenToString("eventPhase", AbstractDomainEvent::getEventPhase)
    ;

    @Override
    public String toString() {
        return toString.toString(this);
    }

}
