/*
 * Copyright (C) 2008-2010 Wayne Meissner
 *
 * This file is part of the JNR 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 jnr.ffi.provider.jffi;

import com.kenai.jffi.Function;
import jnr.ffi.CallingConvention;
import jnr.ffi.NativeType;
import jnr.ffi.provider.ParameterType;
import jnr.ffi.provider.ResultType;
import jnr.x86asm.Assembler;
import jnr.x86asm.Mem;
import jnr.x86asm.Register;

import static jnr.ffi.provider.jffi.CodegenUtils.sig;
import static jnr.x86asm.Asm.X86_32;
import static jnr.x86asm.Asm.al;
import static jnr.x86asm.Asm.ax;
import static jnr.x86asm.Asm.byte_ptr;
import static jnr.x86asm.Asm.dword_ptr;
import static jnr.x86asm.Asm.eax;
import static jnr.x86asm.Asm.edx;
import static jnr.x86asm.Asm.esp;
import static jnr.x86asm.Asm.imm;
import static jnr.x86asm.Asm.qword_ptr;
import static jnr.x86asm.Asm.word_ptr;

/**
 * Stub compiler for i386 unix
 */
final class X86_32StubCompiler extends AbstractX86StubCompiler {

    X86_32StubCompiler(jnr.ffi.Runtime runtime) {
        super(runtime);
    }

    boolean canCompile(ResultType returnType, ParameterType[] parameterTypes, CallingConvention convention) {

        switch (returnType.getNativeType()) {
            case VOID:
            case SCHAR:
            case UCHAR:
            case SSHORT:
            case USHORT:
            case SINT:
            case UINT:
            case SLONG:
            case ULONG:
            case SLONGLONG:
            case ULONGLONG:
            case FLOAT:
            case DOUBLE:
            case ADDRESS:
                break;

            default:
                return false;
        }

        // There is only one calling convention; SYSV, so abort if someone tries to use stdcall
        if (convention != CallingConvention.DEFAULT) {
            return false;
        }

        int fCount = 0;
        int iCount = 0;

        for (ParameterType t : parameterTypes) {
            switch (t.getNativeType()) {
                case SCHAR:
                case UCHAR:
                case SSHORT:
                case USHORT:
                case SINT:
                case UINT:
                case SLONG:
                case ULONG:
                case SLONGLONG:
                case ULONGLONG:
                case ADDRESS:
                    ++iCount;
                    break;

                case FLOAT:
                case DOUBLE:
                    ++fCount;
                    break;

                default:
                    // Fail on anything else
                    return false;
            }
        }

        // We can only safely compile methods with up to 6 integer and 8 floating point parameters
        return true;
    }


