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

import java.io.DataInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.UUID;

import org.apache.geode.internal.HeapDataOutputStream;
import org.apache.geode.internal.Version;
import org.apache.geode.internal.cache.CachedDeserializable;
import org.apache.geode.internal.cache.Token;
import org.apache.geode.internal.i18n.LocalizedStrings;
import org.apache.geode.pdx.PdxInstance;
import org.apache.geode.pdx.WritablePdxInstance;
import org.apache.geode.pdx.internal.PdxUnreadData;

/**
 * A static helper for optimally creating copies. Creating copies of cache values provides improved
 * concurrency as well as isolation. For transactions, creating a copy is the guaranteed way to
 * enforce "Read Committed" isolation on changes to cache <code>Entries</code>.
 *
 * <p>
 * Here is a simple example of how to use <code>CopyHelper.copy</code>
 *
 * <pre>
 * Object o = r.get("stringBuf");
 * StringBuffer s = (StringBuffer) CopyHelper.copy(o);
 * s.append("... and they lived happily ever after.  The End.");
 * r.put("stringBuf", s);
 * </pre>
 *
 * @see Cloneable
 * @see Serializable
 * @see DataSerializer
 * @see org.apache.geode.cache.Cache#setCopyOnRead
 * @see org.apache.geode.cache.CacheTransactionManager
 *
 * @since GemFire 4.0
 */

public final class CopyHelper {

  // no instances allowed
  private CopyHelper() {}

  /**
   * Return true if the given object is an instance of a well known immutable class. The well known
   * classes are:
   * <ul>
   * <li>String
   * <li>Byte
   * <li>Character
   * <li>Short
   * <li>Integer
   * <li>Long
   * <li>Float
   * <li>Double
   * <li>BigInteger
   * <li>BigDecimal
   * <li>UUID
   * <li>PdxInstance but not WritablePdxInstance
   * </ul>
   *
   * @param o the object to check
   * @return true if o is an instance of a well known immutable class.
   * @since GemFire 6.6.2
   */
  public static boolean isWellKnownImmutableInstance(Object o) {
    if (o instanceof String) {
      return true;
    }
    if (o instanceof Number) {
      if (o instanceof Integer)
        return true;
      if (o instanceof Long)
        return true;
      if (o instanceof Byte)
        return true;
      if (o instanceof Short)
        return true;
      if (o instanceof Float)
        return true;
      if (o instanceof Double)
        return true;
      // subclasses of non-final classes may be mutable
      if (o.getClass().equals(BigInteger.class))
        return true;
      if (o.getClass().equals(BigDecimal.class))
        return true;
    }
    if (o instanceof PdxInstance && !(o instanceof WritablePdxInstance)) {
      // no need to copy since it is immutable
      return true;
    }
    if (o instanceof Character)
      return true;
    if (o instanceof UUID)
      return true;
    return false;
  }

