/*
 * Copyright (c) 2019 the Eclipse Milo Authors
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */

package org.eclipse.milo.opcua.stack.core.util;

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.function.Function;

import com.google.common.base.Preconditions;
import com.google.common.primitives.Ints;

public class ArrayUtil {

    /**
     * Flatten a multi-dimensional array into a one-dimensional array.
     *
     * @param array the array to flatten.
     * @return a 1-dimensional array.
     */
    public static Object flatten(Object array) {
        Preconditions.checkArgument(array.getClass().isArray(), "array");

        Class<?> type = getType(array);
        int[] dimensions = getDimensions(array);
        int length = length(dimensions);

        Object flattened = Array.newInstance(type, length);

        flatten(array, flattened, dimensions, 0);

        return flattened;
    }

    private static void flatten(Object array, Object flattened, int[] dimensions, int offset) {
        if (dimensions.length == 1) {
            for (int i = 0; i < dimensions[0]; i++) {
                Object element = Array.get(array, i);
                Array.set(flattened, i + offset, element);
            }
        } else {
            int[] tail = Arrays.copyOfRange(dimensions, 1, dimensions.length);

            for (int i = 0; i < dimensions[0]; i++) {
                Object a = Array.get(array, i);
                flatten(a, flattened, tail, offset + i * length(tail));
            }
        }
    }

    /**
     * Un-flatten a one-dimensional array into an multi-dimensional array based on the provided dimensions.
     *
     * @param array      the 1-dimensional array to un-flatten.
     * @param dimensions the dimensions to un-flatten to.
     * @return a multi-dimensional array of the provided dimensions.
     */
    public static Object unflatten(Object array, int[] dimensions) {
        Class<?> type = getType(array);

        return unflatten(type, array, dimensions, 0);
    }

    private static Object unflatten(Class<?> type, Object array, int[] dimensions, int offset) {
        Object a;
        if (dimensions.length == 1) {
            a = Array.newInstance(type, dimensions[0]);

            for (int i = 0; i < dimensions[0]; i++) {
                Array.set(a, i, Array.get(array, offset + i));
            }
        } else {
            a = Array.newInstance(type, dimensions);

            int[] tail = Arrays.copyOfRange(dimensions, 1, dimensions.length);

            for (int i = 0; i < dimensions[0]; i++) {
                Object element = unflatten(type, array, tail, offset + i * length(tail));
                Array.set(a, i, element);
            }
        }
        return a;
    }

    public static int[] getDimensions(Object array) {
        int[] dimensions = new int[0];
        Class<?> type = array.getClass();

        while (type.isArray()) {
            int length = array != null ? Array.getLength(array) : 0;
            dimensions = Ints.concat(dimensions, new int[]{length});

            array = length > 0 ? Array.get(array, 0) : null;
            type = type.getComponentType();
        }

        return dimensions;
    }

    public static Class<?> getType(Object array) {
        Class<?> type = array.getClass();

        while (type.isArray()) {
            type = type.getComponentType();
        }

        return type;
    }


    private static int length(int[] tail) {
        int product = 1;
        for (int aTail : tail) {
            product *= aTail;
        }
        return product;
    }

    /**
     * Transform the values in an array from one type to another using a transformation function.
     * <p>
     * Handles multi-dimensional arrays by flattening before the transformation and un-flattening afterwards.
     *
     * @param array     the original array.
     * @param transform a function that transforms from {@code F} to {@code T}.
     * @param toType    the destination type.
     * @return a new array of the same size and dimensions with all elements transformed from {@code F} to {@code T}
     * using {@code transform}.
     */
    public static <F, T> Object transformArray(Object array, Function<F, T> transform, Class<T> toType) {
        int[] dimensions = ArrayUtil.getDimensions(array);

        Object flatArray = dimensions.length > 1 ? flatten(array) : array;

        Object transformedArray = transformFlatArray(flatArray, transform, toType);

        return dimensions.length > 1 ? unflatten(transformedArray, dimensions) : transformedArray;
    }

    private static <F, T> Object transformFlatArray(Object flatArray, Function<F, T> transform, Class<T> toType) {
        int length = Array.getLength(flatArray);
        Object array = Array.newInstance(toType, length);
        for (int i = 0; i < length; i++) {
            @SuppressWarnings("unchecked")
            Object transformed = transform.apply((F) Array.get(flatArray, i));
            Array.set(array, i, transformed);
        }
        return array;
    }

}
