/*
 *  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.facets.object.viewmodel;

import java.lang.reflect.Constructor;
import java.lang.reflect.RecordComponent;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Stream;

import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.internal.memento._Mementos;
import org.apache.causeway.commons.internal.memento._Mementos.SerializingAdapter;
import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
import org.apache.causeway.core.metamodel.object.ManagedObject;
import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation;

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

/**
 * @since 3.0.0
 */
public class ViewModelFacetForJavaRecord
extends ViewModelFacetAbstract {

    public static Optional<ViewModelFacetForJavaRecord> create(
            final Class<?> cls,
            final FacetHolder holder) {
        return cls.isRecord()
                ? Optional.of(new ViewModelFacetForJavaRecord(cls, holder))
                : Optional.empty();
    }

    private UrlEncodingService codec;
    private SerializingAdapter serializer;

    private final Constructor<?> canonicalConstructor;

    protected ViewModelFacetForJavaRecord(
            final Class<?> recordClass,
            final FacetHolder holder) {
        // is overruled by ViewModel interface semantics
        super(holder, Precedence.DEFAULT);
        this.canonicalConstructor = canonicalConstructor(recordClass);
    }

    @Override @SneakyThrows
    protected ManagedObject createViewmodel(
            final @NonNull ObjectSpecification viewmodelSpec,
            final @NonNull Bookmark bookmark) {

        var memento = parseMemento(bookmark);

        var recordComponentPojos = streamRecordComponents(viewmodelSpec)
        .map(association->{
            var associationId = association.getId();
            var elementType = association.getElementType();
            var elementClass = elementType.getCorrespondingClass();
            var associationPojo = association.isProperty()
                    ? memento.get(associationId, elementClass)
                    //TODO collection values not yet supported by memento (as workaround use Serializable record)
                    : null;
            return associationPojo;
        }).toArray();

        return ManagedObject.viewmodel(viewmodelSpec,
                canonicalConstructor.newInstance(recordComponentPojos),
                Optional.of(bookmark));
    }

    @Override
    public String serialize(final ManagedObject viewModel) {

        final _Mementos.Memento memento = newMemento();

        var viewmodelSpec = viewModel.objSpec();

        streamRecordComponents(viewmodelSpec)
        .forEach(association->{

            final ManagedObject associationValue =
                    association.get(viewModel, InteractionInitiatedBy.PASS_THROUGH);

            if(association != null
                    //TODO collection values not yet supported by memento (as workaround use Serializable record)
                    && association.isProperty()
                    && associationValue.getPojo()!=null) {
                memento.put(association.getId(), associationValue.getPojo());
            }
        });

        return memento.asString();
    }

    // -- HELPER

    private Can<ObjectAssociation> recordComponentsAsAssociations;
    private Stream<ObjectAssociation> streamRecordComponents(
            final @NonNull ObjectSpecification viewmodelSpec) {
        if(recordComponentsAsAssociations==null) {
            this.recordComponentsAsAssociations = recordComponentsAsAssociations(viewmodelSpec);
        }
        return recordComponentsAsAssociations.stream();
    }

    private void initDependencies() {
        var serviceRegistry = getServiceRegistry();
        this.codec = serviceRegistry.lookupServiceElseFail(UrlEncodingService.class);
        this.serializer = serviceRegistry.lookupServiceElseFail(SerializingAdapter.class);
    }

    private void ensureDependenciesInited() {
        if(codec==null) {
            initDependencies();
        }
    }

    private _Mementos.Memento newMemento() {
        ensureDependenciesInited();
        return _Mementos.create(codec, serializer);
    }

    private _Mementos.Memento parseMemento(final Bookmark bookmark) {
        ensureDependenciesInited();
        return _Mementos.parse(codec, serializer, bookmark.identifier());
    }

    private static Can<ObjectAssociation> recordComponentsAsAssociations(
            final @NonNull ObjectSpecification viewmodelSpec) {
        return Arrays.stream(viewmodelSpec.getCorrespondingClass().getRecordComponents())
                .map(RecordComponent::getName)
                .map(memberId->viewmodelSpec.getAssociationElseFail(memberId, MixedIn.EXCLUDED))
                .collect(Can.toCan());
    }

    @SneakyThrows
    private static <T> Constructor<T> canonicalConstructor(final @NonNull Class<T> recordClass) {
        var constructorParamTypes = Arrays.stream(recordClass.getRecordComponents())
                .map(RecordComponent::getType)
                .toArray(Class[]::new);
        return recordClass.getDeclaredConstructor(constructorParamTypes);
    }

}
