/*
 * Copyright (c) 2016 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jme3.util.clone;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 *  A deep clone utility that provides similar object-graph-preserving
 *  qualities to typical serialization schemes.  An internal registry
 *  of cloned objects is kept to be used by other objects in the deep
 *  clone process that implement JmeCloneable.
 *
 *  <p>By default, objects that do not implement JmeCloneable will
 *  be treated like normal Java Cloneable objects.  If the object does
 *  not implement the JmeCloneable or the regular JDK Cloneable interfaces
 *  AND has no special handling defined then an IllegalArgumentException
 *  will be thrown.</p>
 *
 *  <p>Enhanced object cloning is done in a two step process.  First,
 *  the object is cloned using the normal Java clone() method and stored
 *  in the clone registry.  After that, if it implements JmeCloneable then
 *  its cloneFields() method is called to deep clone any of the fields.
 *  This two step process has a few benefits.  First, it means that objects
 *  can easily have a regular shallow clone implementation just like any
 *  normal Java objects.  Second, the deep cloning of fields happens after
 *  creation wich means that the clone is available to future field cloning
 *  to resolve circular references.</p>
 *
 *  <p>Similar to Java serialization, the handling of specific object
 *  types can be customized.  This allows certain objects to be cloned gracefully
 *  even if they aren't normally Cloneable.  This can also be used as a
 *  sort of filter to keep certain types of objects from being cloned.
 *  (For example, adding the IdentityCloneFunction for Mesh.class would cause
 *  all mesh instances to be shared with the original object graph.)</p>
 *
 *  <p>By default, the Cloner registers serveral default clone functions
 *  as follows:</p>
 *  <ul>
 *  <li>java.util.ArrayList: ListCloneFunction
 *  <li>java.util.LinkedList: ListCloneFunction
 *  <li>java.util.concurrent.CopyOnWriteArrayList: ListCloneFunction
 *  <li>java.util.Vector: ListCloneFunction
 *  <li>java.util.Stack: ListCloneFunction
 *  <li>com.jme3.util.SafeArrayList: ListCloneFunction
 *  </ul>
 *
 *  <p>Usage:</p>
 *  <pre>
 *  // Example 1: using an instantiated, reusable cloner.
 *  Cloner cloner = new Cloner();
 *  Foo fooClone = cloner.clone(foo);
 *  cloner.clearIndex(); // prepare it for reuse
 *  Foo fooClone2 = cloner.clone(foo);
 *
 *  // Example 2: using the utility method that self-instantiates a temporary cloner.
 *  Foo fooClone = Cloner.deepClone(foo);
 *
 *  </pre>
 *
 *  @author    Paul Speed
 */
public class Cloner {

    static Logger log = Logger.getLogger(Cloner.class.getName());

    /**
     *  Keeps track of the objects that have been cloned so far.
     */
    private IdentityHashMap<Object, Object> index = new IdentityHashMap<Object, Object>();

    /**
     *  Custom functions for cloning objects.
     */
    private Map<Class, CloneFunction> functions = new HashMap<Class, CloneFunction>();

    /**
     *  Cache the clone methods once for all cloners.
     */
    private static final Map<Class, Method> methodCache = new ConcurrentHashMap<>();

    /**
     *  Creates a new cloner with only default clone functions and an empty
     *  object index.
     */
    public Cloner() {
        // Register some standard types
        ListCloneFunction listFunction = new ListCloneFunction();
        functions.put(java.util.ArrayList.class, listFunction);
        functions.put(java.util.LinkedList.class, listFunction);
        functions.put(java.util.concurrent.CopyOnWriteArrayList.class, listFunction);
        functions.put(java.util.Vector.class, listFunction);
        functions.put(java.util.Stack.class, listFunction);
        functions.put(com.jme3.util.SafeArrayList.class, listFunction);
    }

    /**
     *  Convenience utility function that creates a new Cloner, uses it to
     *  deep clone the object, and then returns the result.
     */
    public static <T> T deepClone( T object ) {
        return new Cloner().clone(object);
    }

