/*
 *  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.core.metamodel.layout;

import java.util.Comparator;

import org.apache.causeway.applib.annotation.ActionLayout;
import org.apache.causeway.applib.annotation.BookmarkPolicy;
import org.apache.causeway.applib.annotation.LabelPosition;
import org.apache.causeway.applib.annotation.TableDecorator;
import org.apache.causeway.applib.annotation.Where;
import org.apache.causeway.applib.layout.component.ActionLayoutData;
import org.apache.causeway.applib.layout.component.CollectionLayoutData;
import org.apache.causeway.applib.layout.component.DomainObjectLayoutData;
import org.apache.causeway.applib.layout.component.HasBookmarking;
import org.apache.causeway.applib.layout.component.HasCssClass;
import org.apache.causeway.applib.layout.component.HasCssClassFa;
import org.apache.causeway.applib.layout.component.HasDescribedAs;
import org.apache.causeway.applib.layout.component.HasHidden;
import org.apache.causeway.applib.layout.component.HasNamed;
import org.apache.causeway.applib.layout.component.PropertyLayoutData;
import org.apache.causeway.applib.layout.grid.Grid;
import org.apache.causeway.commons.internal.base._Strings;
import org.apache.causeway.commons.internal.functions._Functions;
import org.apache.causeway.core.metamodel.facetapi.Facet;
import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
import org.apache.causeway.core.metamodel.facetapi.FacetUtil;
import org.apache.causeway.core.metamodel.facets.actions.position.ActionPositionFacet;
import org.apache.causeway.core.metamodel.facets.all.described.MemberDescribedFacet;
import org.apache.causeway.core.metamodel.facets.all.described.ObjectDescribedFacet;
import org.apache.causeway.core.metamodel.facets.all.hide.HiddenFacet;
import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacet;
import org.apache.causeway.core.metamodel.facets.all.named.ObjectNamedFacet;
import org.apache.causeway.core.metamodel.facets.collections.collection.defaultview.DefaultViewFacet;
import org.apache.causeway.core.metamodel.facets.collections.sortedby.SortedByFacet;
import org.apache.causeway.core.metamodel.facets.members.cssclass.CssClassFacet;
import org.apache.causeway.core.metamodel.facets.members.iconfa.FaFacet;
import org.apache.causeway.core.metamodel.facets.object.bookmarkpolicy.BookmarkPolicyFacet;
import org.apache.causeway.core.metamodel.facets.object.paged.PagedFacet;
import org.apache.causeway.core.metamodel.facets.object.tabledec.TableDecoratorFacet;
import org.apache.causeway.core.metamodel.facets.objectvalue.daterenderedadjust.DateRenderAdjustFacet;
import org.apache.causeway.core.metamodel.facets.objectvalue.labelat.LabelAtFacet;
import org.apache.causeway.core.metamodel.facets.objectvalue.multiline.MultiLineFacet;
import org.apache.causeway.core.metamodel.facets.objectvalue.typicallen.TypicalLengthFacet;
import org.apache.causeway.core.metamodel.spec.ObjectSpecification;

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

/**
 *
 * @since 2.0
 *
 */
@UtilityClass
public class LayoutFacetUtil {

    public void setBookmarkingIfAny(
            final HasBookmarking hasBookmarking,
            final FacetHolder facetHolder) {

        var bookmarkPolicyFacet = facetHolder.getFacet(BookmarkPolicyFacet.class);
        if(isDoOp(bookmarkPolicyFacet)) {
            final BookmarkPolicy bookmarking = bookmarkPolicyFacet.value();
            if(bookmarking != null) {
                hasBookmarking.setBookmarking(bookmarking);
            }
        }
    }

    public void setCssClassIfAny(
            final HasCssClass hasCssClass,
            final FacetHolder facetHolder) {

        var cssClassFacet = facetHolder.getFacet(CssClassFacet.class);
        if(isDoOp(cssClassFacet)) {
            try {
                // try...finally because CSS class may vary by object, and we pass in only null
                final String cssClass = cssClassFacet.cssClass(null);
                if(!_Strings.isNullOrEmpty(cssClass)) {
                    hasCssClass.setCssClass(cssClass);
                }
            } catch(Exception ignore) {
                // ignore
            }
        }
    }

