/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed 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 android.uwb.util;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.ParcelUuid;
import android.os.PersistableBundle;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.TreeSet;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/** @hide */
public class PersistableBundleUtils {
//    private static final String LIST_KEY_FORMAT = "LIST_ITEM_%d";
//    private static final String COLLECTION_SIZE_KEY = "COLLECTION_LENGTH";
//    private static final String MAP_KEY_FORMAT = "MAP_KEY_%d";
//    private static final String MAP_VALUE_FORMAT = "MAP_VALUE_%d";
//
//    private static final String PARCEL_UUID_KEY = "PARCEL_UUID";
//    private static final String BYTE_ARRAY_KEY = "BYTE_ARRAY_KEY";
//    private static final String INTEGER_KEY = "INTEGER_KEY";
//    private static final String STRING_KEY = "STRING_KEY";
//
//    private final static char[] HEX_DIGITS =
//            {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
//    private final static char[] HEX_LOWER_CASE_DIGITS =
//            {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};


//    /**
//     * Functional interface to convert an object of the specified type to a PersistableBundle.
//     *
//     * @param <T> the type of the source object
//     */
//    public interface Serializer<T> {
//        /**
//         * Converts this object to a PersistableBundle.
//         *
//         * @return the PersistableBundle representation of this object
//         */
//        PersistableBundle toPersistableBundle(T obj);
//    }
//
//    /**
//     * Functional interface used to create an object of the specified type from a PersistableBundle.
//     *
//     * @param <T> the type of the resultant object
//     */
//    public interface Deserializer<T> {
//        /**
//         * Creates an instance of specified type from a PersistableBundle representation.
//         *
//         * @param in the PersistableBundle representation
//         * @return an instance of the specified type
//         */
//        T fromPersistableBundle(PersistableBundle in);
//    }
//
//    /** Serializer to convert an integer to a PersistableBundle. */
//    public static final Serializer<Integer> INTEGER_SERIALIZER =
//            (i) -> {
//                final PersistableBundle result = new PersistableBundle();
//                result.putInt(INTEGER_KEY, i);
//                return result;
//            };
//
//    /** Deserializer to convert a PersistableBundle to an integer. */
//    public static final Deserializer<Integer> INTEGER_DESERIALIZER =
//            (bundle) -> {
//                Objects.requireNonNull(bundle, "PersistableBundle is null");
//                return bundle.getInt(INTEGER_KEY);
//            };
//
//    /** Serializer to convert s String to a PersistableBundle. */
//    public static final Serializer<String> STRING_SERIALIZER =
//            (i) -> {
//                final PersistableBundle result = new PersistableBundle();
//                result.putString(STRING_KEY, i);
//                return result;
//            };
//
//    /** Deserializer to convert a PersistableBundle to a String. */
//    public static final Deserializer<String> STRING_DESERIALIZER =
//            (bundle) -> {
//                Objects.requireNonNull(bundle, "PersistableBundle is null");
//                return bundle.getString(STRING_KEY);
//            };
//
//    /**
//     * Converts a ParcelUuid to a PersistableBundle.
//     *
//     * <p>To avoid key collisions, NO additional key/value pairs should be added to the returned
//     * PersistableBundle object.
//     *
//     * @param uuid a ParcelUuid instance to persist
//     * @return the PersistableBundle instance
//     */
//    public static PersistableBundle fromParcelUuid(ParcelUuid uuid) {
//        final PersistableBundle result = new PersistableBundle();
//
//        result.putString(PARCEL_UUID_KEY, uuid.toString());
//
//        return result;
//    }
//
//    /**
//     * Converts from a PersistableBundle to a ParcelUuid.
//     *
//     * @param bundle the PersistableBundle containing the ParcelUuid
//     * @return the ParcelUuid instance
//     */
//    public static ParcelUuid toParcelUuid(PersistableBundle bundle) {
//        return ParcelUuid.fromString(bundle.getString(PARCEL_UUID_KEY));
//    }
//
//    /**
//     * Converts from a list of Persistable objects to a single PersistableBundle.
//     *
//     * <p>To avoid key collisions, NO additional key/value pairs should be added to the returned
//     * PersistableBundle object.
//     *
//     * @param <T>        the type of the objects to convert to the PersistableBundle
//     * @param in         the list of objects to be serialized into a PersistableBundle
//     * @param serializer an implementation of the {@link Serializer} functional interface that
//     *                   converts an object of type T to a PersistableBundle
//     */
//    @NonNull
//    public static <T> PersistableBundle fromList(
//            @NonNull List<T> in, @NonNull Serializer<T> serializer) {
//        final PersistableBundle result = new PersistableBundle();
//
//        result.putInt(COLLECTION_SIZE_KEY, in.size());
//        for (int i = 0; i < in.size(); i++) {
//            final String key = String.format(LIST_KEY_FORMAT, i);
//            result.putPersistableBundle(key, serializer.toPersistableBundle(in.get(i)));
//        }
//        return result;
//    }
//
//    /**
//     * Converts from a PersistableBundle to a list of objects.
//     *
//     * @param <T>          the type of the objects to convert from a PersistableBundle
//     * @param in           the PersistableBundle containing the persisted list
//     * @param deserializer an implementation of the {@link Deserializer} functional interface that
//     *                     builds the relevant type of objects.
//     */
//    @NonNull
//    public static <T> List<T> toList(
//            @NonNull PersistableBundle in, @NonNull Deserializer<T> deserializer) {
//        final int listLength = in.getInt(COLLECTION_SIZE_KEY);
//        final ArrayList<T> result = new ArrayList<>(listLength);
//
//        for (int i = 0; i < listLength; i++) {
//            final String key = String.format(LIST_KEY_FORMAT, i);
//            final PersistableBundle item = in.getPersistableBundle(key);
//
//            result.add(deserializer.fromPersistableBundle(item));
//        }
//        return result;
//    }
//
//    // TODO: b/170513329 Delete #fromByteArray and #toByteArray once BaseBundle#putByteArray and
//    // BaseBundle#getByteArray are exposed.
//
//    /**
//     * Converts a byte array to a PersistableBundle.
//     *
//     * <p>To avoid key collisions, NO additional key/value pairs should be added to the returned
//     * PersistableBundle object.
//     *
//     * @param array a byte array instance to persist
//     * @return the PersistableBundle instance
//     */
//    public static PersistableBundle fromByteArray(byte[] array) {
//        final PersistableBundle result = new PersistableBundle();
//
//        result.putString(BYTE_ARRAY_KEY, toHexString(array));
//
//        return result;
//    }
//
//    // Copied from com.android.internal.util.HexDump
//    @UnsupportedAppUsage
//    public static String toHexString(byte[] array, int offset, int length) {
//        return toHexString(array, offset, length, true);
//    }
//
//    public static String toHexString(byte[] array, int offset, int length, boolean upperCase) {
//        char[] digits = upperCase ? HEX_DIGITS : HEX_LOWER_CASE_DIGITS;
//        char[] buf = new char[length * 2];
//
//        int bufIndex = 0;
//        for (int i = offset; i < offset + length; i++) {
//            byte b = array[i];
//            buf[bufIndex++] = digits[(b >>> 4) & 0x0F];
//            buf[bufIndex++] = digits[b & 0x0F];
//        }
//
//        return new String(buf);
//    }
//
//    @UnsupportedAppUsage
//    public static String toHexString(byte[] array) {
//        return toHexString(array, 0, array.length, true);
//    }
//
//    @UnsupportedAppUsage
//    public static String toHexString(int i) {
//        return toHexString(toByteArray(i));
//    }
//
//    public static byte[] toByteArray(int i) {
//        byte[] array = new byte[4];
//
//        array[3] = (byte) (i & 0xFF);
//        array[2] = (byte) ((i >> 8) & 0xFF);
//        array[1] = (byte) ((i >> 16) & 0xFF);
//        array[0] = (byte) ((i >> 24) & 0xFF);
//
//        return array;
//    }
//
//    @UnsupportedAppUsage
//    public static byte[] hexStringToByteArray(String hexString) {
//        int length = hexString.length();
//        byte[] buffer = new byte[length / 2];
//
//        for (int i = 0; i < length; i += 2) {
//            buffer[i / 2] = (byte) ((toByte(hexString.charAt(i)) << 4) | toByte(
//                    hexString.charAt(i + 1)));
//        }
//
//        return buffer;
//    }
//
//    private static int toByte(char c) {
//        if (c >= '0' && c <= '9') return (c - '0');
//        if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
//        if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
//
//        throw new RuntimeException("Invalid hex char '" + c + "'");
//    }
//
//    /**
//     * Converts from a PersistableBundle to a byte array.
//     *
//     * @param bundle the PersistableBundle containing the byte array
//     * @return the byte array instance
//     */
//    public static byte[] toByteArray(PersistableBundle bundle) {
//        Objects.requireNonNull(bundle, "PersistableBundle is null");
//
//        String hex = bundle.getString(BYTE_ARRAY_KEY);
//        if (hex == null || hex.length() % 2 != 0) {
//            throw new IllegalArgumentException("PersistableBundle contains invalid byte array");
//        }
//
//        return hexStringToByteArray(hex);
//    }
//
//    /**
//     * Converts from a Map of Persistable objects to a single PersistableBundle.
//     *
//     * <p>To avoid key collisions, NO additional key/value pairs should be added to the returned
//     * PersistableBundle object.
//     *
//     * @param <K>             the type of the map-key to convert to the PersistableBundle
//     * @param <V>             the type of the map-value to convert to the PersistableBundle
//     * @param in              the Map of objects implementing the {@link Persistable} interface
//     * @param keySerializer   an implementation of the {@link Serializer} functional interface that
//     *                        converts a map-key of type T to a PersistableBundle
//     * @param valueSerializer an implementation of the {@link Serializer} functional interface that
//     *                        converts a map-value of type E to a PersistableBundle
//     */
//    @NonNull
//    public static <K, V> PersistableBundle fromMap(
//            @NonNull Map<K, V> in,
//            @NonNull Serializer<K> keySerializer,
//            @NonNull Serializer<V> valueSerializer) {
//        final PersistableBundle result = new PersistableBundle();
//
//        result.putInt(COLLECTION_SIZE_KEY, in.size());
//        int i = 0;
//        for (Entry<K, V> entry : in.entrySet()) {
//            final String keyKey = String.format(MAP_KEY_FORMAT, i);
//            final String valueKey = String.format(MAP_VALUE_FORMAT, i);
//            result.putPersistableBundle(keyKey, keySerializer.toPersistableBundle(entry.getKey()));
//            result.putPersistableBundle(
//                    valueKey, valueSerializer.toPersistableBundle(entry.getValue()));
//
//            i++;
//        }
//
//        return result;
//    }
//
//    /**
//     * Converts from a PersistableBundle to a Map of objects.
//     *
//     * <p>In an attempt to preserve ordering, the returned map will be a LinkedHashMap. However, the
//     * guarantees on the ordering can only ever be as strong as the map that was serialized in
//     * {@link fromMap()}. If the initial map that was serialized had no ordering guarantees, the
//     * deserialized map similarly may be of a non-deterministic order.
//     *
//     * @param <K>               the type of the map-key to convert from a PersistableBundle
//     * @param <V>               the type of the map-value to convert from a PersistableBundle
//     * @param in                the PersistableBundle containing the persisted Map
//     * @param keyDeserializer   an implementation of the {@link Deserializer} functional interface
//     *                          that builds the relevant type of map-key.
//     * @param valueDeserializer an implementation of the {@link Deserializer} functional interface
//     *                          that builds the relevant type of map-value.
//     * @return An instance of the parsed map as a LinkedHashMap (in an attempt to preserve
//     * ordering).
//     */
//    @NonNull
//    public static <K, V> LinkedHashMap<K, V> toMap(
//            @NonNull PersistableBundle in,
//            @NonNull Deserializer<K> keyDeserializer,
//            @NonNull Deserializer<V> valueDeserializer) {
//        final int mapSize = in.getInt(COLLECTION_SIZE_KEY);
//        final LinkedHashMap<K, V> result = new LinkedHashMap<>(mapSize);
//
//        for (int i = 0; i < mapSize; i++) {
//            final String keyKey = String.format(MAP_KEY_FORMAT, i);
//            final String valueKey = String.format(MAP_VALUE_FORMAT, i);
//            final PersistableBundle keyBundle = in.getPersistableBundle(keyKey);
//            final PersistableBundle valueBundle = in.getPersistableBundle(valueKey);
//
//            final K key = keyDeserializer.fromPersistableBundle(keyBundle);
//            final V value = valueDeserializer.fromPersistableBundle(valueBundle);
//            result.put(key, value);
//        }
//        return result;
//    }
//
//    /**
//     * Converts a PersistableBundle into a disk-stable byte array format
//     *
//     * @param bundle the PersistableBundle to be converted to a disk-stable format
//     * @return the byte array representation of the PersistableBundle
//     */
//    @Nullable
//    public static byte[] toDiskStableBytes(@NonNull PersistableBundle bundle) throws IOException {
//        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
//        bundle.writeToStream(outputStream);
//        return outputStream.toByteArray();
//    }
//
//    /**
//     * Converts from a disk-stable byte array format to a PersistableBundle
//     *
//     * @param bytes the disk-stable byte array
//     * @return the PersistableBundle parsed from this byte array.
//     */
//    public static PersistableBundle fromDiskStableBytes(@NonNull byte[] bytes) throws IOException {
//        final ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
//        return PersistableBundle.readFromStream(inputStream);
//    }
//
//    /**
//     * Ensures safe reading and writing of {@link PersistableBundle}s to and from disk.
//     *
//     * <p>This class will enforce exclusion between reads and writes using the standard semantics of
//     * a ReadWriteLock. Specifically, concurrent readers ARE allowed, but reads/writes from/to the
//     * file are mutually exclusive. In other words, for an unbounded number n, the acceptable states
//     * are n readers, OR 1 writer (but not both).
//     */
//    public static class LockingReadWriteHelper {
//        private final ReadWriteLock mDiskLock = new ReentrantReadWriteLock();
//        private final String mPath;
//
//        public LockingReadWriteHelper(@NonNull String path) {
//            mPath = Objects.requireNonNull(path, "fileName was null");
//        }
//
//        /**
//         * Reads the {@link PersistableBundle} from the disk.
//         *
//         * @return the PersistableBundle, if the file existed, or null otherwise
//         */
//        @Nullable
//        public PersistableBundle readFromDisk() throws IOException {
//            try {
//                mDiskLock.readLock().lock();
//                final File file = new File(mPath);
//                if (!file.exists()) {
//                    return null;
//                }
//
//                try (FileInputStream fis = new FileInputStream(file)) {
//                    return PersistableBundle.readFromStream(fis);
//                }
//            } finally {
//                mDiskLock.readLock().unlock();
//            }
//        }
//
//        /**
//         * Writes a {@link PersistableBundle} to disk.
//         *
//         * @param bundle the {@link PersistableBundle} to write to disk
//         */
//        public void writeToDisk(@NonNull PersistableBundle bundle) throws IOException {
//            Objects.requireNonNull(bundle, "bundle was null");
//
//            try {
//                mDiskLock.writeLock().lock();
//                final File file = new File(mPath);
//                if (!file.exists()) {
//                    file.getParentFile().mkdirs();
//                }
//
//                try (FileOutputStream fos = new FileOutputStream(file)) {
//                    bundle.writeToStream(fos);
//                }
//            } finally {
//                mDiskLock.writeLock().unlock();
//            }
//        }
//    }
//
//    /**
//     * Returns a copy of the persistable bundle with only the specified keys
//     *
//     * <p>This allows for holding minimized copies for memory-saving purposes.
//     */
//    @NonNull
//    public static PersistableBundle minimizeBundle(
//            @NonNull PersistableBundle bundle, String... keys) {
//        final PersistableBundle minimized = new PersistableBundle();
//
//        if (bundle == null) {
//            return minimized;
//        }
//
//        for (String key : keys) {
//            if (bundle.containsKey(key)) {
//                final Object value = bundle.get(key);
//                if (value == null) {
//                    continue;
//                }
//
//                if (value instanceof Boolean) {
//                    minimized.putBoolean(key, (Boolean) value);
//                } else if (value instanceof boolean[]) {
//                    minimized.putBooleanArray(key, (boolean[]) value);
//                } else if (value instanceof Double) {
//                    minimized.putDouble(key, (Double) value);
//                } else if (value instanceof double[]) {
//                    minimized.putDoubleArray(key, (double[]) value);
//                } else if (value instanceof Integer) {
//                    minimized.putInt(key, (Integer) value);
//                } else if (value instanceof int[]) {
//                    minimized.putIntArray(key, (int[]) value);
//                } else if (value instanceof Long) {
//                    minimized.putLong(key, (Long) value);
//                } else if (value instanceof long[]) {
//                    minimized.putLongArray(key, (long[]) value);
//                } else if (value instanceof String) {
//                    minimized.putString(key, (String) value);
//                } else if (value instanceof String[]) {
//                    minimized.putStringArray(key, (String[]) value);
//                } else if (value instanceof PersistableBundle) {
//                    minimized.putPersistableBundle(key, (PersistableBundle) value);
//                } else {
//                    continue;
//                }
//            }
//        }
//
//        return minimized;
//    }