    /**
     *  Deeps clones the specified object, reusing previous clones when possible.
     *
     *  <p>Object cloning priority works as follows:</p>
     *  <ul>
     *  <li>If the object has already been cloned then its clone is returned.
     *  <li>If there is a custom CloneFunction then it is called to clone the object.
     *  <li>If the object implements Cloneable then its clone() method is called, arrays are
     *      deep cloned with entries passing through clone().
     *  <li>If the object implements JmeCloneable then its cloneFields() method is called on the
     *      clone.
     *  <li>Else an IllegalArgumentException is thrown.
     *  </ul>
     *
     *  Note: objects returned by this method may not have yet had their cloneField()
     *  method called.
     */
    public <T> T clone( T object ) {
        return clone(object, true);
    }

    /**
     *  Internal method to work around a Java generics typing issue by
     *  isolating the 'bad' case into a method with suppressed warnings.
     */
    @SuppressWarnings("unchecked")
    private <T> Class<T> objectClass( T object ) {
        // This should be 100% allowed without a cast but Java generics
        // is not that smart sometimes.
        // Wrapping it in a method at least isolates the warning suppression
        return (Class<T>)object.getClass();
    }

    /**
     *  Deeps clones the specified object, reusing previous clones when possible.
     *
     *  <p>Object cloning priority works as follows:</p>
     *  <ul>
     *  <li>If the object has already been cloned then its clone is returned.
     *  <li>If useFunctions is true and there is a custom CloneFunction then it is
     *      called to clone the object.
     *  <li>If the object implements Cloneable then its clone() method is called, arrays are
     *      deep cloned with entries passing through clone().
     *  <li>If the object implements JmeCloneable then its cloneFields() method is called on the
     *      clone.
     *  <li>Else an IllegalArgumentException is thrown.
     *  </ul>
     *
     *  <p>The abililty to selectively use clone functions is useful when
     *  being called from a clone function.</p>
     *
     *  Note: objects returned by this method may not have yet had their cloneField()
     *  method called.
     */
    public <T> T clone( T object, boolean useFunctions ) {

        if( object == null ) {
            return null;
        }

        if( log.isLoggable(Level.FINER) ) {
            log.finer("cloning:" + object.getClass() + "@" + System.identityHashCode(object));
        }

        Class<T> type = objectClass(object);

        // Check the index to see if we already have it
        Object clone = index.get(object);
        if( clone != null ) {
            if( log.isLoggable(Level.FINER) ) {
                log.finer("cloned:" + object.getClass() + "@" + System.identityHashCode(object)
                            + " as cached:" + clone.getClass() + "@" + System.identityHashCode(clone));
            }
            return type.cast(clone);
        }

        // See if there is a custom function... that trumps everything.
        CloneFunction<T> f = getCloneFunction(type);
        if( f != null ) {
            T result = f.cloneObject(this, object);

            // Store the object in the identity map so that any circular references
            // are resolvable.
            index.put(object, result);

            // Now call the function again to deep clone the fields
            f.cloneFields(this, result, object);

            if( log.isLoggable(Level.FINER) ) {
                if( result == null ) {
                    log.finer("cloned:" + object.getClass() + "@" + System.identityHashCode(object)
                                + " as transformed:null");
                } else {
                    log.finer("clone:" + object.getClass() + "@" + System.identityHashCode(object)
                                + " as transformed:" + result.getClass() + "@" + System.identityHashCode(result));
                }
            }
            return result;
        }

        if( object.getClass().isArray() ) {
            // Perform an array clone
            clone = arrayClone(object);

            // Array clone already indexes the clone
        } else if( object instanceof JmeCloneable ) {
            // Use the two-step cloning semantics
            clone = ((JmeCloneable)object).jmeClone();

            // Store the object in the identity map so that any circular references
            // are resolvable
            index.put(object, clone);

            ((JmeCloneable)clone).cloneFields(this, object);
        } else if( object instanceof Cloneable ) {

            // Perform a regular Java shallow clone
            try {
                clone = javaClone(object);
            } catch( CloneNotSupportedException e ) {
                throw new IllegalArgumentException("Object is not cloneable, type:" + type, e);
            }

            // Store the object in the identity map so that any circular references
            // are resolvable
            index.put(object, clone);
        } else {
            throw new IllegalArgumentException("Object is not cloneable, type:" + type);
        }

        if( log.isLoggable(Level.FINER) ) {
            log.finer("cloned:" + object.getClass() + "@" + System.identityHashCode(object)
                        + " as " + clone.getClass() + "@" + System.identityHashCode(clone));
        }
        return type.cast(clone);
    }

