/*
 * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.internal.foreign.abi;

import jdk.internal.access.JavaLangAccess;
import jdk.internal.access.JavaLangInvokeAccess;
import jdk.internal.access.SharedSecrets;
import jdk.internal.foreign.CABI;
import jdk.internal.foreign.abi.aarch64.linux.LinuxAArch64Linker;
import jdk.internal.foreign.abi.aarch64.macos.MacOsAArch64Linker;
import jdk.internal.foreign.abi.x64.sysv.SysVx64Linker;
import jdk.internal.foreign.abi.x64.windows.Windowsx64Linker;
import jdk.internal.vm.annotation.ForceInline;

import java.lang.foreign.Linker;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.GroupLayout;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SegmentScope;
import java.lang.foreign.SegmentAllocator;
import java.lang.foreign.VaList;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.invoke.VarHandle;
import java.lang.ref.Reference;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static java.lang.foreign.ValueLayout.*;
import static java.lang.invoke.MethodHandles.*;
import static java.lang.invoke.MethodType.methodType;

public final class SharedUtils {

    private SharedUtils() {
    }

    private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess();
    private static final JavaLangInvokeAccess JLIA = SharedSecrets.getJavaLangInvokeAccess();

    private static final MethodHandle MH_ALLOC_BUFFER;
    private static final MethodHandle MH_BUFFER_COPY;
    private static final MethodHandle MH_REACHABILITY_FENCE;

    static {
        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            MH_ALLOC_BUFFER = lookup.findVirtual(SegmentAllocator.class, "allocate",
                    methodType(MemorySegment.class, MemoryLayout.class));
            MH_BUFFER_COPY = lookup.findStatic(SharedUtils.class, "bufferCopy",
                    methodType(MemorySegment.class, MemorySegment.class, MemorySegment.class));
            MH_REACHABILITY_FENCE = lookup.findStatic(Reference.class, "reachabilityFence",
                    methodType(void.class, Object.class));
        } catch (ReflectiveOperationException e) {
            throw new BootstrapMethodError(e);
        }
    }

    // this allocator should be used when no allocation is expected
    public static final SegmentAllocator THROWING_ALLOCATOR = (size, align) -> {
        throw new IllegalStateException("Cannot get here");
    };

    public static long alignUp(long addr, long alignment) {
        return ((addr - 1) | (alignment - 1)) + 1;
    }

    /**
     * Takes a MethodHandle that takes an input buffer as a first argument (a MemorySegment), and returns nothing,
     * and adapts it to return a MemorySegment, by allocating a MemorySegment for the input
     * buffer, calling the target MethodHandle, and then returning the allocated MemorySegment.
     *
     * This allows viewing a MethodHandle that makes use of in memory return (IMR) as a MethodHandle that just returns
     * a MemorySegment without requiring a pre-allocated buffer as an explicit input.
     *
     * @param handle the target handle to adapt
     * @param cDesc the function descriptor of the native function (with actual return layout)
     * @return the adapted handle
     */
    public static MethodHandle adaptDowncallForIMR(MethodHandle handle, FunctionDescriptor cDesc, CallingSequence sequence) {
        if (handle.type().returnType() != void.class)
            throw new IllegalArgumentException("return expected to be void for in memory returns: " + handle.type());
        int imrAddrIdx = sequence.numLeadingParams();
        if (handle.type().parameterType(imrAddrIdx) != MemorySegment.class)
            throw new IllegalArgumentException("MemorySegment expected as third param: " + handle.type());
        if (cDesc.returnLayout().isEmpty())
            throw new IllegalArgumentException("Return layout needed: " + cDesc);

        MethodHandle ret = identity(MemorySegment.class); // (MemorySegment) MemorySegment
        handle = collectArguments(ret, 1, handle); // (MemorySegment, MemorySegment, SegmentAllocator, MemorySegment, ...) MemorySegment
        handle = mergeArguments(handle, 0, 1 + imrAddrIdx);  // (MemorySegment, MemorySegment, SegmentAllocator, ...) MemorySegment
        handle = collectArguments(handle, 0, insertArguments(MH_ALLOC_BUFFER, 1, cDesc.returnLayout().get())); // (SegmentAllocator, MemorySegment, SegmentAllocator, ...) MemorySegment
        handle = mergeArguments(handle, 0, 2);  // (SegmentAllocator, MemorySegment, ...) MemorySegment
        handle = swapArguments(handle, 0, 1); // (MemorySegment, SegmentAllocator, ...) MemorySegment
        return handle;
    }

    /**
     * Takes a MethodHandle that returns a MemorySegment, and adapts it to take an input buffer as a first argument
     * (a MemorySegment), and upon invocation, copies the contents of the returned MemorySegment into the input buffer
     * passed as the first argument.
     *
     * @param target the target handle to adapt
     * @return the adapted handle
     */
    public static MethodHandle adaptUpcallForIMR(MethodHandle target, boolean dropReturn) {
        if (target.type().returnType() != MemorySegment.class)
            throw new IllegalArgumentException("Must return MemorySegment for IMR");

        target = collectArguments(MH_BUFFER_COPY, 1, target); // (MemorySegment, ...) MemorySegment

        if (dropReturn) { // no handling for return value, need to drop it
            target = dropReturn(target);
        } else {
            // adjust return type so it matches the inferred type of the effective
            // function descriptor
            target = target.asType(target.type().changeReturnType(MemorySegment.class));
        }

        return target;
    }

    private static MemorySegment bufferCopy(MemorySegment dest, MemorySegment buffer) {
        return dest.copyFrom(buffer);
    }

    public static Class<?> primitiveCarrierForSize(long size, boolean useFloat) {
        if (useFloat) {
            if (size == 4) {
                return float.class;
            } else if (size == 8) {
                return double.class;
            }
        } else {
            if (size == 1) {
                return byte.class;
            } else if (size == 2) {
                return short.class;
            } else if (size <= 4) {
                return int.class;
            } else if (size <= 8) {
                return long.class;
            }
        }

        throw new IllegalArgumentException("No type for size: " + size + " isFloat=" + useFloat);
    }

    public static Linker getSystemLinker() {
        return switch (CABI.current()) {
            case WIN_64 -> Windowsx64Linker.getInstance();
            case SYS_V -> SysVx64Linker.getInstance();
            case LINUX_AARCH_64 -> LinuxAArch64Linker.getInstance();
            case MAC_OS_AARCH_64 -> MacOsAArch64Linker.getInstance();
        };
    }

    public static String toJavaStringInternal(MemorySegment segment, long start) {
        int len = strlen(segment, start);
        byte[] bytes = new byte[len];
        MemorySegment.copy(segment, JAVA_BYTE, start, bytes, 0, len);
        return new String(bytes, StandardCharsets.UTF_8);
    }

    private static int strlen(MemorySegment segment, long start) {
        // iterate until overflow (String can only hold a byte[], whose length can be expressed as an int)
        for (int offset = 0; offset >= 0; offset++) {
            byte curr = segment.get(JAVA_BYTE, start + offset);
            if (curr == 0) {
                return offset;
            }
        }
        throw new IllegalArgumentException("String too large");
    }

    static Map<VMStorage, Integer> indexMap(Binding.Move[] moves) {
        return IntStream.range(0, moves.length)
                        .boxed()
                        .collect(Collectors.toMap(i -> moves[i].storage(), i -> i));
    }

    static MethodHandle mergeArguments(MethodHandle mh, int sourceIndex, int destIndex) {
        MethodType oldType = mh.type();
        Class<?> sourceType = oldType.parameterType(sourceIndex);
        Class<?> destType = oldType.parameterType(destIndex);
        if (sourceType != destType) {
            // TODO meet?
            throw new IllegalArgumentException("Parameter types differ: " + sourceType + " != " + destType);
        }
        MethodType newType = oldType.dropParameterTypes(destIndex, destIndex + 1);
        int[] reorder = new int[oldType.parameterCount()];
        if (destIndex < sourceIndex) {
            sourceIndex--;
        }
        for (int i = 0, index = 0; i < reorder.length; i++) {
            if (i != destIndex) {
                reorder[i] = index++;
            } else {
                reorder[i] = sourceIndex;
            }
        }
        return permuteArguments(mh, newType, reorder);
    }


    static MethodHandle swapArguments(MethodHandle mh, int firstArg, int secondArg) {
        MethodType mtype = mh.type();
        int[] perms = new int[mtype.parameterCount()];
        MethodType swappedType = MethodType.methodType(mtype.returnType());
        for (int i = 0 ; i < perms.length ; i++) {
            int dst = i;
            if (i == firstArg) dst = secondArg;
            if (i == secondArg) dst = firstArg;
            perms[i] = dst;
            swappedType = swappedType.appendParameterTypes(mtype.parameterType(dst));
        }
        return permuteArguments(mh, swappedType, perms);
    }

    private static MethodHandle reachabilityFenceHandle(Class<?> type) {
        return MH_REACHABILITY_FENCE.asType(MethodType.methodType(void.class, type));
    }

    static void handleUncaughtException(Throwable t) {
        if (t != null) {
            t.printStackTrace();
            JLA.exit(1);
        }
    }

    public static long unboxSegment(MemorySegment segment) {
        if (!segment.isNative()) {
            throw new IllegalArgumentException("Heap segment not allowed: " + segment);
        }
        return segment.address();
    }

    public static void checkExceptions(MethodHandle target) {
        Class<?>[] exceptions = JLIA.exceptionTypes(target);
        if (exceptions != null && exceptions.length != 0) {
            throw new IllegalArgumentException("Target handle may throw exceptions: " + Arrays.toString(exceptions));
        }
    }

    public static MethodHandle maybeInsertAllocator(FunctionDescriptor descriptor, MethodHandle handle) {
        if (descriptor.returnLayout().isEmpty() || !(descriptor.returnLayout().get() instanceof GroupLayout)) {
            // not returning segment, just insert a throwing allocator
            handle = insertArguments(handle, 1, THROWING_ALLOCATOR);
        }
        return handle;
    }

    @ForceInline
    public static void checkSymbol(MemorySegment symbol) {
        Objects.requireNonNull(symbol);
        if (symbol.equals(MemorySegment.NULL))
            throw new IllegalArgumentException("Symbol is NULL: " + symbol);
    }

    public static VaList newVaList(Consumer<VaList.Builder> actions, SegmentScope scope) {
        return switch (CABI.current()) {
            case WIN_64 -> Windowsx64Linker.newVaList(actions, scope);
            case SYS_V -> SysVx64Linker.newVaList(actions, scope);
            case LINUX_AARCH_64 -> LinuxAArch64Linker.newVaList(actions, scope);
            case MAC_OS_AARCH_64 -> MacOsAArch64Linker.newVaList(actions, scope);
        };
    }

    public static VaList newVaListOfAddress(long address, SegmentScope scope) {
        return switch (CABI.current()) {
            case WIN_64 -> Windowsx64Linker.newVaListOfAddress(address, scope);
            case SYS_V -> SysVx64Linker.newVaListOfAddress(address, scope);
            case LINUX_AARCH_64 -> LinuxAArch64Linker.newVaListOfAddress(address, scope);
            case MAC_OS_AARCH_64 -> MacOsAArch64Linker.newVaListOfAddress(address, scope);
        };
    }

    public static VaList emptyVaList() {
        return switch (CABI.current()) {
            case WIN_64 -> Windowsx64Linker.emptyVaList();
            case SYS_V -> SysVx64Linker.emptyVaList();
            case LINUX_AARCH_64 -> LinuxAArch64Linker.emptyVaList();
            case MAC_OS_AARCH_64 -> MacOsAArch64Linker.emptyVaList();
        };
    }

    static void checkType(Class<?> actualType, Class<?> expectedType) {
        if (expectedType != actualType) {
            throw new IllegalArgumentException(
                    String.format("Invalid operand type: %s. %s expected", actualType, expectedType));
        }
    }

    public static NoSuchElementException newVaListNSEE(MemoryLayout layout) {
        return new NoSuchElementException("No such element: " + layout);
    }

    public static final class SimpleVaArg {
        public final MemoryLayout layout;
        public final Object value;

        public SimpleVaArg(MemoryLayout layout, Object value) {
            this.layout = layout;
            this.value = value;
        }

        public VarHandle varHandle() {
            return layout.varHandle();
        }
    }

    public static final class EmptyVaList implements VaList {

        private final MemorySegment address;

        public EmptyVaList(MemorySegment address) {
            this.address = address;
        }

        private static UnsupportedOperationException uoe() {
            return new UnsupportedOperationException("Empty VaList");
        }

        @Override
        public int nextVarg(ValueLayout.OfInt layout) {
            throw uoe();
        }

        @Override
        public long nextVarg(ValueLayout.OfLong layout) {
            throw uoe();
        }

        @Override
        public double nextVarg(ValueLayout.OfDouble layout) {
            throw uoe();
        }

        @Override
        public MemorySegment nextVarg(ValueLayout.OfAddress layout) {
            throw uoe();
        }

        @Override
        public MemorySegment nextVarg(GroupLayout layout, SegmentAllocator allocator) {
            throw uoe();
        }

        @Override
        public void skip(MemoryLayout... layouts) {
            throw uoe();
        }

        @Override
        public VaList copy() {
            return this;
        }

        @Override
        public MemorySegment segment() {
            return address;
        }
    }

    static void writeOverSized(MemorySegment ptr, Class<?> type, Object o) {
        // use VH_LONG for integers to zero out the whole register in the process
        if (type == long.class) {
            ptr.set(JAVA_LONG_UNALIGNED, 0, (long) o);
        } else if (type == int.class) {
            ptr.set(JAVA_LONG_UNALIGNED, 0, (int) o);
        } else if (type == short.class) {
            ptr.set(JAVA_LONG_UNALIGNED, 0, (short) o);
        } else if (type == char.class) {
            ptr.set(JAVA_LONG_UNALIGNED, 0, (char) o);
        } else if (type == byte.class) {
            ptr.set(JAVA_LONG_UNALIGNED, 0, (byte) o);
        } else if (type == float.class) {
            ptr.set(JAVA_FLOAT_UNALIGNED, 0, (float) o);
        } else if (type == double.class) {
            ptr.set(JAVA_DOUBLE_UNALIGNED, 0, (double) o);
        } else if (type == boolean.class) {
            boolean b = (boolean)o;
            ptr.set(JAVA_LONG_UNALIGNED, 0, b ? (long)1 : (long)0);
        } else {
            throw new IllegalArgumentException("Unsupported carrier: " + type);
        }
    }

    static void write(MemorySegment ptr, Class<?> type, Object o) {
        if (type == long.class) {
            ptr.set(JAVA_LONG_UNALIGNED, 0, (long) o);
        } else if (type == int.class) {
            ptr.set(JAVA_INT_UNALIGNED, 0, (int) o);
        } else if (type == short.class) {
            ptr.set(JAVA_SHORT_UNALIGNED, 0, (short) o);
        } else if (type == char.class) {
            ptr.set(JAVA_CHAR_UNALIGNED, 0, (char) o);
        } else if (type == byte.class) {
            ptr.set(JAVA_BYTE, 0, (byte) o);
        } else if (type == float.class) {
            ptr.set(JAVA_FLOAT_UNALIGNED, 0, (float) o);
        } else if (type == double.class) {
            ptr.set(JAVA_DOUBLE_UNALIGNED, 0, (double) o);
        } else if (type == boolean.class) {
            ptr.set(JAVA_BOOLEAN, 0, (boolean) o);
        } else {
            throw new IllegalArgumentException("Unsupported carrier: " + type);
        }
    }

    static Object read(MemorySegment ptr, Class<?> type) {
        if (type == long.class) {
            return ptr.get(JAVA_LONG_UNALIGNED, 0);
        } else if (type == int.class) {
            return ptr.get(JAVA_INT_UNALIGNED, 0);
        } else if (type == short.class) {
            return ptr.get(JAVA_SHORT_UNALIGNED, 0);
        } else if (type == char.class) {
            return ptr.get(JAVA_CHAR_UNALIGNED, 0);
        } else if (type == byte.class) {
            return ptr.get(JAVA_BYTE, 0);
        } else if (type == float.class) {
            return ptr.get(JAVA_FLOAT_UNALIGNED, 0);
        } else if (type == double.class) {
            return ptr.get(JAVA_DOUBLE_UNALIGNED, 0);
        } else if (type == boolean.class) {
            return ptr.get(JAVA_BOOLEAN, 0);
        } else {
            throw new IllegalArgumentException("Unsupported carrier: " + type);
        }
    }
}
