/* Copyright (c) 2007 Timothy Wall, All Rights Reserved
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.  
 */
package com.sun.jna;

import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import junit.framework.TestCase;

//@SuppressWarnings("unused")
public class NativeTest extends TestCase {
    
    public void testLongStringGeneration() {
        StringBuffer buf = new StringBuffer();
        final int MAX = Platform.isWindowsCE() ? 200000 : 2000000;
        for (int i=0;i < MAX;i++) {
            buf.append('a');
        }
        String s1 = buf.toString();
        Memory m = new Memory((MAX + 1)*Native.WCHAR_SIZE);
        m.setString(0, s1, true);
        assertEquals("Missing terminator after write", 0, m.getChar(MAX*Native.WCHAR_SIZE));
        String s2 = m.getString(0, true);
        assertEquals("Wrong string read length", s1.length(), s2.length());
        assertEquals("Improper wide string read", s1, s2);
    }

    public void testDefaultStringEncoding() throws Exception {
        String encoding = System.getProperty("file.encoding");
        // Keep stuff within the extended ASCII range so we work with more
        // limited native encodings
        String unicode = "Un \u00e9l\u00e9ment gr\u00e2ce \u00e0 l'index";
        
        if (!unicode.equals(new String(unicode.getBytes()))) {
            // If the extended characters aren't encodable in the default 
            // encoding, punt and use straight ASCII
            unicode = "";
            for (char ch=1;ch < 128;ch++) {
                unicode += ch;
            }
        }
        String unicodez = unicode + "\0more stuff";
        
        byte[] defaultEncoded = Native.getBytes(unicode);
        byte[] expected = unicode.getBytes();
        for (int i=0;i < Math.min(defaultEncoded.length, expected.length);i++) {
            assertEquals("Improperly encoded (" + encoding + ") from Java at " + i, 
                         expected[i], defaultEncoded[i]);
        }
        assertEquals("Wrong number of encoded characters (" + encoding + ")", 
                     expected.length, defaultEncoded.length);
        String result = Native.toString(defaultEncoded);
        assertEquals("Improperly decoded from native bytes (" + encoding + ")", 
                     unicode, result);
        
        assertEquals("Should truncate bytes at NUL terminator",
                     unicode, Native.toString(unicodez.getBytes()));
    }
    
    public void testCustomStringEncoding() throws Exception {
        Properties oldprops = (Properties)System.getProperties().clone();
        try {
            String encoding = "UTF8";
            System.setProperty("jna.encoding", encoding);
            String unicode = "\u0444\u043b\u0441\u0432\u0443";
            String unicodez = unicode + "\0more stuff";
            byte[] utf8 = Native.getBytes(unicode);
            byte[] expected = unicode.getBytes(encoding);
            for (int i=0;i < Math.min(utf8.length, expected.length);i++) {
                assertEquals("Improperly encoded at " + i, 
                             expected[i], utf8[i]);
            }
            assertEquals("Wrong number of encoded characters", expected.length, utf8.length);
            String result = Native.toString(utf8);
            assertEquals("Improperly decoded", unicode, result);
            
            assertEquals("Should truncate bytes at NUL terminator",
                         unicode, Native.toString(unicodez.getBytes(encoding)));
        }
        finally {
            System.setProperties(oldprops);
        }
    }
    
    public static interface TestLib extends Library {
        interface VoidCallback extends Callback {
            void callback();
        }
        void callVoidCallback(VoidCallback callback);
    }
    public void testSynchronizedAccess() throws Exception {
        final boolean[] lockHeld = { false };
        final NativeLibrary nlib = NativeLibrary.getInstance("testlib");
        final TestLib lib = (TestLib)Native.loadLibrary("testlib", TestLib.class);
        final TestLib synchlib = (TestLib)Native.synchronizedLibrary(lib); 
        final TestLib.VoidCallback cb = new TestLib.VoidCallback() {
            public void callback() {
                lockHeld[0] = Thread.holdsLock(nlib);
            }
        };
        Thread t0 = new Thread() {
            public void run() {
                lib.callVoidCallback(cb);
            }
        };
        t0.start();
        t0.join();
        assertFalse("NativeLibrary lock should not be held during native call to normal library",
                   lockHeld[0]);

        Thread t1 = new Thread() {
            public void run() {
                synchlib.callVoidCallback(cb);
            }
        };
        t1.start();
        t1.join();
        assertTrue("NativeLibrary lock should be held during native call to synchronized library",
                   lockHeld[0]);
    }

