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

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

import com.google.common.base.Function;
import com.google.common.collect.Lists;

import org.apache.isis.applib.annotation.Where;
import org.apache.isis.core.commons.lang.ClassExtensions;
import org.apache.isis.core.commons.lang.ListExtensions;
import org.apache.isis.core.commons.lang.MethodExtensions;
import org.apache.isis.core.commons.lang.MethodUtil;
import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager;
import org.apache.isis.core.metamodel.adapter.oid.ParentedCollectionOid;
import org.apache.isis.core.metamodel.adapter.oid.Oid;
import org.apache.isis.core.metamodel.adapter.version.ConcurrencyException;
import org.apache.isis.core.metamodel.adapter.version.Version;
import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
import org.apache.isis.core.metamodel.consent.InteractionResult;
import org.apache.isis.core.metamodel.facets.collections.modify.CollectionFacet;
import org.apache.isis.core.metamodel.facets.object.title.TitleFacet;
import org.apache.isis.core.metamodel.interactions.InteractionUtils;
import org.apache.isis.core.metamodel.interactions.ObjectVisibilityContext;
import org.apache.isis.core.metamodel.interactions.VisibilityContext;
import org.apache.isis.core.metamodel.spec.ElementSpecificationProvider;
import org.apache.isis.core.metamodel.spec.Instance;
import org.apache.isis.core.metamodel.spec.ObjectSpecification;
import org.apache.isis.core.metamodel.spec.Specification;

/**
 * Adapters to domain objects, where the application is written in terms of
 * domain objects and those objects are represented within the NOF through these
 * adapter, and not directly.
 */
public interface ObjectAdapter extends Instance, org.apache.isis.applib.annotation.When.Persistable {

    /**
     * Refines {@link Instance#getSpecification()}.
     */
    @Override
    ObjectSpecification getSpecification();

    /**
     * Returns the adapted domain object, the POJO, that this adapter represents
     * with the framework.
     */
    Object getObject();

    /**
     * Returns the title to display this object with, usually obtained from
     * the wrapped {@link #getObject() domain object}.
     * 
     * @deprecated - use {@link #titleString(ObjectAdapter)}
     */
    @Deprecated
    String titleString();

    /**
     * Returns the title to display this object with, rendered within the context
     * of some other adapter.
     * 
     * <p>
     * @see TitleFacet#title(ObjectAdapter, ObjectAdapter)
     */
    String titleString(ObjectAdapter contextAdapter);

    /**
     * Return an {@link Instance} of the specified {@link Specification} with
     * respect to this {@link ObjectAdapter}.
     * 
     * <p>
     * If called with {@link ObjectSpecification}, then just returns
     * <tt>this</tt>). If called for other subinterfaces, then should provide an
     * appropriate {@link Instance} implementation.
     * 
     * <p>
     * Designed to be called in a double-dispatch design from
     * {@link Specification#getInstance(ObjectAdapter)}.
     * 
     * <p>
     * Note: this method will throw an {@link UnsupportedOperationException}
     * unless the extended <tt>PojoAdapterXFactory</tt> is configured. (That is,
     * only <tt>PojoAdapterX</tt> provides support for this; the regular
     * <tt>PojoAdapter</tt> does not currently.
     * 
     * @return
     */
    Instance getInstance(Specification specification);

    /**
     * Sometimes it is necessary to manage the replacement of the underlying
     * domain object (by another component such as an object store). This method
     * allows the adapter to be kept while the domain object is replaced.
     */
    void replacePojo(Object pojo);

    /**
     * For (stand-alone) collections, returns the element type.
     * 
     * <p>
     * For owned (aggregated) collections, the element type can be determined
     * from the <tt>TypeOfFacet</tt> associated with the
     * <tt>ObjectAssociation</tt> representing the collection.
     * 
     * @see #setElementSpecificationProvider(ElementSpecificationProvider)
     */
    ObjectSpecification getElementSpecification();

