package com.amplitude.api;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.HashSet;
import java.util.Set;

/**
 *  <h1>Identify</h1>
 *  Identify objects are a wrapper for user property operations. Each method adds a user
 *  property operation to the Identify object, and returns the same Identify object, allowing
 *  you to chain multiple method calls together, for example:
 *  {@code Identify identify = new Identify().set("color", "green").add("karma", 1);}
 *  <br><br>
 *  <b>Note:</b> if the same user property is used
 *  in multiple operations on a single Identify object, only the first operation on that
 *  property will be saved, and the rest will be ignored.
 *  <br><br>
 *  After creating an Identify object and setting the desired operations, send it to Amplitude
 *  servers by calling {@code Amplitude.getInstance().identify(identify);} and pass in the object.
 *
 *  @see <a href="https://github.com/amplitude/Amplitude-Android#user-properties-and-user-property-operations">
 *      Android SDK README</a> for more information on the Identify API and user property operations.
 */
public class Identify {

    /**
     * The class identifier tag used in logging. TAG = {@code "com.amplitude.api.Identify";}
     */
    private static final String TAG = Identify.class.getName();

    /**
     * Internal {@code JSONObject} to hold all of the user property operations.
     */
    protected JSONObject userPropertiesOperations = new JSONObject();
    /**
     * Internal set to keep track of user property keys and test for duplicates.
     */
    protected Set<String> userProperties = new HashSet<String>();