    interface TestInterface extends Library {
        static class InnerTestClass extends Structure {
            interface TestCallback extends Callback { }
            static class InnerSubclass extends InnerTestClass implements Structure.ByReference { }
            protected List getFieldOrder() { 
                return Collections.EMPTY_LIST;
            }
        }
    }
    
    public void testFindInterfaceClass() throws Exception {
        Class interfaceClass = TestInterface.class;
        Class cls = TestInterface.InnerTestClass.class;
        Class subClass = TestInterface.InnerTestClass.InnerSubclass.class;
        Class callbackClass = TestInterface.InnerTestClass.TestCallback.class;
        assertEquals("Enclosing interface not found for class",
                     interfaceClass, Native.findEnclosingLibraryClass(cls));
        assertEquals("Enclosing interface not found for derived class",
                     interfaceClass, Native.findEnclosingLibraryClass(subClass));
        assertEquals("Enclosing interface not found for callback",
                     interfaceClass, Native.findEnclosingLibraryClass(callbackClass));
    }

    public interface TestInterfaceWithInstance extends Library {
        int TEST_ALIGNMENT = Structure.ALIGN_NONE;
        TypeMapper TEST_MAPPER = new DefaultTypeMapper();
        Map TEST_OPTS = new HashMap() { {
            put(OPTION_TYPE_MAPPER, TEST_MAPPER);
            put(OPTION_STRUCTURE_ALIGNMENT, new Integer(TEST_ALIGNMENT));
        }};
        TestInterfaceWithInstance ARBITRARY = (TestInterfaceWithInstance) 
            Native.loadLibrary("testlib", TestInterfaceWithInstance.class, TEST_OPTS);
    }
    public void testOptionsInferenceFromInstanceField() {
        assertEquals("Wrong options found for interface which provides an instance", 
                     TestInterfaceWithInstance.TEST_OPTS,
                     Native.getLibraryOptions(TestInterfaceWithInstance.class));
        assertEquals("Wrong type mapper found", 
                     TestInterfaceWithInstance.TEST_MAPPER,
                     Native.getTypeMapper(TestInterfaceWithInstance.class));
        assertEquals("Wrong alignment found", 
                     TestInterfaceWithInstance.TEST_ALIGNMENT,
                     Native.getStructureAlignment(TestInterfaceWithInstance.class));
    }
    
    public interface TestInterfaceWithOptions extends Library {
        int TEST_ALIGNMENT = Structure.ALIGN_NONE;
        TypeMapper TEST_MAPPER = new DefaultTypeMapper();
        Map OPTIONS = new HashMap() { {
            put(OPTION_TYPE_MAPPER, TEST_MAPPER);
            put(OPTION_STRUCTURE_ALIGNMENT, new Integer(TEST_ALIGNMENT));
        }};
    }
    public void testOptionsInferenceFromOptionsField() {
        assertEquals("Wrong options found for interface which provides OPTIONS", 
                     TestInterfaceWithOptions.OPTIONS,
                     Native.getLibraryOptions(TestInterfaceWithOptions.class));
        assertEquals("Wrong type mapper found", 
                     TestInterfaceWithOptions.TEST_MAPPER,
                     Native.getTypeMapper(TestInterfaceWithOptions.class));
        assertEquals("Wrong alignment found", 
                     TestInterfaceWithOptions.TEST_ALIGNMENT,
                     Native.getStructureAlignment(TestInterfaceWithOptions.class));
    }

    public interface TestInterfaceWithTypeMapper extends Library {
        TypeMapper TEST_MAPPER = new DefaultTypeMapper();
        TypeMapper TYPE_MAPPER = TEST_MAPPER;
    }
    public void testOptionsInferenceFromTypeMapperField() {
        assertEquals("Wrong type mapper found for interface which provides TYPE_MAPPER", 
                     TestInterfaceWithTypeMapper.TEST_MAPPER,
                     Native.getTypeMapper(TestInterfaceWithTypeMapper.class));
    }

    public interface TestInterfaceWithAlignment extends Library {
        int STRUCTURE_ALIGNMENT = Structure.ALIGN_NONE;
    }
    public void testOptionsInferenceFromAlignmentField() {
        assertEquals("Wrong alignment found for interface which provides STRUCTURE_ALIGNMENT", 
                     Structure.ALIGN_NONE,
                     Native.getStructureAlignment(TestInterfaceWithAlignment.class));
    }