    /** Builds a stable hashcode */
    public static int getHashCode(@Nullable PersistableBundle bundle) {
        if (bundle == null) {
            return -1;
        }

        int iterativeHashcode = 0;
        TreeSet<String> treeSet = new TreeSet<>(bundle.keySet());
        for (String key : treeSet) {
            Object val = bundle.get(key);
            if (val instanceof PersistableBundle) {
                iterativeHashcode =
                        Objects.hash(iterativeHashcode, key, getHashCode((PersistableBundle) val));
            } else {
                iterativeHashcode = Objects.hash(iterativeHashcode, key, val);
            }
        }

        return iterativeHashcode;
    }

    /** Checks for persistable bundle equality */
    public static boolean isEqual(
            @Nullable PersistableBundle left, @Nullable PersistableBundle right) {
        // Check for pointer equality & null equality
        if (Objects.equals(left, right)) {
            return true;
        }

        // If only one of the two is null, but not the other, not equal by definition.
        if (Objects.isNull(left) != Objects.isNull(right)) {
            return false;
        }

        if (!left.keySet().equals(right.keySet())) {
            return false;
        }

        for (String key : left.keySet()) {
            Object leftVal = left.get(key);
            Object rightVal = right.get(key);

            // Check for equality
            if (Objects.equals(leftVal, rightVal)) {
                continue;
            } else if (Objects.isNull(leftVal) != Objects.isNull(rightVal)) {
                // If only one of the two is null, but not the other, not equal by definition.
                return false;
            } else if (!Objects.equals(leftVal.getClass(), rightVal.getClass())) {
                // If classes are different, not equal by definition.
                return false;
            }
            if (leftVal instanceof PersistableBundle) {
                if (!isEqual((PersistableBundle) leftVal, (PersistableBundle) rightVal)) {
                    return false;
                }
            } else if (leftVal.getClass().isArray()) {
                if (leftVal instanceof boolean[]) {
                    if (!Arrays.equals((boolean[]) leftVal, (boolean[]) rightVal)) {
                        return false;
                    }
                } else if (leftVal instanceof double[]) {
                    if (!Arrays.equals((double[]) leftVal, (double[]) rightVal)) {
                        return false;
                    }
                } else if (leftVal instanceof int[]) {
                    if (!Arrays.equals((int[]) leftVal, (int[]) rightVal)) {
                        return false;
                    }
                } else if (leftVal instanceof long[]) {
                    if (!Arrays.equals((long[]) leftVal, (long[]) rightVal)) {
                        return false;
                    }
                } else if (!Arrays.equals((Object[]) leftVal, (Object[]) rightVal)) {
                    return false;
                }
            } else {
                if (!Objects.equals(leftVal, rightVal)) {
                    return false;
                }
            }
        }

        return true;
    }

//    /**
//     * Wrapper class around PersistableBundles to allow equality comparisons
//     *
//     * <p>This class exposes the minimal getters to retrieve values.
//     */
//    public static class PersistableBundleWrapper {
//        @NonNull
//        private final PersistableBundle mBundle;
//
//        public PersistableBundleWrapper(@NonNull PersistableBundle bundle) {
//            mBundle = Objects.requireNonNull(bundle, "Bundle was null");
//        }
//
//        /**
//         * Retrieves the integer associated with the provided key.
//         *
//         * @param key          the string key to query
//         * @param defaultValue the value to return if key does not exist
//         * @return the int value, or the default
//         */
//        public int getInt(String key, int defaultValue) {
//            return mBundle.getInt(key, defaultValue);
//        }
//
//        @Override
//        public int hashCode() {
//            return getHashCode(mBundle);
//        }
//
//        @Override
//        public boolean equals(Object obj) {
//            if (!(obj instanceof PersistableBundleWrapper)) {
//                return false;
//            }
//
//            final PersistableBundleWrapper other = (PersistableBundleWrapper) obj;
//
//            return isEqual(mBundle, other.mBundle);
//        }
//    }
}