    public void setCssClassFaIfAny(
            final HasCssClassFa hasCssClassFa,
            final FacetHolder facetHolder) {

        facetHolder.lookupNonFallbackFacet(FaFacet.class)
        .map(FaFacet::getSpecialization)
        .ifPresent(specialization->
            specialization.accept(
                    faStaticFacet->{
                        final String cssClassFa = faStaticFacet.getLayers().toQuickNotation();
                        if(!_Strings.isNullOrEmpty(cssClassFa)) {
                            hasCssClassFa.setCssClassFa(cssClassFa);
                            hasCssClassFa.setCssClassFaPosition(faStaticFacet.getLayers().position());
                        }
                    },
                    _Functions.noopConsumer())); // not supported for imperative fa-icons
    }

    public void setDefaultViewIfAny(
            final CollectionLayoutData collectionLayoutData,
            final FacetHolder facetHolder) {

        var defaultViewFacet = facetHolder.getFacet(DefaultViewFacet.class);
        if(isDoOp(defaultViewFacet)) {
            final String defaultView = defaultViewFacet.value();
            if(_Strings.isNotEmpty(defaultView)) {
                collectionLayoutData.setDefaultView(defaultView);
            }
        }
    }

    private void setObjectNamedIfAny(
            final HasNamed hasNamed,
            final FacetHolder facetHolder) {

        facetHolder.lookupNonFallbackFacet(ObjectNamedFacet.class)
        .filter(ObjectNamedFacet::isNounPresent)
        .map(ObjectNamedFacet::singularTranslated)
        .ifPresent(hasNamed::setNamed);
    }

    private void setObjectDescribedIfAny(
            final HasDescribedAs hasDescribedAs,
            final FacetHolder facetHolder) {

        facetHolder.lookupNonFallbackFacet(ObjectDescribedFacet.class)
        .map(ObjectDescribedFacet::translated)
        .filter(_Strings::isNotEmpty)
        .ifPresent(hasDescribedAs::setDescribedAs);
    }

    public void setPagedIfAny(
            final DomainObjectLayoutData domainObjectLayoutData,
            final FacetHolder facetHolder) {

        var pagedFacet = FacetUtil.lookupFacetIn(PagedFacet.class, facetHolder).orElse(null);
        if(isDoOp(pagedFacet)) {
            final int value = pagedFacet.value();
            if(value > 0) {
                domainObjectLayoutData.setPaged(value);
            }
        }
    }

    public void setTableDecoratorIfAny(
            final DomainObjectLayoutData domainObjectLayoutData,
            final FacetHolder facetHolder) {

        var tableDecoratorFacet = FacetUtil.lookupFacetIn(TableDecoratorFacet.class, facetHolder).orElse(null);
        if(isDoOp(tableDecoratorFacet)) {
            final Class<? extends TableDecorator> value = tableDecoratorFacet.value();
            if(value != TableDecorator.Default.class) {
                domainObjectLayoutData.setTableDecorator(value);
            }
        }
    }

    private void setMemberNamedIfAny(
            final HasNamed hasNamed,
            final FacetHolder facetHolder) {

        facetHolder.lookupNonFallbackFacet(MemberNamedFacet.class)
        .map(MemberNamedFacet::getSpecialization)
        .ifPresent(specialization->
            specialization.accept(
                    hasStaticText->{
                        var describedAs = hasStaticText.translated();
                        if(_Strings.isNotEmpty(describedAs)) {
                            hasNamed.setNamed(describedAs);
                        }
                    },
                    _Functions.noopConsumer())); // not supported for imperative text
    }

    private void setMemberDescribedIfAny(
            final HasDescribedAs hasDescribedAs,
            final FacetHolder facetHolder) {

        facetHolder.lookupNonFallbackFacet(MemberDescribedFacet.class)
        .map(MemberDescribedFacet::getSpecialization)
        .ifPresent(specialization->
            specialization.accept(
                    hasStaticText->{
                        var describedAs = hasStaticText.translated();
                        if(_Strings.isNotEmpty(describedAs)) {
                            hasDescribedAs.setDescribedAs(describedAs);
                        }
                    },
                    _Functions.noopConsumer())); // not supported for imperative text
    }