    /**
     * For (stand-alone) collections, returns the element type.
     * 
     * @see #getElementSpecification()
     */
    void setElementSpecificationProvider(ElementSpecificationProvider elementSpecificationProvider);

    
    
    
    /**
     * Returns the name of an icon to use if this object is to be displayed
     * graphically.
     * 
     * <p>
     * May return <code>null</code> if no icon is specified.
     */
    String getIconName();

    /**
     * Checks the version of this adapter to make sure that it does not differ
     * from the specified version.
     * 
     * @throws ConcurrencyException
     *             if the specified version differs from the version held this
     *             adapter.
     */
    void checkLock(Version version);

    /**
     * The object's unique {@link Oid}. 
     * 
     * <p>
     * This id allows the object to added to, stored by,
     * and retrieved from the object store.  Objects can be looked up by their
     * {@link Oid} from the {@link AdapterManager}.
     * 
     * <p>
     * Note that standalone value objects ("foobar", or 5, or a date),
     * are not mapped and have a <tt>null</tt> oid.
     */
    Oid getOid();

    /**
     * Since {@link Oid}s are now immutable, it is the reference from the 
     * {@link ObjectAdapter} to its {@link Oid} that must now be updated. 
     */
    void replaceOid(Oid persistedOid);

    /**
     * Returns either itself (if this is a root) or the parented collections, the
     * adapter corresponding to their {@link ParentedCollectionOid#getRootOid() root oid}.
     */
    ObjectAdapter getAggregateRoot();

    Version getVersion();
    void setVersion(Version version);


    /**
     * Whether this instance belongs to another object (meaning its
     * {@link #getOid()} will be a {@link ParentedCollectionOid}).
     */
    boolean isParentedCollection();


    /**
     * Whether this is a value (standalone, has no oid).
     */
    boolean isValue();


    public final class Util {

        private Util() {
        }

        public static Object unwrap(final ObjectAdapter adapter) {
            return adapter != null ? adapter.getObject() : null;
        }

        public static Object[] unwrap(final ObjectAdapter[] adapters) {
            if (adapters == null) {
                return null;
            }
            final Object[] unwrappedObjects = new Object[adapters.length];
            int i = 0;
            for (final ObjectAdapter adapter : adapters) {
                unwrappedObjects[i++] = unwrap(adapter);
            }
            return unwrappedObjects;
        }

        public static List<Object> unwrap(final List<ObjectAdapter> adapters) {
            List<Object> objects = Lists.newArrayList();
            for (ObjectAdapter adapter : adapters) {
                objects.add(unwrap(adapter));
            }
            return objects;
        }

        @SuppressWarnings("unchecked")
        public static <T> List<T> unwrapT(final List<ObjectAdapter> adapters) {
            return (List<T>) unwrap(adapters);
        }

        public static String unwrapAsString(final ObjectAdapter adapter) {
            final Object obj = unwrap(adapter);
            if (obj == null) {
                return null;
            }
            if (!(obj instanceof String)) {
                return null;
            }
            return (String) obj;
        }

        public static String titleString(final ObjectAdapter adapter) {
            return adapter != null ? adapter.titleString(null) : "";
        }

        public static boolean exists(final ObjectAdapter adapter) {
            return adapter != null && adapter.getObject() != null;
        }

        public static boolean wrappedEqual(final ObjectAdapter adapter1, final ObjectAdapter adapter2) {
            final boolean defined1 = exists(adapter1);
            final boolean defined2 = exists(adapter2);
            if (defined1 && !defined2) {
                return false;
            }
            if (!defined1 && defined2) {
                return false;
            }
            if (!defined1 && !defined2) {
                return true;
            } // both null
            return adapter1.getObject().equals(adapter2.getObject());
        }

        public static boolean nullSafeEquals(final Object obj1, final Object obj2) {
            if (obj1 == null && obj2 == null) {
                return true;
            }
            if (obj1 == null || obj2 == null) {
                return false;
            }
            if (obj1.equals(obj2)) {
                return true;
            }
            if (obj1 instanceof ObjectAdapter && obj2 instanceof ObjectAdapter) {
                final ObjectAdapter adapterObj1 = (ObjectAdapter) obj1;
                final ObjectAdapter adapterObj2 = (ObjectAdapter) obj2;
                return nullSafeEquals(adapterObj1.getObject(), adapterObj2.getObject());
            }
            return false;
        }

