/*
 * Twitch Android App
 * Copyright (c) 2012-2014 Twitch Interactive, Inc.
 */

package tv.twitch.util;

import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import tv.twitch.twiglib.Logger;

/**
 * Adapted by loohill on 05/09/18
 * Last sync: 05/09/18
 * Source: twitch-apps/twitch-android/blob/master/Twitch/src/main/java/tv/twitch/android/util/ParcelableHelper.java
 * Changes:
 *** Replace logger to ours;
 *** Get rid of BuildConfigUtil in constructor;
 */

/**
 * Simplifies Parceling an object.  All you need to do is instantiate an instance with the class type you want to parcel
 * and pass an array of the names of the members you want to persist.  Then you call pack() to serialize an instance of
 * your object into a Parcel and unpack() to deserialize a new instance from a Parcel.
 *
 * Supported Types:
 * - int, Integer
 * - long, Long
 * - double, Double
 * - float, Float
 * - boolean, Boolean
 * - String
 * - HashMap<String, String>
 * - nested Parcelable objects
 *
 * @param <T> The class type you want to parcel.
 *
 * @Deprecated - usage of the @Parcel annotation (Parceler library) is preferred
 */
@Deprecated
public class ParcelableHelper<T> {

	private static final Set<Class> sBasicParcelableClasses = new HashSet<Class>() {{
		add(int.class);
		add(Integer.class);
		add(long.class);
		add(Long.class);
		add(float.class);
		add(Float.class);
		add(double.class);
		add(Double.class);
		add(boolean.class);
		add(Boolean.class);
		add(String.class);
		// Also supports HashMaps, but only for HashMap<String, String>
	}};

	private static final Comparator<Field> sFieldComparator = new Comparator<Field>() {
		@Override
		public int compare(Field o1, Field o2) {
			return o1.getName().compareTo(o2.getName());
		}
	};

	@NonNull private Class<T> mClass;

	public ParcelableHelper(@NonNull Class<T> klass) {
		mClass = klass;
	}

	private void writeBasicField(@NonNull Object obj, @NonNull Field field, @NonNull Parcel out) throws IllegalAccessException {
		if (field.getType() == int.class) {
			out.writeInt(field.getInt(obj));
		} else if (field.getType() == Integer.class) {
			Integer val = (Integer) field.get(obj);
			out.writeInt(val != null ? val.intValue() : 0);
		} else if (field.getType() == long.class) {
			out.writeLong(field.getLong(obj));
		} else if (field.getType() == Long.class) {
			Long val = (Long) field.get(obj);
			out.writeLong(val != null ? val.longValue() : 0);
		} else if (field.getType() == float.class) {
			out.writeFloat(field.getFloat(obj));
		} else if (field.getType() == Float.class) {
			Float val = (Float) field.get(obj);
			out.writeFloat(val != null ? val.floatValue() : 0);
		} else if (field.getType() == double.class) {
			out.writeDouble(field.getDouble(obj));
		} else if (field.getType() == Double.class) {
			Double val = (Double) field.get(obj);
			out.writeDouble(val != null ? val.doubleValue() : 0);
		} else if (field.getType() == boolean.class) {
			out.writeBooleanArray(new boolean[]{field.getBoolean(obj)});
		} else if (field.getType() == Boolean.class) {
			Boolean val = (Boolean) field.get(obj);
			out.writeBooleanArray(new boolean[]{val != null ? val.booleanValue() : false});
		} else if (field.getType() == String.class) {
			out.writeString((String) field.get(obj));
		}
	}

	private void readBasicField(@NonNull Object result, @NonNull Field field, @NonNull Parcel in) throws IllegalAccessException {
		if (field.getType() == int.class) {
			field.setInt(result, in.readInt());
		} else if (field.getType() == Integer.class) {
			field.set(result, in.readInt());
		} else if (field.getType() == long.class) {
			field.setLong(result, in.readLong());
		} else if (field.getType() == Long.class) {
			field.set(result, in.readLong());
		} else if (field.getType() == float.class) {
			field.setFloat(result, in.readFloat());
		} else if (field.getType() == Float.class) {
			field.set(result, in.readFloat());
		} else if (field.getType() == double.class) {
			field.setDouble(result, in.readDouble());
		} else if (field.getType() == Double.class) {
			field.set(result, in.readDouble());
		} else if (field.getType() == boolean.class) {
			boolean[] arr = new boolean[1];
			in.readBooleanArray(arr);
			field.setBoolean(result, arr[0]);
		} else if (field.getType() == Boolean.class) {
			boolean[] arr = new boolean[1];
			in.readBooleanArray(arr);
			field.set(result, arr[0]);
		} else if (field.getType() == String.class) {
			field.set(result, in.readString());
		}
	}