    /**
     *  Sets a custom CloneFunction for implementations of the specified Java type.  Some
     *  inheritance checks are made but no disambiguation is performed.
     *  <p>Note: in the general case, it is better to register against specific classes and
     *  not super-classes or super-interfaces unless you know specifically that they are cloneable.</p>
     *  <p>By default ListCloneFunction is registered for ArrayList, LinkedList, CopyOnWriteArrayList,
     *  Vector, Stack, and JME's SafeArrayList.</p>
     */
    public <T> void setCloneFunction( Class<T> type, CloneFunction<T> function ) {
        if( function == null ) {
            functions.remove(type);
        } else {
            functions.put(type, function);
        }
    }

    /**
     *  Returns a previously registered clone function for the specified type or null
     *  if there is no custom clone function for the type.
     */
    @SuppressWarnings("unchecked")
    public <T> CloneFunction<T> getCloneFunction( Class<T> type ) {
        CloneFunction<T> result = (CloneFunction<T>)functions.get(type);
        if( result == null ) {
            // Do a more exhaustive search
            for( Map.Entry<Class, CloneFunction> e : functions.entrySet() ) {
                if( e.getKey().isAssignableFrom(type) ) {
                    result = e.getValue();
                    break;
                }
            }
            if( result != null ) {
                // Cache it for later
                functions.put(type, result);
            }
        }
        return result;
    }

    /**
     *  Forces an object to be added to the indexing cache such that attempts
     *  to clone the 'original' will always result in the 'clone' being returned.
     *  This can be used to stub out specific values from being cloned or to
     *  force global shared instances to be used even if the object is cloneable
     *  normally.
     */
    public <T> void setClonedValue( T original, T clone ) {
        index.put(original, clone);
    }

    /**
     *  Returns true if the specified object has already been cloned
     *  by this cloner during this session.  Cloned objects are cached
     *  for later use and it's sometimes convenient to know if some
     *  objects have already been cloned.
     */
    public boolean isCloned( Object o ) {
        return index.containsKey(o);
    }

    /**
     *  Clears the object index allowing the cloner to be reused for a brand new
     *  cloning operation.
     */
    public void clearIndex() {
        index.clear();
    }

    /**
     *  Performs a raw shallow Java clone using reflection.  This call does NOT
     *  check against the clone index and so will return new objects every time
     *  it is called.  That's because these are shallow clones and have not (and may
     *  not ever, depending on the caller) get resolved.
     *
     *  <p>This method is provided as a convenient way for CloneFunctions to call
     *  clone() and objects without necessarily knowing their real type.</p>
     */
    public <T> T javaClone( T object ) throws CloneNotSupportedException {
        if( object == null ) {
            return null;
        }
        Method m = methodCache.get(object.getClass());
        if( m == null ) {
            try {
                // Lookup the method and cache it
                m = object.getClass().getMethod("clone");
            } catch( NoSuchMethodException e ) {
                throw new CloneNotSupportedException("No public clone method found for:" + object.getClass());
            }
            methodCache.put(object.getClass(), m);

            // Note: yes we might cache the method twice... but so what?
        }

        try {
            Class<? extends T> type = objectClass(object);
            return type.cast(m.invoke(object));
        } catch( IllegalAccessException | InvocationTargetException e ) {
            throw new RuntimeException("Error cloning object of type:" + object.getClass(), e);
        }
    }

    /**
     *  Clones a primitive array by coping it and clones an object
     *  array by coping it and then running each of its values through
     *  Cloner.clone().
     */
    protected <T> T arrayClone( T object ) {

        // Java doesn't support the cloning of arrays through reflection unless
        // you open access to Object's protected clone array... which requires
        // elevated privileges.  So we will do a work-around that is slightly less
        // elegant.
        // This should be 100% allowed without a case but Java generics
        // is not that smart
        Class<T> type = objectClass(object);
        Class elementType = type.getComponentType();
        int size = Array.getLength(object);
        Object clone = Array.newInstance(elementType, size);

        // Store the clone for later lookups
        index.put(object, clone);

        if( elementType.isPrimitive() ) {
            // Then our job is a bit easier
            System.arraycopy(object, 0, clone, 0, size);
        } else {
            // Else it's an object array so we'll clone it and its children
            for( int i = 0; i < size; i++ ) {
                Object element = clone(Array.get(object, i));
                Array.set(clone, i, element);
            }
        }

        return type.cast(clone);
    }
}