    public void testCharArrayToString() {
        char[] buf = { 'a', 'b', 'c', '\0', 'd', 'e' };
        assertEquals("Wrong String generated", "abc", Native.toString(buf));
    }

    public void testByteArrayToString() {
        byte[] buf = { 'a', 'b', 'c', '\0', 'd', 'e' };
        assertEquals("Wrong String generated", "abc", Native.toString(buf));
    }

    public void testToByteArray() {
        final String VALUE = getName();
        byte[] buf = Native.toByteArray(VALUE);
        assertEquals("Wrong byte array length", VALUE.length()+1, buf.length);
        assertEquals("Missing NUL terminator", (byte)0, buf[buf.length-1]);
        assertEquals("Wrong byte array contents", VALUE, new String(buf, 0, buf.length-1));
    }

    public void testToCharArray() {
        final String VALUE = getName();
        char[] buf = Native.toCharArray(VALUE);
        assertEquals("Wrong char array length", VALUE.length()+1, buf.length);
        assertEquals("Missing NUL terminator", (char)0, buf[buf.length-1]);
        assertEquals("Wrong char array contents: " + new String(buf), VALUE, new String(buf, 0, buf.length-1));
    }

    public void testOSPrefix() {
        assertEquals("Wrong resource path", "win32-x86",
                     Native.getNativeLibraryResourcePrefix(Platform.WINDOWS,
                                                           "x86", "Windows"));
        assertEquals("Wrong resource path Windows/i386", "win32-x86",
                     Native.getNativeLibraryResourcePrefix(Platform.WINDOWS,
                                                           "i386", "Windows"));
        assertEquals("Wrong resource path Windows CE/arm", "w32ce-arm",
                     Native.getNativeLibraryResourcePrefix(Platform.WINDOWSCE,
                                                           "arm", "Windows CE"));
        assertEquals("Wrong resource path Mac/x86", "darwin",
                     Native.getNativeLibraryResourcePrefix(Platform.MAC,
                                                           "x86", "Darwin"));
        assertEquals("Wrong resource path Mac/x86_64", "darwin",
                     Native.getNativeLibraryResourcePrefix(Platform.MAC,
                                                           "x86_64", "Mac"));
        assertEquals("Wrong resource path Solaris/sparc", "sunos-sparc",
                     Native.getNativeLibraryResourcePrefix(Platform.SOLARIS,
                                                           "sparc", "Solaris"));
        assertEquals("Wrong resource path SunOS/sparcv9", "sunos-sparcv9",
                     Native.getNativeLibraryResourcePrefix(Platform.SOLARIS,
                                                           "sparcv9", "SunOS"));
        assertEquals("Wrong resource path Linux/i386", "linux-i386",
                     Native.getNativeLibraryResourcePrefix(Platform.LINUX,
                                                           "i386", "Linux/Gnu"));
        assertEquals("Wrong resource path Linux/x86", "linux-i386",
                     Native.getNativeLibraryResourcePrefix(Platform.LINUX,
                                                           "x86", "Linux"));
        assertEquals("Wrong resource path Linux/ppc", "linux-ppc",
                     Native.getNativeLibraryResourcePrefix(Platform.LINUX,
                                                           "powerpc", "Linux"));
        assertEquals("Wrong resource path OpenBSD/x86", "openbsd-i386",
                     Native.getNativeLibraryResourcePrefix(Platform.OPENBSD,
                                                           "x86", "OpenBSD"));
        assertEquals("Wrong resource path FreeBSD/x86", "freebsd-i386",
                     Native.getNativeLibraryResourcePrefix(Platform.FREEBSD,
                                                           "x86", "FreeBSD"));
        assertEquals("Wrong resource path Linux/armv7l (android)", "android-arm",
                     Native.getNativeLibraryResourcePrefix(Platform.ANDROID,
                                                           "armv7l", "Linux"));
        
        assertEquals("Wrong resource path other/other", "name-ppc",
                     Native.getNativeLibraryResourcePrefix(Platform.UNSPECIFIED,
                                                           "PowerPC", "Name Of System"));
        
    }

