/**
 *  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.isis.core.runtime.services.command;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.annotation.PostConstruct;

import com.google.common.collect.Lists;

import org.apache.isis.applib.annotation.DomainService;
import org.apache.isis.applib.annotation.NatureOfService;
import org.apache.isis.applib.annotation.Programmatic;
import org.apache.isis.applib.services.background.ActionInvocationMemento;
import org.apache.isis.applib.services.bookmark.Bookmark;
import org.apache.isis.applib.services.bookmark.BookmarkService;
import org.apache.isis.applib.services.command.Command;
import org.apache.isis.applib.services.command.CommandContext;
import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager;
import org.apache.isis.core.metamodel.adapter.oid.RootOid;
import org.apache.isis.core.metamodel.facets.actions.action.invocation.CommandUtil;
import org.apache.isis.core.metamodel.services.command.CommandDtoServiceInternal;
import org.apache.isis.core.metamodel.spec.ObjectSpecification;
import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
import org.apache.isis.core.metamodel.spec.feature.ObjectActionParameter;
import org.apache.isis.core.metamodel.spec.feature.ObjectMember;
import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
import org.apache.isis.core.metamodel.specloader.SpecificationLoader;
import org.apache.isis.core.metamodel.specloader.specimpl.dflt.ObjectSpecificationDefault;
import org.apache.isis.core.runtime.services.memento.MementoServiceDefault;
import org.apache.isis.core.runtime.system.session.IsisSessionFactory;
import org.apache.isis.schema.cmd.v1.ActionDto;
import org.apache.isis.schema.cmd.v1.CommandDto;
import org.apache.isis.schema.cmd.v1.ParamDto;
import org.apache.isis.schema.cmd.v1.ParamsDto;
import org.apache.isis.schema.cmd.v1.PropertyDto;
import org.apache.isis.schema.common.v1.InteractionType;
import org.apache.isis.schema.common.v1.OidsDto;
import org.apache.isis.schema.common.v1.ValueWithTypeDto;
import org.apache.isis.schema.utils.CommandDtoUtils;
import org.apache.isis.schema.utils.CommonDtoUtils;

@DomainService(
        nature = NatureOfService.DOMAIN,
        menuOrder = "" + Integer.MAX_VALUE
)
public class CommandDtoServiceInternalDefault implements CommandDtoServiceInternal {

    private final MementoServiceDefault mementoService;

    public CommandDtoServiceInternalDefault() {
        this(new MementoServiceDefault());
    }

    CommandDtoServiceInternalDefault(MementoServiceDefault mementoService) {
        this.mementoService = mementoService.withNoEncoding();
    }
    
    // //////////////////////////////////////

    
    @Programmatic
    @PostConstruct
    public void init(Map<String,String> props) {
    }

    // //////////////////////////////////////


    private ObjectSpecificationDefault getJavaSpecificationOfOwningClass(final Method method) {
        return getJavaSpecification(method.getDeclaringClass());
    }

    private ObjectSpecificationDefault getJavaSpecification(final Class<?> cls) {
        final ObjectSpecification objectSpec = getSpecification(cls);
        if (!(objectSpec instanceof ObjectSpecificationDefault)) {
            throw new UnsupportedOperationException(
                "Only Java is supported "
                + "(specification is '" + objectSpec.getClass().getCanonicalName() + "')");
        }
        return (ObjectSpecificationDefault) objectSpec;
    }

    private ObjectSpecification getSpecification(final Class<?> type) {
        return specificationLoader.loadSpecification(type);
    }


    // //////////////////////////////////////

    @Deprecated
    @Programmatic
    @Override
    public ActionInvocationMemento asActionInvocationMemento(
            final Method method,
            final Object domainObject,
            final Object[] args) {
        
        final ObjectSpecificationDefault targetObjSpec = getJavaSpecificationOfOwningClass(method);
        final ObjectMember member = targetObjSpec.getMember(method);
        if(member == null) {
            return null;
        }

        if(!(member instanceof ObjectAction)) {
            throw new UnsupportedOperationException(String.format(
                    "Method %s does not correspond to an action.", method.getName()));
        }

        final ObjectAction action = (ObjectAction) member;
        final String actionIdentifier = CommandUtil.memberIdentifierFor(action);
        
        final Bookmark domainObjectBookmark = bookmarkService.bookmarkFor(domainObject);

        final List<Class<?>> argTypes = Lists.newArrayList();
        final List<Object> argObjs = Lists.newArrayList();
        CommandUtil.buildMementoArgLists(mementoService, bookmarkService, method, args, argTypes, argObjs);

        final ActionInvocationMemento aim = 
                new ActionInvocationMemento(mementoService, 
                        actionIdentifier, 
                        domainObjectBookmark,
                        argTypes,
                        argObjs);
       
        return aim;
    }

    @Override
    public CommandDto asCommandDto(
            final List<ObjectAdapter> targetAdapters,
            final ObjectAction objectAction,
            final ObjectAdapter[] argAdapters) {

        final CommandDto dto = asCommandDto(targetAdapters);

        final ActionDto actionDto = new ActionDto();
        actionDto.setInteractionType(InteractionType.ACTION_INVOCATION);
        dto.setMember(actionDto);

        addActionArgs(objectAction, actionDto, argAdapters);

        return dto;
    }

    @Override
    public CommandDto asCommandDto(
            final List<ObjectAdapter> targetAdapters,
            final OneToOneAssociation property,
            final ObjectAdapter valueAdapterOrNull) {

        final CommandDto dto = asCommandDto(targetAdapters);

        final PropertyDto propertyDto = new PropertyDto();
        propertyDto.setInteractionType(InteractionType.PROPERTY_EDIT);
        dto.setMember(propertyDto);

        addPropertyValue(property, propertyDto, valueAdapterOrNull);

        return dto;
    }

    private CommandDto asCommandDto(final List<ObjectAdapter> targetAdapters) {
        final CommandDto dto = new CommandDto();
        dto.setMajorVersion("1");
        dto.setMinorVersion("0");

        String transactionId = determineTransactionId().toString();
        dto.setTransactionId(transactionId);

        for (ObjectAdapter targetAdapter : targetAdapters) {
            final RootOid rootOid = (RootOid) targetAdapter.getOid();
            final Bookmark bookmark = rootOid.asBookmark();
            final OidsDto targets = CommandDtoUtils.targetsFor(dto);
            targets.getOid().add(bookmark.toOidDto());
        }
        return dto;
    }

    protected UUID determineTransactionId() {
        Command command = commandContext.getCommand();
        if (command != null && command.getTransactionId() != null) {
            return command.getTransactionId();
        } else {
            return UUID.randomUUID();
        }
    }

    @Override
    public void addActionArgs(
            final ObjectAction objectAction,
            final ActionDto actionDto,
            final ObjectAdapter[] argAdapters) {
        final String actionId = CommandUtil.memberIdentifierFor(objectAction);
        final ObjectSpecification onType = objectAction.getOnType();
        final String objectType = onType.getSpecId().asString();
        final String localId = objectAction.getIdentifier().toNameIdentityString();
        actionDto.setLogicalMemberIdentifier(objectType + "#" + localId);
        actionDto.setMemberIdentifier(actionId);

        List<ObjectActionParameter> actionParameters = objectAction.getParameters();
        for (int paramNum = 0; paramNum < actionParameters.size(); paramNum++) {
            final ObjectActionParameter actionParameter = actionParameters.get(paramNum);
            final String parameterName = actionParameter.getName();
            final Class<?> paramType = actionParameter.getSpecification().getCorrespondingClass();
            final ObjectAdapter argAdapter = argAdapters[paramNum];
            final Object arg = argAdapter != null? argAdapter.getObject(): null;
            final ParamsDto parameters = CommandDtoUtils.parametersFor(actionDto);
            final List<ParamDto> parameterList = parameters.getParameter();

            ParamDto paramDto = CommonDtoUtils.newParamDto(
                    parameterName, paramType, arg, bookmarkService);
            parameterList.add(paramDto);
        }
    }

    @Override
    public void addPropertyValue(
            final OneToOneAssociation property,
            final PropertyDto propertyDto,
            final ObjectAdapter valueAdapter) {

        final String actionIdentifier = CommandUtil.memberIdentifierFor(property);
        final ObjectSpecification onType = property.getOnType();
        final String objectType = onType.getSpecId().asString();
        final String localId = property.getIdentifier().toNameIdentityString();
        propertyDto.setLogicalMemberIdentifier(objectType + "#" + localId);
        propertyDto.setMemberIdentifier(actionIdentifier);

        final ObjectSpecification valueSpec = property.getSpecification();
        final Class<?> valueType = valueSpec.getCorrespondingClass();

        final ValueWithTypeDto newValue = CommonDtoUtils.newValueWithTypeDto(
                valueType, ObjectAdapter.Util.unwrap(valueAdapter), bookmarkService);
        propertyDto.setNewValue(newValue);
    }

    // //////////////////////////////////////

    //region > injected services
    @javax.inject.Inject
    CommandContext commandContext;

    @javax.inject.Inject
    private BookmarkService bookmarkService;

    @javax.inject.Inject
    SpecificationLoader specificationLoader;

    @javax.inject.Inject
    IsisSessionFactory isisSessionFactory;

    protected AdapterManager getAdapterManager() {
        return isisSessionFactory.getCurrentSession().getPersistenceSession();
    }

    //endregion


}
