package com.termux.shared.data;

import android.graphics.Color;
import android.graphics.Point;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.common.base.Strings;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class DataUtils {

    /**
     * Max safe limit of data size to prevent TransactionTooLargeException when transferring data
     * inside or to other apps via transactions.
     */
    // 100KB
    public static final int TRANSACTION_SIZE_LIMIT_IN_BYTES = 100 * 1024;

    private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();

    public static String getTruncatedCommandOutput(String text, int maxLength, boolean fromEnd, boolean onNewline, boolean addPrefix) {
        if (text == null)
            return null;
        String prefix = "(truncated) ";
        if (addPrefix)
            maxLength = maxLength - prefix.length();
        if (maxLength < 0 || text.length() < maxLength)
            return text;
        if (fromEnd) {
            text = text.substring(0, maxLength);
        } else {
            int cutOffIndex = text.length() - maxLength;
            if (onNewline) {
                int nextNewlineIndex = text.indexOf('\n', cutOffIndex);
                if (nextNewlineIndex != -1 && nextNewlineIndex != text.length() - 1) {
                    cutOffIndex = nextNewlineIndex + 1;
                }
            }
            text = text.substring(cutOffIndex);
        }
        if (addPrefix)
            text = prefix + text;
        return text;
    }

    /**
     * Replace a sub string in each item of a {@link String[]}.
     *
     * @param array The {@link String[]} to replace in.
     * @param find The sub string to replace.
     * @param replace The sub string to replace with.
     */
    public static void replaceSubStringsInStringArrayItems(String[] array, String find, String replace) {
        if (array == null || array.length == 0)
            return;
        for (int i = 0; i < array.length; i++) {
            array[i] = array[i].replace(find, replace);
        }
    }

    /**
     * Get the {@code float} from a {@link String}.
     *
     * @param value The {@link String} value.
     * @param def The default value if failed to read a valid value.
     * @return Returns the {@code float} value after parsing the {@link String} value, otherwise
     * returns default if failed to read a valid value, like in case of an exception.
     */
    public static float getFloatFromString(String value, float def) {
        if (value == null)
            return def;
        try {
            return Float.parseFloat(value);
        } catch (Exception e) {
            return def;
        }
    }

    /**
     * Get the {@code int} from a {@link String}.
     *
     * @param value The {@link String} value.
     * @param def The default value if failed to read a valid value.
     * @return Returns the {@code int} value after parsing the {@link String} value, otherwise
     * returns default if failed to read a valid value, like in case of an exception.
     */
    public static int getIntFromString(String value, int def) {
        if (value == null)
            return def;
        try {
            return Integer.parseInt(value);
        } catch (Exception e) {
            return def;
        }
    }

    /**
     * Get the {@code String} from an {@link Integer}.
     *
     * @param value The {@link Integer} value.
     * @param def The default {@link String} value.
     * @return Returns {@code value} if it is not {@code null}, otherwise returns {@code def}.
     */
    public static String getStringFromInteger(Integer value, String def) {
        return (value == null) ? def : String.valueOf((int) value);
    }

    /**
     * Get the {@code hex string} from a {@link byte[]}.
     *
     * @param bytes The {@link byte[]} value.
     * @return Returns the {@code hex string} value.
     */
    public static String bytesToHex(byte[] bytes) {
        char[] hexChars = new char[bytes.length * 2];
        for (int j = 0; j < bytes.length; j++) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = HEX_ARRAY[v >>> 4];
            hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
        }
        return new String(hexChars);
    }

    /**
     * Get an {@code int} from {@link Bundle} that is stored as a {@link String}.
     *
     * @param bundle The {@link Bundle} to get the value from.
     * @param key The key for the value.
     * @param def The default value if failed to read a valid value.
     * @return Returns the {@code int} value after parsing the {@link String} value stored in
     * {@link Bundle}, otherwise returns default if failed to read a valid value,
     * like in case of an exception.
     */
    public static int getIntStoredAsStringFromBundle(Bundle bundle, String key, int def) {
        if (bundle == null)
            return def;
        return getIntFromString(bundle.getString(key, Integer.toString(def)), def);
    }

    /**
     * If value is not in the range [min, max], set it to either min or max.
     */
    public static int clamp(int value, int min, int max) {
        return Math.min(Math.max(value, min), max);
    }

    /**
     * If value is not in the range [min, max], set it to default.
     */
    public static float rangedOrDefault(float value, float def, float min, float max) {
        if (value < min || value > max)
            return def;
        else
            return value;
    }

    /**
     * Add a space indent to a {@link String}. Each indent is 4 space characters long.
     *
     * @param string The {@link String} to add indent to.
     * @param count The indent count.
     * @return Returns the indented {@link String}.
     */
    public static String getSpaceIndentedString(String string, int count) {
        if (string == null || string.isEmpty())
            return string;
        else
            return getIndentedString(string, "    ", count);
    }

    /**
     * Add a tab indent to a {@link String}. Each indent is 1 tab character long.
     *
     * @param string The {@link String} to add indent to.
     * @param count The indent count.
     * @return Returns the indented {@link String}.
     */
    public static String getTabIndentedString(String string, int count) {
        if (string == null || string.isEmpty())
            return string;
        else
            return getIndentedString(string, "\t", count);
    }

    /**
     * Add an indent to a {@link String}.
     *
     * @param string The {@link String} to add indent to.
     * @param indent The indent characters.
     * @param count The indent count.
     * @return Returns the indented {@link String}.
     */
    public static String getIndentedString(String string, @NonNull String indent, int count) {
        if (string == null || string.isEmpty())
            return string;
        else
            return string.replaceAll("(?m)^", Strings.repeat(indent, Math.max(count, 1)));
    }

    /**
     * Get the object itself if it is not {@code null}, otherwise default.
     *
     * @param object The {@link Object} to check.
     * @param def The default {@link Object}.
     * @return Returns {@code object} if it is not {@code null}, otherwise returns {@code def}.
     */
    public static <T> T getDefaultIfNull(@Nullable T object, @Nullable T def) {
        return (object == null) ? def : object;
    }

    /**
     * Get the {@link String} itself if it is not {@code null} or empty, otherwise default.
     *
     * @param value The {@link String} to check.
     * @param def The default {@link String}.
     * @return Returns {@code value} if it is not {@code null} or empty, otherwise returns {@code def}.
     */
    public static String getDefaultIfUnset(@Nullable String value, String def) {
        return (value == null || value.isEmpty()) ? def : value;
    }

    /**
     * Check if a string is null or empty.
     */
    public static boolean isNullOrEmpty(String string) {
        return string == null || string.isEmpty();
    }

    /**
     * Get size of a serializable object.
     */
    public static long getSerializedSize(Serializable object) {
        if (object == null)
            return 0;
        try {
            ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteOutputStream);
            objectOutputStream.writeObject(object);
            objectOutputStream.flush();
            objectOutputStream.close();
            return byteOutputStream.toByteArray().length;
        } catch (Exception e) {
            return -1;
        }
    }

    /**
     * Wrapper for {@link #getIntColorFromString(String, int, boolean)} with `setAlpha` `false`.
     */
    public static int getIntColorFromString(String value, int def) {
        if (value == null)
            return def;
        try {
            return Color.parseColor(value);
        } catch (Exception e) {
            return def;
        }
    }

    /**
     * Get an {@code int} color from {@link String} with alpha value change. If {@code setAlpha}
     * is {@code true} and given value is missing alpha then set it using def alpha.
     *
     * @param value    The {@link String} value.
     * @param def      The default value if failed to read a valid value.
     * @param setAlpha The {@code boolean} value that decides whether to set alpha or not.
     * @return Returns the {@code int} color value after parsing the {@link String}
     * value, otherwise returns default value.
     */
    public static int getIntColorFromString(String value, int def, boolean setAlpha) {
        if (value == null)
            return def;
        try {
            int color = Color.parseColor(value);
            if (setAlpha && value.length() == 7) {
                // Use alpha value of `def` color and rgb value of given `value`.
                color = (def & 0xff000000) | (color & 0x00ffffff);
            }
            return color;
        } catch (Exception e) {
            return def;
        }
    }

    /**
     * Exchanges the value of x and y in {@link Point}.
     *
     * @param point The original source point to swap.
     * @return Returns new swaped point.
     */
    public static Point swap(Point point) {
        return new Point(point.y, point.x);
    }
}