        /**
         * Filters a collection (an adapter around either a Collection or an Object[]) and returns a list of
         * {@link ObjectAdapter}s of those that are visible (as per any facet(s) installed on the element class
         * of the collection).
         *  @param collectionAdapter - an adapter around a collection (as returned by a getter of a collection, or of an autoCompleteNXxx() or choicesNXxx() method, etc
         * @param interactionInitiatedBy
         */
        public static List<ObjectAdapter> visibleAdapters(
                final ObjectAdapter collectionAdapter,
                final InteractionInitiatedBy interactionInitiatedBy) {

            final CollectionFacet facet = CollectionFacet.Utils.getCollectionFacetFromSpec(collectionAdapter);
            Iterable<ObjectAdapter> objectAdapters = facet.iterable(collectionAdapter);

            return visibleAdapters(objectAdapters, interactionInitiatedBy);
        }

        /**
         * as per {@link #visibleAdapters(ObjectAdapter, InteractionInitiatedBy)}.
         *  @param objectAdapters - iterable over the respective adapters of a collection (as returned by a getter of a collection, or of an autoCompleteNXxx() or choicesNXxx() method, etc
         * @param interactionInitiatedBy
         */
        public static List<ObjectAdapter> visibleAdapters(
                final Iterable<ObjectAdapter> objectAdapters,
                final InteractionInitiatedBy interactionInitiatedBy) {
            final List<ObjectAdapter> adapters = Lists.newArrayList();
            for (final ObjectAdapter adapter : objectAdapters) {
                final boolean visible = isVisible(adapter,
                        interactionInitiatedBy);
                if(visible) {
                    adapters.add(adapter);
                }
            }
            return adapters;
        }

        /**
         * @param adapter - an adapter around the domain object whose visibility is being checked
         * @param interactionInitiatedBy
         */
        public static boolean isVisible(
                final ObjectAdapter adapter,
                final InteractionInitiatedBy interactionInitiatedBy) {
            if(adapter == null) {
                // a choices list could include a null (eg example in ToDoItems#choices1Categorized()); want to show as "visible"
                return true;
            }
            if(adapter.isDestroyed()) {
                return false;
            }
            if(interactionInitiatedBy == InteractionInitiatedBy.FRAMEWORK) { return true; }
            return isVisibleForUser(adapter);
        }

        private static boolean isVisibleForUser(final ObjectAdapter adapter) {
            final VisibilityContext<?> context = createVisibleInteractionContextForUser(adapter);
            final ObjectSpecification objectSpecification = adapter.getSpecification();
            final InteractionResult visibleResult = InteractionUtils.isVisibleResult(objectSpecification, context);
            return visibleResult.isNotVetoing();
        }

        private static VisibilityContext<?> createVisibleInteractionContextForUser(
                final ObjectAdapter objectAdapter) {
            return new ObjectVisibilityContext(
                    objectAdapter,
                    objectAdapter.getSpecification().getIdentifier(),
                    InteractionInitiatedBy.USER,
                    Where.OBJECT_FORMS);
        }
    }

    boolean representsPersistent();
    boolean isDestroyed();


    public final class InvokeUtils {

        private InvokeUtils() {
        }

        public static void invokeAll(final List<Method> methods, final ObjectAdapter adapter) {
            MethodUtil.invoke(methods, Util.unwrap(adapter));
        }

        public static Object invoke(final Method method, final ObjectAdapter adapter) {
            return MethodExtensions.invoke(method, Util.unwrap(adapter));
        }

        public static Object invoke(final Method method, final ObjectAdapter adapter, final Object arg0) {
            return MethodExtensions.invoke(method, Util.unwrap(adapter), new Object[] {arg0});
        }

        public static Object invoke(final Method method, final ObjectAdapter adapter, final ObjectAdapter arg0Adapter) {
            return invoke(method, adapter, Util.unwrap(arg0Adapter));
        }