    public void setHiddenIfAny(
            final HasHidden hasHidden,
            final FacetHolder facetHolder) {

        var hiddenFacet = facetHolder.getFacet(HiddenFacet.class);
        if (isDoOp(hiddenFacet)) {
            final Where where = hiddenFacet.where();
            if(where != null) {
                hasHidden.setHidden(where);
            }
        }
    }

    public void setLabelPositionIfAny(
            final PropertyLayoutData propertyLayoutData,
            final FacetHolder facetHolder) {

        var labelAtFacet = facetHolder.getFacet(LabelAtFacet.class);
        if(isDoOp(labelAtFacet)) {
            final LabelPosition labelPosition = labelAtFacet.label();
            if(labelPosition != null) {
                propertyLayoutData.setLabelPosition(labelPosition);
            }
        }
    }

    public void setMultiLineIfAny(
            final PropertyLayoutData propertyLayoutData,
            final FacetHolder facetHolder) {

        var multiLineFacet = facetHolder.getFacet(MultiLineFacet.class);
        if(isDoOp(multiLineFacet)) {
            final int numberOfLines = multiLineFacet.numberOfLines();
            if(numberOfLines > 0) {
                propertyLayoutData.setMultiLine(numberOfLines);
            }
        }
    }

    public void setPagedIfAny(
            final CollectionLayoutData collectionLayoutData,
            final FacetHolder facetHolder, final ObjectSpecification objectSpec) {

        var pagedFacet = FacetUtil.lookupFacetIn(PagedFacet.class, facetHolder, objectSpec).orElse(null);
        if(isDoOp(pagedFacet)) {
            final int value = pagedFacet.value();
            if(value > 0) {
                collectionLayoutData.setPaged(value);
            }
        }
    }

    public void setTableDecoratorIfAny(
            final CollectionLayoutData collectionLayoutData,
            final FacetHolder facetHolder, final ObjectSpecification objectSpec) {

        var tableDecoratorFacet = FacetUtil.lookupFacetIn(TableDecoratorFacet.class, facetHolder, objectSpec).orElse(null);
        if(isDoOp(tableDecoratorFacet)) {
            final Class<? extends TableDecorator> value = tableDecoratorFacet.value();
            if(value != TableDecorator.Default.class) {
                collectionLayoutData.setTableDecorator(value);
            }
        }
    }

    public void setActionPositionIfAny(
            final ActionLayoutData actionLayoutData,
            final FacetHolder facetHolder) {

        var actionPositionFacet = facetHolder.getFacet(ActionPositionFacet.class);
        if(isDoOp(actionPositionFacet)) {
            final ActionLayout.Position position = actionPositionFacet.position();
            if(position != null) {
                actionLayoutData.setPosition(position);
            }
        }
    }

    public void setRenderedAsDayBeforeIfAny(
            final PropertyLayoutData propertyLayoutData,
            final FacetHolder facetHolder) {

        facetHolder.lookupNonFallbackFacet(DateRenderAdjustFacet.class)
        .ifPresent(dateRenderAdjustFacet->
            propertyLayoutData.setDateRenderAdjustDays(dateRenderAdjustFacet.getDateRenderAdjustDays()));
    }

    public void setSortedByIfAny(
            final CollectionLayoutData collectionLayoutData,
            final FacetHolder facetHolder) {

        var sortedByFacet = facetHolder.getFacet(SortedByFacet.class);
        if(isDoOp(sortedByFacet)) {
            final Class<? extends Comparator<?>> cls = sortedByFacet.value();
            if(cls != null
                    && cls.getCanonicalName()!=null) {
                collectionLayoutData.setSortedBy(cls.getName());
            }
        }
    }

    public void setTypicalLengthIfAny(
            final PropertyLayoutData propertyLayoutData,
            final FacetHolder facetHolder) {

        var typicalLengthFacet = facetHolder.getFacet(TypicalLengthFacet.class);
        if(isDoOp(typicalLengthFacet)) {
            final int typicalLength = typicalLengthFacet.value();
            if(typicalLength > 0) {
                propertyLayoutData.setTypicalLength(typicalLength);
            }
        }
    }