    @Override
    void compile(Function function, String name, ResultType resultType, ParameterType[] parameterTypes, Class resultClass, Class[] parameterClasses, CallingConvention convention, boolean saveErrno) {

        int psize = 0;
        for (ParameterType t : parameterTypes) {
            psize += parameterSize(t);
        }

        int rsize = resultSize(resultType);


        //
        // JNI functions all look like:
        // foo(JNIEnv* env, jobject self, arg...)

        // We need to align the stack to 16 bytes, then copy all the old args
        // into the new parameter space.
        // It already has 4 bytes pushed (the return address) so we need to account for that.
        //        
        final int stackadj = align(Math.max(psize, rsize) + 4, 16) - 4;

        Assembler a = new Assembler(X86_32);

        a.sub(esp, imm(stackadj));

        // copy and convert the parameters from the orig stack to the new location
        for (int i = 0, srcoff = 0, dstoff = 0; i < parameterTypes.length; i++)  {
            int srcParameterSize = parameterSize(parameterClasses[i]);
            int dstParameterSize = parameterSize(parameterTypes[i]);
            int disp = stackadj + 4 + 8 + srcoff;

            switch (parameterTypes[i].getNativeType()) {
                case SCHAR:
                case SSHORT:
                    a.movsx(eax, ptr(esp, disp, parameterTypes[i].getNativeType()));
                    break;

                case UCHAR:
                case USHORT:
                    a.movzx(eax, ptr(esp, disp, parameterTypes[i].getNativeType()));
                    break;

                default:
                    a.mov(eax, dword_ptr(esp, disp));
                    break;
            }
            a.mov(dword_ptr(esp, dstoff), eax);

            if (dstParameterSize > 4) {
                if (parameterTypes[i].getNativeType() == NativeType.SLONGLONG && long.class != parameterClasses[i]) {
                    // sign extend from int.class -> long long
                    a.sar(eax, imm(31));

                } else if (parameterTypes[i].getNativeType() == NativeType.ULONGLONG && long.class != parameterClasses[i]) {
                    // zero extend from int.class -> unsigned long long
                    a.mov(dword_ptr(esp, dstoff + 4), imm(0));

                } else {
                    a.mov(eax, dword_ptr(esp, disp + 4));
                }
                a.mov(dword_ptr(esp, dstoff + 4), eax);
            }

            dstoff += dstParameterSize;
            srcoff += srcParameterSize;
        }


        // Call to the actual native function
        a.call(imm(function.getFunctionAddress() & 0xffffffffL));
        
        if (saveErrno) {
            int save = 0;
            switch (resultType.getNativeType()) {
                case FLOAT:
                    a.fstp(dword_ptr(esp, save));
                    break;

                case DOUBLE:
                    a.fstp(qword_ptr(esp, save));
                    break;

                case SLONGLONG:
                case ULONGLONG:
                    a.mov(dword_ptr(esp, save), eax);
                    a.mov(dword_ptr(esp, save + 4), edx);
                    break;

                case VOID:
                    // No need to save for void values
                    break;

                default:
                    a.mov(dword_ptr(esp, save), eax);
            }

            // Save the errno in a thread-local variable
            a.call(imm(errnoFunctionAddress & 0xffffffffL));

            // Retrieve return value and put it back in the appropriate return register
            switch (resultType.getNativeType()) {
                case FLOAT:
                    a.fld(dword_ptr(esp, save));
                    break;

                case DOUBLE:
                    a.fld(qword_ptr(esp, save));
                    break;

                case SCHAR:
                    a.movsx(eax, byte_ptr(esp, save));
                    break;

                case UCHAR:
                    a.movzx(eax, byte_ptr(esp, save));
                    break;

                case SSHORT:
                    a.movsx(eax, word_ptr(esp, save));
                    break;

                case USHORT:
                    a.movzx(eax, word_ptr(esp, save));
                    break;

                case SLONGLONG:
                case ULONGLONG:
                    a.mov(eax, dword_ptr(esp, save));
                    a.mov(edx, dword_ptr(esp, save + 4));
                    break;

                case VOID:
                    // No need to save for void values
                    break;

                default:
                    a.mov(eax, dword_ptr(esp, save));
            }

        } else {

            switch (resultType.getNativeType()) {
                case SCHAR:
                    a.movsx(eax, al);
                    break;

                case UCHAR:
                    a.movzx(eax, al);
                    break;

                case SSHORT:
                    a.movsx(eax, ax);
                    break;

                case USHORT:
                    a.movzx(eax, ax);
                    break;
            }
        }

        if (long.class == resultClass) {
            // sign or zero extend the result to 64 bits
            switch (resultType.getNativeType()) {
                case SCHAR:
                case SSHORT:
                case SINT:
                case SLONG:
                    // sign extend eax to edx:eax
                    a.mov(edx, eax);
                    a.sar(edx, imm(31));
                    break;

                case UCHAR:
                case USHORT:
                case UINT:
                case ULONG:
                case ADDRESS:
                    a.mov(edx, imm(0));
                    break;
            }

        }
        // Restore esp to the original position and return
        a.add(esp, imm(stackadj));
        a.ret();

        stubs.add(new Stub(name, sig(resultClass, parameterClasses), a));
    }

    static int parameterSize(ParameterType parameterType) {
        switch (parameterType.getNativeType()) {
            case SCHAR:
            case UCHAR:
            case SSHORT:
            case USHORT:
            case SINT:
            case UINT:
            case SLONG:
            case ULONG:
            case ADDRESS:
            case FLOAT:
                return 4;

            case SLONGLONG:
            case ULONGLONG:
            case DOUBLE:
                return 8;

            default:
                throw new IllegalArgumentException("invalid parameter type" + parameterType);
        }
    }

    static int parameterSize(Class t) {
        if (byte.class == t || short.class == t || char.class == t | int.class == t || float.class == t) {
            return 4;

        } else if (long.class == t || double.class == t) {
            return 8;
        }
        throw new IllegalArgumentException("invalid parameter type" + t);
    }


    static int resultSize(ResultType resultType) {
        switch (resultType.getNativeType()) {
            case SCHAR:
            case UCHAR:
            case SSHORT:
            case USHORT:
            case SINT:
            case UINT:
            case SLONG:
            case ULONG:
            case ADDRESS:
                return 4;

            case SLONGLONG:
            case ULONGLONG:
                return 8;

            case FLOAT:
            case DOUBLE:
                return 16;

            case VOID:
                return 0;

            default:
                throw new IllegalArgumentException("invalid return type " + resultType);
        }
    }

    static Mem ptr(Register base, long disp, NativeType nativeType) {
        switch (nativeType) {
            case SCHAR:
            case UCHAR:
                return byte_ptr(base, disp);

            case SSHORT:
            case USHORT:
                return word_ptr(base, disp);

            default:
                return dword_ptr(base, disp);
        }
    }

}