        public static Object invoke(final Method method, final ObjectAdapter adapter, final ObjectAdapter[] argumentAdapters) {
            return MethodExtensions.invoke(method, Util.unwrap(adapter), Util.unwrap(argumentAdapters));
        }

        public static Object invoke(final Method method, final ObjectAdapter adapter, final Map<Integer, ObjectAdapter> argumentAdapters) {
            return invoke(method, adapter, asArray(argumentAdapters, method.getParameterTypes().length));
        }

        private static ObjectAdapter[] asArray(Map<Integer, ObjectAdapter> argumentAdapters, int length) {
            ObjectAdapter[] args = new ObjectAdapter[length];
            for (final Map.Entry<Integer, ObjectAdapter> entry : argumentAdapters.entrySet()) {
                final Integer paramNum = entry.getKey();
                if(paramNum < length) {
                    args[paramNum] = entry.getValue();
                }
            }
            return args;
        }

        /**
         * Invokes the method, adjusting arguments as required to make them fit the method's parameters.
         *
         * <p>
         * That is:
         * <ul>
         * <li>if the method declares parameters but arguments are missing, then will provide 'null' defaults for these.
         * <li>if the method does not declare all parameters for arguments, then truncates arguments.
         * </ul>
         */
        public static Object invokeAutofit(final Method method, final ObjectAdapter target, List<ObjectAdapter> argumentsIfAvailable, final AdapterManager adapterManager) {
            final List<ObjectAdapter> args = Lists.newArrayList();
            if(argumentsIfAvailable != null) {
                args.addAll(argumentsIfAvailable);
            }

            adjust(method, args, adapterManager);

            final ObjectAdapter[] argArray = args.toArray(new ObjectAdapter[]{});
            return invoke(method, target, argArray);
        }

        private static void adjust(final Method method, final List<ObjectAdapter> args, final AdapterManager adapterManager) {
            final Class<?>[] parameterTypes = method.getParameterTypes();
            ListExtensions.adjust(args, parameterTypes.length);

            for(int i=0; i<parameterTypes.length; i++) {
                final Class<?> cls = parameterTypes[i];
                if(args.get(i) == null && cls.isPrimitive()) {
                    final Object object = ClassExtensions.toDefault(cls);
                    final ObjectAdapter adapter = adapterManager.adapterFor(object);
                    args.set(i, adapter);
                }
            }
        }

        /**
         * Invokes the method, adjusting arguments as required.
         *
         * <p>
         * That is:
         * <ul>
         * <li>if the method declares parameters but no arguments are provided, then will provide 'null' defaults for these.
         * <li>if the method does not declare parameters but arguments were provided, then will ignore those argumens.
         * </ul>
         */
        @SuppressWarnings("unused")
        private static Object invokeWithDefaults(final Method method, final ObjectAdapter adapter, final ObjectAdapter[] argumentAdapters) {
            final int numParams = method.getParameterTypes().length;
            ObjectAdapter[] adapters;

            if(argumentAdapters == null || argumentAdapters.length == 0) {
                adapters = new ObjectAdapter[numParams];
            } else if(numParams == 0) {
                // ignore any arguments, even if they were supplied.
                // eg used by contributee actions, but
                // underlying service 'default' action declares no params
                adapters = new ObjectAdapter[0];
            } else if(argumentAdapters.length == numParams){
                adapters = argumentAdapters;
            } else {
                throw new IllegalArgumentException("Method has " + numParams + " params but " + argumentAdapters.length + " arguments provided");
            }

            return invoke(method, adapter, adapters);
        }
    }

    public static class Functions {
        
        private Functions(){}

        public static Function<ObjectAdapter, Object> getObject() {
            return new Function<ObjectAdapter, Object>() {
                @Override
                public Object apply(ObjectAdapter input) {
                    return Util.unwrap(input);
                }
            };
        }
        
        public static Function<Object, ObjectAdapter> adapterForUsing(final AdapterManager adapterManager) {
            return new Function<Object, ObjectAdapter>() {
                @Override
                public ObjectAdapter apply(final Object pojo) {
                    return adapterManager.adapterFor(pojo);
                }
            };
        }
    }


}