    /**
     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user
     * property will be ignored.
     *
     * @param property the user property to setOnce
     * @param value    the value of the user property
     * @return the same Identify object
     */
    public Identify setOnce(String property, boolean value) {
        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);
        return this;
    }

    /**
     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user
     * property will be ignored.
     *
     * @param property the user property to setOnce
     * @param value    the value of the user property
     * @return the same Identify object
     */
    public Identify setOnce(String property, double value) {
        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);
        return this;
    }

    /**
     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user
     * property will be ignored.
     *
     * @param property the user property to setOnce
     * @param value    the value of the user property
     * @return the same Identify object
     */
    public Identify setOnce(String property, float value) {
        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);
        return this;
    }

    /**
     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user
     * property will be ignored.
     *
     * @param property the user property to setOnce
     * @param value    the value of the user property
     * @return the same Identify object
     */
    public Identify setOnce(String property, int value) {
        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);
        return this;
    }

    /**
     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user
     * property will be ignored.
     *
     * @param property the user property to setOnce
     * @param value    the value of the user property
     * @return the same Identify object
     */
    public Identify setOnce(String property, long value) {
        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);
        return this;
    }

    /**
     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user
     * property will be ignored.
     *
     * @param property the user property to setOnce
     * @param value    the value of the user property
     * @return the same Identify object
     */
    public Identify setOnce(String property, String value) {
        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);
        return this;
    }

    /**
     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user
     * property will be ignored.
     *
     * @param property the user property to setOnce
     * @param values    the value of the user property
     * @return the same Identify object
     */
    public Identify setOnce(String property, JSONArray values) {
        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, values);
        return this;
    }

    /**
     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user
     * property will be ignored.
     *
     * @param property the user property to setOnce
     * @param values    the value of the user property
     * @return the same Identify object
     */
    public Identify setOnce(String property, JSONObject values) {
        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, values);
        return this;
    }

    /**
     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user
     * property will be ignored.
     *
     * @param property the user property to setOnce
     * @param values    the value of the user property
     * @return the same Identify object
     */
    public Identify setOnce(String property, boolean[] values) {
        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, booleanArrayToJSONArray(values));
        return this;
    }

    /**
     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user
     * property will be ignored.
     *
     * @param property the user property to setOnce
     * @param values    the value of the user property
     * @return the same Identify object
     */
    public Identify setOnce(String property, double[] values) {
        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, doubleArrayToJSONArray(values));
        return this;
    }

    /**
     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user
     * property will be ignored.
     *
     * @param property the user property to setOnce
     * @param values    the value of the user property
     * @return the same Identify object
     */
    public Identify setOnce(String property, float[] values) {
        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, floatArrayToJSONArray(values));
        return this;
    }

    /**
     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user
     * property will be ignored.
     *
     * @param property the user property to setOnce
     * @param values    the value of the user property
     * @return the same Identify object
     */
    public Identify setOnce(String property, int[] values) {
        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, intArrayToJSONArray(values));
        return this;
    }

    /**
     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user
     * property will be ignored.
     *
     * @param property the user property to setOnce
     * @param values    the value of the user property
     * @return the same Identify object
     */
    public Identify setOnce(String property, long[] values) {
        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, longArrayToJSONArray(values));
        return this;
    }

    /**
     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user
     * property will be ignored.
     *
     * @param property the user property to setOnce
     * @param values    the value of the user property
     * @return the same Identify object
     */
    public Identify setOnce(String property, String[] values) {
        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, stringArrayToJSONArray(values));
        return this;
    }


    /**
     * Sets a user property value. Existing values for that user property will be overwritten.
     *
     * @param property the user property to set
     * @param value    the value of the user property
     * @return the same Identify object
     */
    public Identify set(String property, boolean value) {
        addToUserProperties(Constants.AMP_OP_SET, property, value);
        return this;
    }

    /**
     * Sets a user property value. Existing values for that user property will be overwritten.
     *
     * @param property the user property to set
     * @param value    the value of the user property
     * @return the same Identify object
     */
    public Identify set(String property, double value) {
        addToUserProperties(Constants.AMP_OP_SET, property, value);
        return this;
    }

    /**
     * Sets a user property value. Existing values for that user property will be overwritten.
     *
     * @param property the user property to set
     * @param value    the value of the user property
     * @return the same Identify object
     */
    public Identify set(String property, float value) {
        addToUserProperties(Constants.AMP_OP_SET, property, value);
        return this;
    }

    /**
     * Sets a user property value. Existing values for that user property will be overwritten.
     *
     * @param property the user property to set
     * @param value    the value of the user property
     * @return the same Identify object
     */
    public Identify set(String property, int value) {
        addToUserProperties(Constants.AMP_OP_SET, property, value);
        return this;
    }

    /**
     * Sets a user property value. Existing values for that user property will be overwritten.
     *
     * @param property the user property to set
     * @param value    the value of the user property
     * @return the same Identify object
     */
    public Identify set(String property, long value) {
        addToUserProperties(Constants.AMP_OP_SET, property, value);
        return this;
    }

    /**
     * Sets a user property value. Existing values for that user property will be overwritten.
     *
     * @param property the user property to set
     * @param value    the value of the user property
     * @return the same Identify object
     */
    public Identify set(String property, String value) {
        addToUserProperties(Constants.AMP_OP_SET, property, value);
        return this;
    }

    /**
     * Sets a user property value. Existing values for that user property will be overwritten.
     *
     * @param property the user property to set
     * @param values    the value of the user property
     * @return the same Identify object
     */
    public Identify set(String property, JSONObject values) {
        addToUserProperties(Constants.AMP_OP_SET, property, values);
        return this;
    }

    /**
     * Sets a user property value. Existing values for that user property will be overwritten.
     *
     * @param property the user property to set
     * @param values    the value of the user property
     * @return the same Identify object
     */
    public Identify set(String property, JSONArray values) {
        addToUserProperties(Constants.AMP_OP_SET, property, values);
        return this;
    }

    /**
     * Sets a user property value. Existing values for that user property will be overwritten.
     *
     * @param property the user property to set
     * @param values    the value of the user property
     * @return the same Identify object
     */
    public Identify set(String property, boolean[] values) {
        addToUserProperties(Constants.AMP_OP_SET, property, booleanArrayToJSONArray(values));
        return this;
    }

    /**
     * Sets a user property value. Existing values for that user property will be overwritten.
     *
     * @param property the user property to set
     * @param values    the value of the user property
     * @return the same Identify object
     */
    public Identify set(String property, double[] values) {
        addToUserProperties(Constants.AMP_OP_SET, property, doubleArrayToJSONArray(values));
        return this;
    }

    /**
     * Sets a user property value. Existing values for that user property will be overwritten.
     *
     * @param property the user property to set
     * @param values    the value of the user property
     * @return the same Identify object
     */
    public Identify set(String property, float[] values) {
        addToUserProperties(Constants.AMP_OP_SET, property, floatArrayToJSONArray(values));
        return this;
    }

    /**
     * Sets a user property value. Existing values for that user property will be overwritten.
     *
     * @param property the user property to set
     * @param values    the value of the user property
     * @return the same Identify object
     */
    public Identify set(String property, int[] values) {
        addToUserProperties(Constants.AMP_OP_SET, property, intArrayToJSONArray(values));
        return this;
    }

    /**
     * Sets a user property value. Existing values for that user property will be overwritten.
     *
     * @param property the user property to set
     * @param values    the value of the user property
     * @return the same Identify object
     */
    public Identify set(String property, long[] values) {
        addToUserProperties(Constants.AMP_OP_SET, property, longArrayToJSONArray(values));
        return this;
    }

    /**
     * Sets a user property value. Existing values for that user property will be overwritten.
     *
     * @param property the user property to set
     * @param values    the value of the user property
     * @return the same Identify object
     */
    public Identify set(String property, String[] values) {
        addToUserProperties(Constants.AMP_OP_SET, property, stringArrayToJSONArray(values));
        return this;
    }


    /**
     * Increment a user property by some numerical value. If the user property does not have
     * a value set, it will be initialized to 0 before being incremented. Value can be
     * negative to decrement a user property value.
     *
     * @param property the user property to increment
     * @param value    the value (can be negative) to increment
     * @return the same Identify object
     */
    public Identify add(String property, double value) {
        addToUserProperties(Constants.AMP_OP_ADD, property, value);
        return this;
    }

    /**
     * Increment a user property by some numerical value. If the user property does not have
     * a value set, it will be initialized to 0 before being incremented. Value can be
     * negative to decrement a user property value.
     *
     * @param property the user property to increment
     * @param value    the value (can be negative) to increment
     * @return the same Identify object
     */
    public Identify add(String property, float value) {
        addToUserProperties(Constants.AMP_OP_ADD, property, value);
        return this;
    }

    /**
     * Increment a user property by some numerical value. If the user property does not have
     * a value set, it will be initialized to 0 before being incremented. Value can be
     * negative to decrement a user property value.
     *
     * @param property the user property to increment
     * @param value    the value (can be negative) to increment
     * @return the same Identify object
     */
    public Identify add(String property, int value) {
        addToUserProperties(Constants.AMP_OP_ADD, property, value);
        return this;
    }

    /**
     * Increment a user property by some numerical value. If the user property does not have
     * a value set, it will be initialized to 0 before being incremented. Value can be
     * negative to decrement a user property value.
     *
     * @param property the user property to increment
     * @param value    the value (can be negative) to increment
     * @return the same Identify object
     */
    public Identify add(String property, long value) {
        addToUserProperties(Constants.AMP_OP_ADD, property, value);
        return this;
    }

    /**
     * Increment a user property by some numerical value. If the user property does not have
     * a value set, it will be initialized to 0 before being incremented. Value can be
     * negative to decrement a user property value.
     *
     * @param property the user property to increment
     * @param value    the value (can be negative) to increment. Server-side we convert
     *                 the string into a number if possible.
     * @return the same Identify object
     */
    public Identify add(String property, String value) {
        addToUserProperties(Constants.AMP_OP_ADD, property, value);
        return this;
    }

    /**
     * Increment a user property by some numerical value. If the user property does not have
     * a value set, it will be initialized to 0 before being incremented. Value can be
     * negative to decrement a user property value.
     *
     * @param property the user property to increment
     * @param values    the value (can be negative) to increment. Server-side we flatten
     *                 dictionaries and apply add to each flattened property value.
     * @return the same Identify object
     */
    public Identify add(String property, JSONObject values) {
        addToUserProperties(Constants.AMP_OP_ADD, property, values);
        return this;
    }


    /**
     * Append a value or values to a user property. If the user property does not have a value
     * set, it will be initialized to an empty list before the new values are appended. If
     * the user property has an existing value and it is not a list, it will be converted into
     * a list with the new value(s) appended.
     *
     * @param property the user property property to which to append
     * @param value    the value being appended
     * @return the same Identify object
     */
    public Identify append(String property, boolean value) {
        addToUserProperties(Constants.AMP_OP_APPEND, property, value);
        return this;
    }

    /**
     * Append a value or values to a user property. If the user property does not have a value
     * set, it will be initialized to an empty list before the new values are appended. If
     * the user property has an existing value and it is not a list, it will be converted into
     * a list with the new value(s) appended.
     *
     * @param property the user property property to which to append
     * @param value    the value being appended
     * @return the same Identify object
     */
    public Identify append(String property, double value) {
        addToUserProperties(Constants.AMP_OP_APPEND, property, value);
        return this;
    }

    /**
     * Append a value or values to a user property. If the user property does not have a value
     * set, it will be initialized to an empty list before the new values are appended. If
     * the user property has an existing value and it is not a list, it will be converted into
     * a list with the new value(s) appended.
     *
     * @param property the user property property to which to append
     * @param value    the value being appended
     * @return the same Identify object
     */
    public Identify append(String property, float value) {
        addToUserProperties(Constants.AMP_OP_APPEND, property, value);
        return this;
    }

    /**
     * Append a value or values to a user property. If the user property does not have a value
     * set, it will be initialized to an empty list before the new values are appended. If
     * the user property has an existing value and it is not a list, it will be converted into
     * a list with the new value(s) appended.
     *
     * @param property the user property property to which to append
     * @param value    the value being appended
     * @return the same Identify object
     */
    public Identify append(String property, int value) {
        addToUserProperties(Constants.AMP_OP_APPEND, property, value);
        return this;
    }

    /**
     * Append a value or values to a user property. If the user property does not have a value
     * set, it will be initialized to an empty list before the new values are appended. If
     * the user property has an existing value and it is not a list, it will be converted into
     * a list with the new value(s) appended.
     *
     * @param property the user property property to which to append
     * @param value    the value being appended
     * @return the same Identify object
     */
    public Identify append(String property, long value) {
        addToUserProperties(Constants.AMP_OP_APPEND, property, value);
        return this;
    }

    /**
     * Append a value or values to a user property. If the user property does not have a value
     * set, it will be initialized to an empty list before the new values are appended. If
     * the user property has an existing value and it is not a list, it will be converted into
     * a list with the new value(s) appended.
     *
     * @param property the user property property to which to append
     * @param value    the value being appended
     * @return the same Identify object
     */
    public Identify append(String property, String value) {
        addToUserProperties(Constants.AMP_OP_APPEND, property, value);
        return this;
    }

    /**
     * Append a value or values to a user property. If the user property does not have a value
     * set, it will be initialized to an empty list before the new values are appended. If
     * the user property has an existing value and it is not a list, it will be converted into
     * a list with the new value(s) appended.
     *
     * @param property the user property property to which to append
     * @param values    the values being appended
     * @return the same Identify object
     */
    public Identify append(String property, JSONArray values) {
        addToUserProperties(Constants.AMP_OP_APPEND, property, values);
        return this;
    }

    /**
     * Append a value or values to a user property. If the user property does not have a value
     * set, it will be initialized to an empty list before the new values are appended. If
     * the user property has an existing value and it is not a list, it will be converted into
     * a list with the new value(s) appended.
     *
     * @param property the user property property to which to append
     * @param values    the values being appended. Server-side we flatten dictionaries and apply
     *                  append to each flattened property.
     * @return the same Identify object
     */
    public Identify append(String property, JSONObject values) {
        addToUserProperties(Constants.AMP_OP_APPEND, property, values);
        return this;
    }

    /**
     * Append a value or values to a user property. If the user property does not have a value
     * set, it will be initialized to an empty list before the new values are appended. If
     * the user property has an existing value and it is not a list, it will be converted into
     * a list with the new value(s) appended.
     *
     * @param property the user property property to which to append
     * @param values    the values being appended
     * @return the same Identify object
     */
    public Identify append(String property, boolean[] values) {
        addToUserProperties(Constants.AMP_OP_APPEND, property, booleanArrayToJSONArray(values));
        return this;
    }

    /**
     * Append a value or values to a user property. If the user property does not have a value
     * set, it will be initialized to an empty list before the new values are appended. If
     * the user property has an existing value and it is not a list, it will be converted into
     * a list with the new value(s) appended.
     *
     * @param property the user property property to which to append
     * @param values    the values being appended
     * @return the same Identify object
     */
    public Identify append(String property, double[] values) {
        addToUserProperties(Constants.AMP_OP_APPEND, property, doubleArrayToJSONArray(values));
        return this;
    }

    /**
     * Append a value or values to a user property. If the user property does not have a value
     * set, it will be initialized to an empty list before the new values are appended. If
     * the user property has an existing value and it is not a list, it will be converted into
     * a list with the new value(s) appended.
     *
     * @param property the user property property to which to append
     * @param values    the values being appended
     * @return the same Identify object
     */
    public Identify append(String property, float[] values) {
        addToUserProperties(Constants.AMP_OP_APPEND, property, floatArrayToJSONArray(values));
        return this;
    }

    /**
     * Append a value or values to a user property. If the user property does not have a value
     * set, it will be initialized to an empty list before the new values are appended. If
     * the user property has an existing value and it is not a list, it will be converted into
     * a list with the new value(s) appended.
     *
     * @param property the user property property to which to append
     * @param values    the values being appended
     * @return the same Identify object
     */
    public Identify append(String property, int[] values) {
        addToUserProperties(Constants.AMP_OP_APPEND, property, intArrayToJSONArray(values));
        return this;
    }

    /**
     * Append a value or values to a user property. If the user property does not have a value
     * set, it will be initialized to an empty list before the new values are appended. If
     * the user property has an existing value and it is not a list, it will be converted into
     * a list with the new value(s) appended.
     *
     * @param property the user property property to which to append
     * @param values    the values being appended
     * @return the same Identify object
     */
    public Identify append(String property, long[] values) {
        addToUserProperties(Constants.AMP_OP_APPEND, property, longArrayToJSONArray(values));
        return this;
    }

    /**
     * Append a value or values to a user property. If the user property does not have a value
     * set, it will be initialized to an empty list before the new values are appended. If
     * the user property has an existing value and it is not a list, it will be converted into
     * a list with the new value(s) appended.
     *
     * @param property the user property property to which to append
     * @param values    the values being appended
     * @return the same Identify object
     */
    public Identify append(String property, String[] values) {
        addToUserProperties(Constants.AMP_OP_APPEND, property, stringArrayToJSONArray(values));
        return this;
    }


    /**
     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) prepended.
     *
     * @param property the user property to which to append
     * @param value    the value being prepended
     * @return the same Identify object
     */
    public Identify prepend(String property, boolean value) {
        addToUserProperties(Constants.AMP_OP_PREPEND, property, value);
        return this;
    }

    /**
     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) prepended.
     *
     * @param property the user property to which to append
     * @param value    the value being prepended
     * @return the same Identify object
     */
    public Identify prepend(String property, double value) {
        addToUserProperties(Constants.AMP_OP_PREPEND, property, value);
        return this;
    }

    /**
     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) prepended.
     *
     * @param property the user property to which to append
     * @param value    the value being prepended
     * @return the same Identify object
     */
    public Identify prepend(String property, float value) {
        addToUserProperties(Constants.AMP_OP_PREPEND, property, value);
        return this;
    }

    /**
     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) prepended.
     *
     * @param property the user property to which to append
     * @param value    the value being prepended
     * @return the same Identify object
     */
    public Identify prepend(String property, int value) {
        addToUserProperties(Constants.AMP_OP_PREPEND, property, value);
        return this;
    }

    /**
     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) prepended.
     *
     * @param property the user property to which to append
     * @param value    the value being prepended
     * @return the same Identify object
     */
    public Identify prepend(String property, long value) {
        addToUserProperties(Constants.AMP_OP_PREPEND, property, value);
        return this;
    }

    /**
     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) prepended.
     *
     * @param property the user property to which to append
     * @param value    the value being prepended
     * @return the same Identify object
     */
    public Identify prepend(String property, String value) {
        addToUserProperties(Constants.AMP_OP_PREPEND, property, value);
        return this;
    }

    /**
     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) prepended.
     *
     * @param property the user property to which to append
     * @param values    the value being prepended
     * @return the same Identify object
     */
    public Identify prepend(String property, JSONArray values) {
        addToUserProperties(Constants.AMP_OP_PREPEND, property, values);
        return this;
    }

    /**
     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) prepended.
     *
     * @param property the user property to which to append
     * @param values    the values being prepended. Server-side we flatten dictionaries and apply
     *                  prepend to each flattened property.
     * @return the same Identify object
     */
    public Identify prepend(String property, JSONObject values) {
        addToUserProperties(Constants.AMP_OP_PREPEND, property, values);
        return this;
    }

    /**
     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) prepended.
     *
     * @param property the user property to which to append
     * @param values    the values being prepended
     * @return the same Identify object
     */
    public Identify prepend(String property, boolean[] values) {
        addToUserProperties(Constants.AMP_OP_PREPEND, property, booleanArrayToJSONArray(values));
        return this;
    }

    /**
     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) prepended.
     *
     * @param property the user property to which to append
     * @param values    the values being prepended
     * @return the same Identify object
     */
    public Identify prepend(String property, double[] values) {
        addToUserProperties(Constants.AMP_OP_PREPEND, property, doubleArrayToJSONArray(values));
        return this;
    }

    /**
     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) prepended.
     *
     * @param property the user property to which to append
     * @param values    the values being prepended
     * @return the same Identify object
     */
    public Identify prepend(String property, float[] values) {
        addToUserProperties(Constants.AMP_OP_PREPEND, property, floatArrayToJSONArray(values));
        return this;
    }

    /**
     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) prepended.
     *
     * @param property the user property to which to append
     * @param values    the values being prepended
     * @return the same Identify object
     */
    public Identify prepend(String property, int[] values) {
        addToUserProperties(Constants.AMP_OP_PREPEND, property, intArrayToJSONArray(values));
        return this;
    }

    /**
     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) prepended.
     *
     * @param property the user property to which to append
     * @param values    the values being prepended
     * @return the same Identify object
     */
    public Identify prepend(String property, long[] values) {
        addToUserProperties(Constants.AMP_OP_PREPEND, property, longArrayToJSONArray(values));
        return this;
    }

    /**
     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) prepended.
     *
     * @param property the user property to which to append
     * @param values    the values being prepended
     * @return the same Identify object
     */
    public Identify prepend(String property, String[] values) {
        addToUserProperties(Constants.AMP_OP_PREPEND, property, stringArrayToJSONArray(values));
        return this;
    }


    /**
     * Unset and remove a user property.
     *
     * @param property the user property to unset and remove.
     * @return the same Identify object
     */
    public Identify unset(String property) {
        addToUserProperties(Constants.AMP_OP_UNSET, property, "-");
        return this;
    }


    /**
     * Clear all user properties. <b>Note:</b> the result is irreversible! <b>Also Note:</b>
     * clearAll needs to be be sent on its own Identify object without any other operations.
     *
     * @return the same Identify object.
     */
    public Identify clearAll() {
        if (userPropertiesOperations.length() > 0) {
            if (!userProperties.contains(Constants.AMP_OP_CLEAR_ALL)) {
                AmplitudeLog.getLogger().w(TAG, String.format(
                   "Need to send $clearAll on its own Identify object without any other " +
                   "operations, ignoring $clearAll"
                ));
            }
            return this;
        }

        try {
            userPropertiesOperations.put(Constants.AMP_OP_CLEAR_ALL, "-");
        } catch (JSONException e) {
            AmplitudeLog.getLogger().e(TAG, e.toString());
        }
        return this;
    }

    /**
     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to preInsert
     * @param value    the values being preInsert
     * @return the same Identify object
     */
    public Identify preInsert(String property, boolean value) {
        addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);
        return this;
    }

    /**
     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to preInsert
     * @param value    the values being preInsert
     * @return the same Identify object
     */
    public Identify preInsert(String property, double value) {
        addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);
        return this;
    }

    /**
     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to preInsert
     * @param value    the values being preInsert
     * @return the same Identify object
     */
    public Identify preInsert(String property, float value) {
        addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);
        return this;
    }

    /**
     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to preInsert
     * @param value    the values being preInsert
     * @return the same Identify object
     */
    public Identify preInsert(String property, int value) {
        addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);
        return this;
    }

    /**
     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to preInsert
     * @param value    the values being preInsert
     * @return the same Identify object
     */
    public Identify preInsert(String property, long value) {
        addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);
        return this;
    }

    /**
     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to preInsert
     * @param value    the values being preInsert
     * @return the same Identify object
     */
    public Identify preInsert(String property, String value) {
        addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);
        return this;
    }

    /**
     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to preInsert
     * @param values    the values being preInsert
     * @return the same Identify object
     */
    public Identify preInsert(String property, JSONArray values) {
        addToUserProperties(Constants.AMP_OP_PREINSERT, property, values);
        return this;
    }

    /**
     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to preInsert
     * @param values    the values being preInsert
     * @return the same Identify object
     */
    public Identify preInsert(String property, JSONObject values) {
        addToUserProperties(Constants.AMP_OP_PREINSERT, property, values);
        return this;
    }


    /**
     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to preInsert
     * @param values    the values being preInsert
     * @return the same Identify object
     */
    public Identify preInsert(String property, boolean[] values) {
        addToUserProperties(Constants.AMP_OP_PREINSERT, property, booleanArrayToJSONArray(values));
        return this;
    }

    /**
     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to preInsert
     * @param values    the values being preInsert
     * @return the same Identify object
     */
    public Identify preInsert(String property, double[] values) {
        addToUserProperties(Constants.AMP_OP_PREINSERT, property, doubleArrayToJSONArray(values));
        return this;
    }

    /**
     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to preInsert
     * @param values    the values being preInsert
     * @return the same Identify object
     */
    public Identify preInsert(String property, float[] values){
        addToUserProperties(Constants.AMP_OP_PREINSERT, property, floatArrayToJSONArray(values));
        return this;
    }

    /**
     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to preInsert
     * @param values    the values being preInsert
     * @return the same Identify object
     */
    public Identify preInsert(String property, int[] values){
        addToUserProperties(Constants.AMP_OP_PREINSERT, property, intArrayToJSONArray(values));
        return this;
    }

    /**
     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to preInsert
     * @param values    the values being preInsert
     * @return the same Identify object
     */
    public Identify preInsert(String property, long[] values) {
        addToUserProperties(Constants.AMP_OP_PREINSERT, property, longArrayToJSONArray(values));
        return this;
    }

    /**
     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
     * front of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to preInsert
     * @param values    the values being preInsert
     * @return the same Identify object
     */
    public Identify preInsert(String property, String[] values) {
        addToUserProperties(Constants.AMP_OP_PREINSERT, property, stringArrayToJSONArray(values));
        return this;
    }

    /**
     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
     * end of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) post-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to postInsert
     * @param value    the values being preInsert
     * @return the same Identify object
     */
    public Identify postInsert(String property, boolean value) {
        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);
        return this;
    }

    /**
     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
     * end of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) post-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to postInsert 
     * @param value    the values being preInsert
     * @return the same Identify object
     */
    public Identify postInsert(String property, double value) {
        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);
        return this;
    }

    /**
     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
     * end of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) post-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to postInsert
     * @param value    the values being preInsert
     * @return the same Identify object
     */
    public Identify postInsert(String property, float value) {
        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);
        return this;
    }

    /**
     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
     * end of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) post-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to postInsert
     * @param value    the values being preInsert
     * @return the same Identify object
     */
    public Identify postInsert(String property, int value) {
        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);
        return this;
    }

    /**
     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
     * end of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) post-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to postInsert
     * @param value    the values being preInsert
     * @return the same Identify object
     */
    public Identify postInsert(String property, long value) {
        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);
        return this;
    }

    /**
     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
     * end of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) post-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to postInsert
     * @param value    the values being preInsert
     * @return the same Identify object
     */
    public Identify postInsert(String property, String value) {
        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);
        return this;
    }

    /**
     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
     * end of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) post-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to postInsert
     * @param values    the values being preInsert
     * @return the same Identify object
     */
    public Identify postInsert(String property, JSONArray values) {
        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, values);
        return this;
    }

    /**
     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
     * end of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) post-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to postInsert
     * @param values    the values being preInsert
     * @return the same Identify object
     */
    public Identify postInsert(String property, JSONObject values) {
        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, values);
        return this;
    }

    /**
     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
     * end of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) post-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to postInsert
     * @param values    the values being preInsert
     * @return the same Identify object
     */
    public Identify postInsert(String property, boolean[] values) {
        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, booleanArrayToJSONArray(values));
        return this;
    }

    /**
     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
     * end of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) post-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to postInsert
     * @param values    the values being preInsert
     * @return the same Identify object
     */
    public Identify postInsert(String property, double[] values) {
        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, doubleArrayToJSONArray(values));
        return this;
    }

    /**
     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
     * end of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) post-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to postInsert
     * @param values    the values being preInsert
     * @return the same Identify object
     */
    public Identify postInsert(String property, float[] values){
        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, floatArrayToJSONArray(values));
        return this;
    }

    /**
     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
     * end of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) post-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to postInsert
     * @param values    the values being preInsert
     * @return the same Identify object
     */
    public Identify postInsert(String property, int[] values){
        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, intArrayToJSONArray(values));
        return this;
    }

    /**
     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
     * end of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) post-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to postInsert
     * @param values    the values being preInsert
     * @return the same Identify object
     */
    public Identify postInsert(String property, long[] values) {
        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, longArrayToJSONArray(values));
        return this;
    }

    /**
     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
     * end of a given list. if the user property does not have a value set, it will be
     * initialized to an empty list before the new values are prepended. If the user property
     * has an existing value and it is not a list, it will be converted into a list with the
     * new value(s) post-insert. If the user property has an existing value, it will do no operation.
     *
     * @param property the user property to which to postInsert
     * @param values    the values being preInsert
     * @return the same Identify object
     */
    public Identify postInsert(String property, String[] values) {
        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, stringArrayToJSONArray(values));
        return this;
    }

    /**
     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.
     * If the user property has the matching value, it will remove that value from the given list.
     * If the user property does not have that value set, it will do no operation.
     *
     * @param property the user property to which to remove
     * @param value    the values being remove
     * @return the same Identify object
     */
    public Identify remove(String property, boolean value) {
        addToUserProperties(Constants.AMP_OP_REMOVE, property, value);
        return this;
    }

    /**
     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.
     * If the user property has the matching value, it will remove that value from the given list.
     * If the user property does not have that value set, it will do no operation.
     *
     * @param property the user property to which to remove
     * @param value    the values being remove
     * @return the same Identify object
     */
    public Identify remove(String property, double value) {
        addToUserProperties(Constants.AMP_OP_REMOVE, property, value);
        return this;
    }

    /**
     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.
     * If the user property has the matching value, it will remove that value from the given list.
     * If the user property does not have that value set, it will do no operation.
     *
     * @param property the user property to which to remove
     * @param value    the values being remove
     * @return the same Identify object
     */
    public Identify remove(String property, float value) {
        addToUserProperties(Constants.AMP_OP_REMOVE, property, value);
        return this;
    }

    /**
     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.
     * If the user property has the matching value, it will remove that value from the given list.
     * If the user property does not have that value set, it will do no operation.
     *
     * @param property the user property to which to remove
     * @param value    the values being remove
     * @return the same Identify object
     */
    public Identify remove(String property, int value) {
        addToUserProperties(Constants.AMP_OP_REMOVE, property, value);
        return this;
    }

    /**
     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.
     * If the user property has the matching value, it will remove that value from the given list.
     * If the user property does not have that value set, it will do no operation.
     *
     * @param property the user property to which to remove
     * @param value    the values being remove
     * @return the same Identify object
     */
    public Identify remove(String property, long value) {
        addToUserProperties(Constants.AMP_OP_REMOVE, property, value);
        return this;
    }

    /**
     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.
     * If the user property has the matching value, it will remove that value from the given list.
     * If the user property does not have that value set, it will do no operation.
     *
     * @param property the user property to which to remove
     * @param value    the values being remove
     * @return the same Identify object
     */
    public Identify remove(String property, String value) {
        addToUserProperties(Constants.AMP_OP_REMOVE, property, value);
        return this;
    }

    /**
     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.
     * If the user property has the matching value, it will remove that value from the given list.
     * If the user property does not have that value set, it will do no operation.
     *
     * @param property the user property to which to remove
     * @param values    the values being remove
     * @return the same Identify object
     */
    public Identify remove(String property, JSONArray values) {
        addToUserProperties(Constants.AMP_OP_REMOVE, property, values);
        return this;
    }

    /**
     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.
     * If the user property has the matching value, it will remove that value from the given list.
     * If the user property does not have that value set, it will do no operation.
     *
     * @param property the user property to which to remove
     * @param values    the values being remove
     * @return the same Identify object
     */
    public Identify remove(String property, JSONObject values) {
        addToUserProperties(Constants.AMP_OP_REMOVE, property, values);
        return this;
    }

    /**
     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.
     * If the user property has the matching value, it will remove that value from the given list.
     * If the user property does not have that value set, it will do no operation.
     *
     * @param property the user property to which to remove
     * @param values    the values being remove
     * @return the same Identify object
     */
    public Identify remove(String property, boolean[] values) {
        addToUserProperties(Constants.AMP_OP_REMOVE, property, booleanArrayToJSONArray(values));
        return this;
    }

    /**
     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.
     * If the user property has the matching value, it will remove that value from the given list.
     * If the user property does not have that value set, it will do no operation.
     *
     * @param property the user property to which to remove
     * @param values    the values being remove
     * @return the same Identify object
     */
    public Identify remove(String property, double[] values) {
        addToUserProperties(Constants.AMP_OP_REMOVE, property, doubleArrayToJSONArray(values));
        return this;
    }

    /**
     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.
     * If the user property has the matching value, it will remove that value from the given list.
     * If the user property does not have that value set, it will do no operation.
     *
     * @param property the user property to which to remove
     * @param values    the values being remove
     * @return the same Identify object
     */
    public Identify remove(String property, float[] values){
        addToUserProperties(Constants.AMP_OP_REMOVE, property, floatArrayToJSONArray(values));
        return this;
    }

    /**
     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.
     * If the user property has the matching value, it will remove that value from the given list.
     * If the user property does not have that value set, it will do no operation.
     *
     * @param property the user property to which to remove
     * @param values    the values being remove
     * @return the same Identify object
     */
    public Identify remove(String property, int[] values){
        addToUserProperties(Constants.AMP_OP_REMOVE, property, intArrayToJSONArray(values));
        return this;
    }

    /**
     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.
     * If the user property has the matching value, it will remove that value from the given list.
     * If the user property does not have that value set, it will do no operation.
     *
     * @param property the user property to which to remove
     * @param values    the values being remove
     * @return the same Identify object
     */
    public Identify remove(String property, long[] values) {
        addToUserProperties(Constants.AMP_OP_REMOVE, property, longArrayToJSONArray(values));
        return this;
    }

    /**
     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.
     * If the user property has the matching value, it will remove that value from the given list.
     * If the user property does not have that value set, it will do no operation.
     *
     * @param property the user property to which to remove
     * @param values    the values being remove
     * @return the same Identify object
     */
    public Identify remove(String property, String[] values) {
        addToUserProperties(Constants.AMP_OP_REMOVE, property, stringArrayToJSONArray(values));
        return this;
    }

    private void addToUserProperties(String operation, String property, Object value) {
        if (Utils.isEmptyString(property)) {
            AmplitudeLog.getLogger().w(TAG, String.format(
               "Attempting to perform operation %s with a null or empty string property, ignoring",
                operation
            ));
            return;
        }

        if (value == null) {
            AmplitudeLog.getLogger().w(TAG, String.format(
                "Attempting to perform operation %s with null value for property %s, ignoring",
                operation, property
            ));
            return;
        }

        // check that clearAll wasn't already used in this Identify
        if (userPropertiesOperations.has(Constants.AMP_OP_CLEAR_ALL)) {
            AmplitudeLog.getLogger().w(TAG, String.format(
                "This Identify already contains a $clearAll operation, ignoring operation %s",
                operation
            ));
            return;
        }

        // check if property already used in previous operation
        if (userProperties.contains(property)) {
            AmplitudeLog.getLogger().w(TAG, String.format(
                "Already used property %s in previous operation, ignoring operation %s",
                property, operation
            ));
            return;
        }

        try {
            if (!userPropertiesOperations.has(operation)) {
                userPropertiesOperations.put(operation, new JSONObject());
            }
            userPropertiesOperations.getJSONObject(operation).put(property, value);
            userProperties.add(property);
        } catch (JSONException e) {
            AmplitudeLog.getLogger().e(TAG, e.toString());
        }
    }

    private JSONArray booleanArrayToJSONArray(boolean[] values) {
        JSONArray array = new JSONArray();
        for (boolean value : values) array.put(value);
        return array;
    }

    private JSONArray floatArrayToJSONArray(float[] values) {
        JSONArray array = new JSONArray();
        for (float value : values) {
            try {
                array.put(value);
            } catch (JSONException e) {
                AmplitudeLog.getLogger().e(TAG, String.format(
                    "Error converting float %f to JSON: %s", value, e.toString()
                ));
            }
        }
        return array;
    }

    private JSONArray doubleArrayToJSONArray(double[] values) {
        JSONArray array = new JSONArray();
        for (double value : values) {
            try {
                array.put(value);
            } catch (JSONException e) {
                AmplitudeLog.getLogger().e(TAG, String.format(
                    "Error converting double %d to JSON: %s", value, e.toString()
                ));
            }
        }
        return array;
    }

    private JSONArray intArrayToJSONArray(int[] values) {
        JSONArray array = new JSONArray();
        for (int value : values) array.put(value);
        return array;
    }

    private JSONArray longArrayToJSONArray(long[] values) {
        JSONArray array = new JSONArray();
        for (long value : values) array.put(value);
        return array;
    }

    private JSONArray stringArrayToJSONArray(String[] values) {
        JSONArray array = new JSONArray();
        for (String value : values) array.put(value);
        return array;
    }

    /**
     * Sets user property.
     *
     * @param property the property
     * @param value    the value
     * @return the user property
     */
    Identify setUserProperty(String property, Object value) {
        addToUserProperties(Constants.AMP_OP_SET, property, value);
        return this;
    }

    /**
     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user
     * property will be ignored. <b>Note:</b> this method has been deprecated. Please use one
     * with a different signature.
     *
     * @param property the user property to setOnce
     * @param value    the value of the user property
     * @return the same Identify object
     * @deprecated
     */
    public Identify setOnce(String property, Object value) {
        AmplitudeLog.getLogger().w(
            TAG,
            "This version of setOnce is deprecated. Please use one with a different signature."
        );
        return this;
    }

    /**
     * Sets a user property value. Existing values for that user property will be overwritten.
     * <b>Note:</b> this method has been deprecated. Please use one with a different signature.
     *
     * @param property the user property to set
     * @param value    the value of the user property
     * @return the same Identify object
     * @deprecated
     */
    public Identify set(String property, Object value) {
        AmplitudeLog.getLogger().w(
            TAG,
            "This version of set is deprecated. Please use one with a different signature."
        );
        return this;
    }

    /**
     * Public method that exposes the user property operations JSON blob.
      * @return a copy of the User Property Operations JSONObject. If copying fails, returns
     *      an empty JSONObject
     */
    public JSONObject getUserPropertiesOperations() {
        try {
            return new JSONObject(userPropertiesOperations.toString());
        } catch (JSONException e) {
            AmplitudeLog.getLogger().e(TAG, e.toString());
        }
        return new JSONObject();
    }
}