    public static class LayoutDataFactory {

        private final MetamodelToGridOverridingVisitor helper;

        public static LayoutDataFactory of(final ObjectSpecification objectSpec) {
            return new LayoutDataFactory(objectSpec);
        }

        private LayoutDataFactory(final ObjectSpecification objectSpec) {
            this.helper = MetamodelToGridOverridingVisitor.of(objectSpec);
        }

        public ActionLayoutData createActionLayoutData(final String id) {
            var layoutData = new ActionLayoutData(id);
            helper.visit(layoutData);
            return layoutData;
        }

        public CollectionLayoutData createCollectionLayoutData(final String id) {
            var layoutData = new CollectionLayoutData(id);
            helper.visit(layoutData);
            return layoutData;
        }

        public PropertyLayoutData createPropertyLayoutData(final String id) {
            var layoutData = new PropertyLayoutData(id);
            helper.visit(layoutData);
            return layoutData;
        }

        public DomainObjectLayoutData createDomainObjectLayoutData() {
            var layoutData = new DomainObjectLayoutData();
            helper.visit(layoutData);
            return layoutData;
        }

    }

    @RequiredArgsConstructor(staticName = "of")
    public static class MetamodelToGridOverridingVisitor extends Grid.VisitorAdapter  {

        private final @NonNull ObjectSpecification objectSpec;

        @Override
        public void visit(final ActionLayoutData actionLayoutData) {
            objectSpec.getAction(actionLayoutData.getId())
            .ifPresent(objectAction->{
                setCssClassIfAny(actionLayoutData, objectAction);
                setCssClassFaIfAny(actionLayoutData, objectAction);
                setMemberDescribedIfAny(actionLayoutData, objectAction);
                setHiddenIfAny(actionLayoutData, objectAction);
                setMemberNamedIfAny(actionLayoutData, objectAction);
                setActionPositionIfAny(actionLayoutData, objectAction);
            });
        }

        @Override
        public void visit(final CollectionLayoutData collectionLayoutData) {
            objectSpec.getAssociation(collectionLayoutData.getId())
            .ifPresent(collection->{
                setCssClassIfAny(collectionLayoutData, collection);
                setDefaultViewIfAny(collectionLayoutData, collection);
                setMemberDescribedIfAny(collectionLayoutData, collection);
                setHiddenIfAny(collectionLayoutData, collection);
                setMemberNamedIfAny(collectionLayoutData, collection);
                setPagedIfAny(collectionLayoutData, collection, objectSpec);
                setTableDecoratorIfAny(collectionLayoutData, collection, objectSpec);
                setSortedByIfAny(collectionLayoutData, collection);
            });
        }

        @Override
        public void visit(final PropertyLayoutData propertyLayoutData) {
            objectSpec.getAssociation(propertyLayoutData.getId())
            .ifPresent(property->{
                setCssClassIfAny(propertyLayoutData, property);
                setMemberDescribedIfAny(propertyLayoutData, property);
                setHiddenIfAny(propertyLayoutData, property);
                setMemberNamedIfAny(propertyLayoutData, property);
                setLabelPositionIfAny(propertyLayoutData, property);
                setMultiLineIfAny(propertyLayoutData, property);
                setRenderedAsDayBeforeIfAny(propertyLayoutData, property);
                setTypicalLengthIfAny(propertyLayoutData, property);
            });
        }

        @Override
        public void visit(final DomainObjectLayoutData domainObjectLayoutData) {
            setBookmarkingIfAny(domainObjectLayoutData, objectSpec);
            setCssClassIfAny(domainObjectLayoutData, objectSpec);
            setCssClassFaIfAny(domainObjectLayoutData, objectSpec);
            setObjectDescribedIfAny(domainObjectLayoutData, objectSpec);
            setObjectNamedIfAny(domainObjectLayoutData, objectSpec);
            setPagedIfAny(domainObjectLayoutData, objectSpec);
        }
    }

    // -- HELPER

    private static boolean isDoOp(final Facet facet) {
        return facet != null
                && !facet.getPrecedence().isFallback();
    }

}