	/**
	 * Recursively find the fields in the given class which have been marked by the ParcelableField
	 * annotation.
	 */
	private Field[] findSerializableFields(Class klass) {
		ArrayList<Field> result = new ArrayList<>();
		ArrayList<Field> currentClassesParcelableFields = new ArrayList<>();

		while (klass != null) {
			// Get the marked fields of this class
			Field[] fields = klass.getDeclaredFields();

			for (Field currentField : fields) {
				if (currentField.getDeclaringClass() != klass) {
					continue;
				}
				Annotation[] annotations = currentField.getAnnotations();
				for (Annotation annotation : annotations) {
					if (annotation instanceof ParcelableField) {
						currentClassesParcelableFields.add(currentField);
						break;
					}
				}
			}

			Collections.sort(currentClassesParcelableFields, sFieldComparator);
			result.addAll(currentClassesParcelableFields);
			currentClassesParcelableFields.clear();

			klass = klass.getSuperclass();
		}

		return result.toArray(new Field[result.size()]);
	}

	private void packObject(@Nullable Object obj, @Nullable Parcel out) {
		if (obj == null || out == null) {
			Logger.e("packObject - null Object or Parcel; aborting pack");
			return;
		}

		Field[] fields = findSerializableFields(obj.getClass());

		for (Field field : fields) {
			try {
				field.setAccessible(true);

				if (sBasicParcelableClasses.contains(field.getType())) {
					writeBasicField(obj, field, out);
					continue;
				}

				if (field.getType() == HashMap.class) {
					if (field.getGenericType() instanceof ParameterizedType) {
						boolean isValid = isValidFieldForHashMap(field);

						if (isValid) {
							HashMap<String, String> val = (HashMap<String, String>) field.get(obj);
							final int size = val == null ? 0 : val.size();
							out.writeInt(size);
							if (size > 0) {
								for (Map.Entry<String, String> entry : val.entrySet()) {
									out.writeString(entry.getKey());
									out.writeString(entry.getValue());
								}
							}
						}
					}
				} else {
					// Look for custom Parcelable objects
					boolean isCustomParcelable = isCustomParcelable(field);

					if (isCustomParcelable) {
						out.writeInt(field.get(obj) == null ? 0 : 1);
						packObject(field.get(obj), out);
					} else {
						// TODO: add support for more types as needed
						throw new Exception("Unhandled Parcel type: " + field.getType().getName());
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	public void pack(T obj, Parcel out) {
		packObject(obj, out);
	}

	private Object unpack(Class klass, Parcel in) {
		Object result;
		try {
			result = klass.newInstance();
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}

		Field[] fields = findSerializableFields(klass);

		for (Field field : fields) {
			try {
				field.setAccessible(true);

				if (sBasicParcelableClasses.contains(field.getType())) {
					readBasicField(result, field, in);
					continue;
				}

				if (field.getType() == HashMap.class) {
					if (field.getGenericType() instanceof ParameterizedType) {
						boolean isValid = isValidFieldForHashMap(field);

						if (isValid) {
							HashMap<String, String> hashMap = new HashMap<>();
							int size = in.readInt();
							for (int i = 0; i < size; ++i) {
								String key = in.readString();
								String value = in.readString();
								hashMap.put(key, value);
							}
							field.set(result, hashMap);
						}
					}
				} else {
					// Look for custom Parcelable objects
					boolean isCustomParcelable = isCustomParcelable(field);

					if (isCustomParcelable) {
						int isNotNull = in.readInt();
						if (isNotNull > 0) {
							Object recursiveResult = unpack(field.getType(), in);
							field.set(result, recursiveResult);
						} else {
							field.set(result, null);
						}
					} else {
						// TODO: add support for more types as needed
						throw new Exception("Unhandled Parcel type: " + field.getType().getName());
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		return result;
	}

	private boolean isValidFieldForHashMap(Field field) {
		ParameterizedType type = (ParameterizedType) field.getGenericType();
		Type[] typeArguments = type.getActualTypeArguments();
		for (Type typeArgument : typeArguments) {
			if (typeArgument != String.class) {
				return false;
			}
		}
		return true;
	}

	public T unpack(Parcel in) {
		return (T) unpack(mClass, in);
	}

	private boolean checkParcelableFieldsValid(Class klass) {
		Field[] fields = findSerializableFields(klass);
		for (Field field : fields) {
			Class<?> type = field.getType();
			if (!sBasicParcelableClasses.contains(type)) {
				if (type == HashMap.class && field.getGenericType() instanceof ParameterizedType) {
					boolean isValid = isValidFieldForHashMap(field);
					if (!isValid) {
						Logger.e("Not parcelable: " + type.getName());
						return false;
					}
				} else {
					// Look for custom Parcelable objects
					boolean isCustomParcelable = isCustomParcelable(field);

					if (!isCustomParcelable || !checkParcelableFieldsValid(type)) {
						Logger.e("Not parcelable: " + type.getName());
						return false;
					}
				}
			}
		}

		return true;
	}

	private boolean isCustomParcelable(@NonNull Field field) {
		boolean isCustomParcelable = false;
		Class[] classInterfaces = field.getType().getInterfaces();
		for (Class classInterface : classInterfaces) {
			if (classInterface.toString().equals(Parcelable.class.toString())) {
				isCustomParcelable = true;
				break;
			}
		}
		return isCustomParcelable;
	}
}