  /**
   * <p>
   * Makes a copy of the specified object. The object returned is not guaranteed to be a deep copy
   * of the original object, as explained below.
   *
   * <p>
   * Copies can only be made if the original is a <tt>Cloneable</tt> or serializable by GemFire. If
   * o is a {@link #isWellKnownImmutableInstance(Object) well known immutable instance} then it will
   * be returned without copying it.
   *
   * <p>
   * If the argument o is an instance of {@link java.lang.Cloneable}, a copy is made by invoking
   * <tt>clone</tt> on it. Note that not all implementations of <tt>clone</tt> make deep copies
   * (e.g. {@link java.util.HashMap#clone HashMap.clone}). Otherwise, if the argument is not an
   * instance of <tt>Cloneable</tt>, a copy is made using serialization: if GemFire serialization is
   * implemented, it is used; otherwise, java serialization is used.
   *
   * <p>
   * The difference between this method and {@link #deepCopy(Object) deepCopy}, is that this method
   * uses <tt>clone</tt> if available, whereas <tt>deepCopy</tt> does not. As a result, for
   * <tt>Cloneable</tt> objects copied using this method, how deep a copy the returned object is
   * depends on its implementation of <tt>clone</tt>.
   *
   * @param o the original object that a copy is needed of
   * @return the new instance that is a copy of of the original
   * @throws CopyException if copying fails because a class could not be found or could not be
   *         serialized.
   * @see #deepCopy(Object)
   * @since GemFire 4.0
   */
  @SuppressWarnings("unchecked")
  public static <T> T copy(T o) {
    T copy = null;
    try {
      if (o == null) {
        return null;
      } else if (o instanceof Token) {
        return o;
      } else {
        if (isWellKnownImmutableInstance(o))
          return o;
        if (o instanceof Cloneable) {
          try {
            // Note that Object.clone is protected so we need to use reflection
            // to call clone even though this guy implements Cloneable
            Class<?> c = o.getClass();
            // By convention, the user should make the clone method public.
            // But even if they don't, let's go ahead and use it.
            // The other problem is that if the class is private, we still
            // need to make the method accessible even if the method is public,
            // because Object.clone is protected.
            Method m = c.getDeclaredMethod("clone", new Class[0]);
            m.setAccessible(true);
            copy = (T) m.invoke(o, new Object[0]);
            return copy;
          } catch (NoSuchMethodException ignore) {
            // try using Serialization
          } catch (IllegalAccessException ignore) {
            // try using Serialization
          } catch (SecurityException ignore) {
            // try using Serialization
          } catch (InvocationTargetException ex) {
            Throwable cause = ex.getTargetException();
            if (cause instanceof CloneNotSupportedException) {
              // try using Serialization
            } else {
              throw new CopyException(LocalizedStrings.CopyHelper_CLONE_FAILED.toLocalizedString(),
                  cause != null ? cause : ex);
            }
          }
        } else if (o instanceof CachedDeserializable) {
          copy = (T) ((CachedDeserializable) o).getDeserializedWritableCopy(null, null);
          return copy;
        } else if (o.getClass().isArray() && o.getClass().getComponentType().isPrimitive()) {
          if (o instanceof byte[]) {
            byte[] a = (byte[]) o;
            copy = (T) Arrays.copyOf(a, a.length);
          } else if (o instanceof boolean[]) {
            boolean[] a = (boolean[]) o;
            copy = (T) Arrays.copyOf(a, a.length);
          } else if (o instanceof char[]) {
            char[] a = (char[]) o;
            copy = (T) Arrays.copyOf(a, a.length);
          } else if (o instanceof int[]) {
            int[] a = (int[]) o;
            copy = (T) Arrays.copyOf(a, a.length);
          } else if (o instanceof long[]) {
            long[] a = (long[]) o;
            copy = (T) Arrays.copyOf(a, a.length);
          } else if (o instanceof short[]) {
            short[] a = (short[]) o;
            copy = (T) Arrays.copyOf(a, a.length);
          } else if (o instanceof float[]) {
            float[] a = (float[]) o;
            copy = (T) Arrays.copyOf(a, a.length);
          } else if (o instanceof double[]) {
            double[] a = (double[]) o;
            copy = (T) Arrays.copyOf(a, a.length);
          }
          return copy;
        }
        // Copy using serialization
        copy = doDeepCopy(o);
        return copy;
      }
    } finally {
      if (copy != null) {
        PdxUnreadData.copy(o, copy);
      }
    }
  }

  /**
   * Makes a deep copy of the specified object o using serialization, so the object has to be
   * serializable by GemFire.
   *
   * <p>
   * If o is a {@link #isWellKnownImmutableInstance(Object) well known immutable instance} then it
   * will be returned without copying it.
   *
   * <p>
   * The passed in object is serialized in memory, and then deserialized into a new instance, which
   * is returned. If GemFire serialization is implemented for the object, it is used; otherwise,
   * java serialization is used.
   *
   * @param o the original object to be copied
   * @return the new instance that is a copy of the original
   * @throws CopyException if copying fails because a class could not be found or could not be
   *         serialized
   * @see #copy(Object)
   */
  public static <T> T deepCopy(T o) {
    T copy = null;
    try {
      if (o == null) {
        return null;
      } else if (o instanceof Token || isWellKnownImmutableInstance(o)) {
        return o;
      } else {
        copy = doDeepCopy(o);
        return copy;
      }
    } finally {
      if (copy != null) {
        PdxUnreadData.copy(o, copy);
      }
    }
  }

  @SuppressWarnings("unchecked")
  private static <T> T doDeepCopy(T o) {
    try {
      HeapDataOutputStream hdos = new HeapDataOutputStream(Version.CURRENT);
      DataSerializer.writeObject(o, hdos);
      return (T) DataSerializer.readObject(new DataInputStream(hdos.getInputStream()));
    } catch (ClassNotFoundException ex) {
      throw new CopyException(
          LocalizedStrings.CopyHelper_COPY_FAILED_ON_INSTANCE_OF_0.toLocalizedString(o.getClass()),
          ex);
    } catch (IOException ex) {
      throw new CopyException(
          LocalizedStrings.CopyHelper_COPY_FAILED_ON_INSTANCE_OF_0.toLocalizedString(o.getClass()),
          ex);
    }

  }
}
