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

import java.util.Optional;
import java.util.stream.Stream;

import org.apache.wicket.core.request.handler.IPageRequestHandler;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.cycle.PageRequestHandlerTracker;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.mapper.parameter.INamedParameters.NamedPair;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.util.string.StringValue;

import org.jspecify.annotations.Nullable;

import org.apache.causeway.applib.Identifier;
import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.internal.base._Strings;
import org.apache.causeway.core.metamodel.context.MetaModelContext;
import org.apache.causeway.core.metamodel.object.ManagedObject;
import org.apache.causeway.core.metamodel.object.ManagedObjects;
import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
import org.apache.causeway.core.metamodel.util.Facets;
import org.apache.causeway.viewer.wicket.model.mementos.PageParameterNames;
import org.apache.causeway.viewer.wicket.model.models.UiObjectWkt;

import org.jspecify.annotations.NonNull;
import lombok.experimental.UtilityClass;

/**
 * A helper class for dealing with PageParameters
 */
@UtilityClass
public class PageParameterUtils {

    /**
     * The name of the special request parameter that controls whether the page header/navigation bar
     * should be shown or not
     */
    public static final String CAUSEWAY_NO_HEADER_PARAMETER_NAME = "causeway.no.header";

    /**
     * The name of the special request parameter that controls whether the page footer
     * should be shown or not
     */
    public static final String CAUSEWAY_NO_FOOTER_PARAMETER_NAME = "causeway.no.footer";

    /**
     * Creates a new instance of PageParameters that preserves some special request parameters
     * which should propagate in all links created by Causeway
     *
     * @return a new PageParameters instance
     */
    public PageParameters newPageParameters() {
        var newPageParameters = new PageParameters();
        var requestCycle = RequestCycle.get();

        if (requestCycle != null) {
            Optional.ofNullable(PageRequestHandlerTracker.getFirstHandler(requestCycle))
            .map(IPageRequestHandler::getPageParameters)
            .ifPresent(currentPageParameters->{
                final StringValue noHeader = currentPageParameters.get(CAUSEWAY_NO_HEADER_PARAMETER_NAME);
                if (!noHeader.isNull()) {
                    newPageParameters.set(CAUSEWAY_NO_HEADER_PARAMETER_NAME, noHeader.toString());
                }
                final StringValue noFooter = currentPageParameters.get(CAUSEWAY_NO_FOOTER_PARAMETER_NAME);
                if (!noFooter.isNull()) {
                    newPageParameters.set(CAUSEWAY_NO_FOOTER_PARAMETER_NAME, noFooter.toString());
                }

            });

        }
        return newPageParameters;
    }

    public Stream<NamedPair> streamCurrentRequestParameters() {
        return Optional.ofNullable(RequestCycle.get()).stream()
        .map(RequestCycle::getRequest)
        .map(Request::getRequestParameters)
        .flatMap(params->
            params.getParameterNames().stream()
            .map(key->new NamedPair(key, params.getParameterValue(key).toString()))
        );
    }

    // -- FACTORY METHODS FOR PAGE PARAMETERS

    public PageParameters createPageParametersForBookmark(final Bookmark bookmark) {
        var pageParameters = PageParameterUtils.newPageParameters();
        PageParameterNames.OBJECT_OID.addStringTo(pageParameters, bookmark.stringify());
        return pageParameters;
    }

    /**
     * Factory method for creating {@link PageParameters} to represent an
     * object.
     */
    public PageParameters createPageParametersForObject(final ManagedObject adapter) {

        var pageParameters = PageParameterUtils.newPageParameters();
        var isEntity = ManagedObjects.isIdentifiable(adapter);

        if (isEntity) {
            ManagedObjects.stringify(adapter)
            .ifPresent(oidStr->PageParameterNames.OBJECT_OID.addStringTo(pageParameters, oidStr));
        } else {
            // don't do anything; instead the page should be redirected back to
            // a DomainObjectPage so that the underlying EntityModel that contains
            // the memento for the transient ObjectAdapter can be accessed.
        }
        return pageParameters;
    }

    public PageParameters createPageParametersForBookmarkablePageLink(
            final ManagedObject adapter) {

        return
                ManagedObjects.isIdentifiable(adapter)
                    && !ManagedObjects.isNullOrUnspecifiedOrEmpty(adapter)
                ? UiObjectWkt.ofAdapter(
                    Facets.projected(adapter))
                    .getPageParametersWithoutUiHints()
                : PageParameterUtils.createPageParametersForObject(null);
    }

    public PageParameters createPageParametersForAction(
            final ManagedObject adapter,
            final ObjectAction objectAction,
            final Can<ManagedObject> paramValues) {

        var pageParameters = createPageParameters(adapter, objectAction);

        // capture argument values
        for(var argumentAdapter: paramValues) {
            var encodedArg = encodeArg(argumentAdapter);
            PageParameterNames.ACTION_ARGS.addStringTo(pageParameters, encodedArg);
        }

        return pageParameters;
    }

    public Optional<Bookmark> toBookmark(final PageParameters pageParameters) {
        var oidStr = PageParameterNames.OBJECT_OID.getStringFrom(pageParameters);
        return _Strings.isEmpty(oidStr)
                ? Optional.empty()
                : Optional.of(Bookmark.parseElseFail(oidStr));
    }

    // -- HELPERS

    private static PageParameters createPageParameters(
            final ManagedObject adapter, final ObjectAction objectAction) {

        var pageParameters = PageParameterUtils.newPageParameters();

        ManagedObjects.stringify(adapter)
        .ifPresent(oidStr->
            PageParameterNames.OBJECT_OID.addStringTo(pageParameters, oidStr));

        var actionScope = objectAction.getScope();
        PageParameterNames.ACTION_TYPE.addEnumTo(pageParameters, actionScope);

        var actionOnTypeSpec = objectAction.getDeclaringType();
        if (actionOnTypeSpec != null) {
            PageParameterNames.ACTION_OWNING_SPEC.addStringTo(pageParameters, actionOnTypeSpec.getFullIdentifier());
        }

        var actionId = determineActionId(objectAction);
        PageParameterNames.ACTION_ID.addStringTo(pageParameters, actionId);

        return pageParameters;
    }

    private static String determineActionId(final ObjectAction objectAction) {
        final Identifier identifier = objectAction.getFeatureIdentifier();
        if (identifier != null) {
            return identifier.getMemberNameAndParameterClassNamesIdentityString();
        }
        // fallback (used for action sets)
        return objectAction.getId();
    }

    private static final String NULL_ARG = "$nullArg$";
    private String encodeArg(final ManagedObject adapter) {
        if(adapter == null) {
            return NULL_ARG;
        }
        return ManagedObjects.stringify(adapter).orElse(null);
    }

    //FIXME don't silently ignore failures
    private @Nullable ManagedObject decodeArg(
            final @NonNull MetaModelContext mmc,
            final ObjectSpecification objSpec,
            final String encoded) {
        if(NULL_ARG.equals(encoded)) {
            return null;
        }
        try {
            return Bookmark.parseUrlEncoded(encoded)
                    .flatMap(mmc.getObjectManager()::loadObject)
                    .orElse(null);
        } catch (final Exception e) {
            e.printStackTrace(); // I suppose useful when in prototyping mode only
            return null;
        }
    }

}