    public static class DirectMapping {
        public static class DirectStructure extends Structure {
            public int field;
            protected List getFieldOrder() {
                return Arrays.asList(new String[] { "field" });
            }
        }
        public static interface DirectCallback extends Callback {
            void invoke();
        }
        public DirectMapping(Map options) {
            Native.register(getClass(), NativeLibrary.getInstance("testlib", options));
        }
    }

    public void testGetTypeMapperForDirectMapping() {
        final TypeMapper mapper = new DefaultTypeMapper();
        Map options = new HashMap();
        options.put(Library.OPTION_TYPE_MAPPER, mapper);
        DirectMapping lib = new DirectMapping(options);
        assertEquals("Wrong type mapper for direct mapping",
                     mapper, Native.getTypeMapper(DirectMapping.class));
        assertEquals("Wrong type mapper for direct mapping nested structure",
                     mapper, Native.getTypeMapper(DirectMapping.DirectStructure.class));
        assertEquals("Wrong type mapper for direct mapping nested callback",
                     mapper, Native.getTypeMapper(DirectMapping.DirectCallback.class));
    }

    private static class TestCallback implements Callback {
        public static final TypeMapper TYPE_MAPPER = new DefaultTypeMapper();
        public void callback() { }
    }
    public void testGetTypeMapperFromCallbackInterface() throws Exception {
        assertEquals("Wrong type mapper for callback class",
                     TestCallback.TYPE_MAPPER,
                     Native.getTypeMapper(TestCallback.class));
    }

    public void testStringReplace() {
        assertEquals("Bad replace", "abcdefg", Native.replace("z", "a", "zbcdefg"));
        assertEquals("Bad replace", "abcdefg", Native.replace("z", "g", "abcdefz"));
        assertEquals("Bad replace", "abcdefg", Native.replace("z", "d", "abczefg"));
        assertEquals("Bad replace", "abcaefa", Native.replace("z", "a", "zbczefz"));
    }

    public void testRemoveTemporaries() throws Exception {
        File dir = Native.getTempDir();
        File tmp = new File(dir, Native.JNA_TMPLIB_PREFIX);
        tmp.delete();
        try {
            assertTrue("Couldn't create temporary file " + tmp, tmp.createNewFile());
            assertTrue("File isn't recognized as unpacked", Native.isUnpacked(tmp));
            Native.markTemporaryFile(tmp);
            Native.removeTemporaryFiles();
            assertFalse("Temporary file still exists", tmp.exists());
        }
        finally {
            tmp.delete();
        }
    }

    public static void main(String[] args) {
        if (args.length == 0) {
            junit.textui.TestRunner.run(NativeTest.class);
        }
        else {
            if (args.length == 1 && "all".equals(args[0])) {
                args = new String[] {
                    "com.sun.jna.NativeTest",
                    "com.sun.jna.NativeLibraryTest",
                    "com.sun.jna.PointerTest",
                    "com.sun.jna.MemoryTest",
                    "com.sun.jna.LibraryLoadTest", 
                    "com.sun.jna.ArgumentsMarshalTest",
                    "com.sun.jna.ReturnTypesTest",
                    "com.sun.jna.TypeMapperTest", 
                    "com.sun.jna.ByReferenceArgumentsTest",
                    "com.sun.jna.LastErrorTest", 
                    "com.sun.jna.StructureTest",// 1 wce failure (RO fields)
                    "com.sun.jna.StructureByValueTest",
                    "com.sun.jna.UnionTest",
                    "com.sun.jna.IntegerTypeTest", 
                    "com.sun.jna.VMCrashProtectionTest",
                    "com.sun.jna.CallbacksTest", 
                    "com.sun.jna.JNAUnloadTest",
                    "com.sun.jna.DirectTest",
                    "com.sun.jna.DirectArgumentsMarshalTest",
                    "com.sun.jna.DirectByReferenceArgumentsTest",
                    "com.sun.jna.DirectTypeMapperTest",
                    "com.sun.jna.DirectReturnTypesTest",
                    "com.sun.jna.DirectStructureByValueTest",
                    "com.sun.jna.DirectCallbacksTest",
                };
            }
            System.out.println("Test suites: " + args.length);
            for (int i=0;i < args.length;i++) {
                System.out.println("Running tests on class " + args[i]);
                try {
                    junit.textui.TestRunner.run(Class.forName(args[i]));
                }
                catch(Throwable e) {
                    e.printStackTrace();
                }
            }
            try { Thread.sleep(300000); } catch(Exception e) { }
        }
    }
}
