/*
 * Created on Jul 14, 2004
 */
package freenet.support.CPUInformation;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

/**
 * @author Iakin
 * A class for retrieveing details about the CPU using the CPUID assembly instruction.
 * A good resource for information about the CPUID instruction can be found here:
 * http://www.paradicesoftware.com/specs/cpuid/index.htm
 * 
 * free (adj.): unencumbered; not under the control of others
 * Written by Iakin in 2004 and released into the public domain 
 * with no warranty of any kind, either expressed or implied.  
 * It probably won't make your computer catch on fire, or eat 
 * your children, but it might. Use at your own risk.
*/

public class CPUID {

    /** did we load the native lib correctly? */
    private static boolean _nativeOk = false;
    
    /** 
     * do we want to dump some basic success/failure info to stderr during 
     * initialization?  this would otherwise use the Log component, but this makes
     * it easier for other systems to reuse this class
     */
    private static final boolean _doLog = true;
    private static final boolean isX86 = System.getProperty("os.arch").toLowerCase().matches("(i?[x0-9]86(_64)?|amd64)");
    
	static
	{
		loadNative();
	}
	
	//A class that can (amongst other things I assume) represent the state of the
	//different CPU registers after a call to the CPUID assembly method
	protected static class CPUIDResult {
		final int EAX;
		final int EBX;
		final int ECX;
		final int EDX;
		CPUIDResult(int EAX,int EBX,int ECX, int EDX)
		{
			this.EAX = EAX;
			this.EBX = EBX;
			this.ECX = ECX;
			this.EDX = EDX;
		}
	}
	
	/**Calls the indicated CPUID function and returns the result of the execution
	 * 
	 * @param iFunction The CPUID function to call, should be 0 or larger
	 * @return The contents of the CPU registers after the call to the CPUID function
	 */
	private static native CPUIDResult doCPUID(int iFunction);

	private static String getCPUVendorID()
	{
		CPUIDResult c = doCPUID(0);
		StringBuilder sb= new StringBuilder(13);
		sb.append((char)( c.EBX        & 0xFF));
		sb.append((char)((c.EBX >> 8)  & 0xFF));
		sb.append((char)((c.EBX >> 16) & 0xFF));
		sb.append((char)((c.EBX >> 24) & 0xFF));
		
		sb.append((char)( c.EDX        & 0xFF));
		sb.append((char)((c.EDX >> 8)  & 0xFF));
		sb.append((char)((c.EDX >> 16) & 0xFF));
		sb.append((char)((c.EDX >> 24) & 0xFF));
		
		sb.append((char)( c.ECX        & 0xFF));
		sb.append((char)((c.ECX >> 8)  & 0xFF));
		sb.append((char)((c.ECX >> 16) & 0xFF));
		sb.append((char)((c.ECX >> 24) & 0xFF));
	
		return sb.toString();
	}
	private static int getCPUFamily()
	{
		CPUIDResult c = doCPUID(1);
		return (c.EAX >> 8) & 0xf;
	}
	private static int getCPUModel()
	{
		CPUIDResult c = doCPUID(1);
		return (c.EAX >> 4) & 0xf;
	}

	private static int getCPUExtendedFamily()
	{
		CPUIDResult c = doCPUID(1);
		return (c.EAX >> 20) & 0xff;
	}
	private static int getCPUStepping()
	{
		CPUIDResult c = doCPUID(1);
		return c.EAX & 0xf;
	}
	private static int getCPUFlags()
	{
		CPUIDResult c = doCPUID(1);
		return c.EDX;
	}
	
	//Returns a CPUInfo item for the current type of CPU
	//If I could I would declare this method in a interface named
	//CPUInfoProvider and implement that interface in this class.
	//This would make it easier for other people to understand that there
	//is nothing preventing them from coding up new providers, probably using
	//other detection methods than the x86-only CPUID instruction
	public static CPUInfo getInfo() throws UnknownCPUException
	{
		if(!_nativeOk)
			throw new UnknownCPUException("Failed to read CPU information from the system. Please verify the existence of the jcpuid dll/so.");
		if(!isX86)
			throw new UnknownCPUException("Failed to read CPU information from the system. The CPUID instruction exists on x86 CPU's only");
		if(getCPUVendorID().equals("AuthenticAMD"))
			return new AMDInfoImpl();
		if(getCPUVendorID().equals("GenuineIntel"))
			return new IntelInfoImpl();
		throw new UnknownCPUException("Unknown CPU type: '"+getCPUVendorID()+ '\'');
	}
	
	protected abstract static class CPUIDCPUInfo
	{
		public String getVendor()
		{
			return getCPUVendorID();
		}
		public boolean hasMMX(){
			return (getCPUFlags() & 0x800000) != 0; //Bit 23
		}
		public boolean hasSSE(){
			return (getCPUFlags() & 0x2000000) != 0; //Bit 25
		}
		public boolean hasSSE2(){
			return (getCPUFlags() & 0x4000000) != 0; //Bit 26
		}
	}
	protected static class AMDInfoImpl extends CPUIDCPUInfo implements AMDCPUInfo
	{
		public boolean IsK6Compatible()
		{
			return (getCPUFamily() >= 5) && (getCPUModel() >= 6);
		}
		public boolean IsK6_2_Compatible()
		{
			return (getCPUFamily() >= 5) && (getCPUModel() >= 8);
		}
		public boolean IsK6_3_Compatible()
		{
			return (getCPUFamily() >= 5) && (getCPUModel() >= 9);
		}
		public boolean IsAthlonCompatible()
		{
			return getCPUFamily() >= 6;
		}
		public boolean IsAthlon64Compatible()
		{
			return (getCPUFamily() == 15) && (getCPUExtendedFamily() == 0);
		}

		public String getCPUModelString() throws UnknownCPUException
		{
			if(getCPUFamily() == 4){
				switch(getCPUModel()){
					case 3:
						return "486 DX/2";
					case 7:
						return "486 DX/2-WB";
					case 8:
						return "486 DX/4";
					case 9:
						return "486 DX/4-WB";
					case 14:
						return "Am5x86-WT";
					case 15:
						return "Am5x86-WB";
				}
			}
			if(getCPUFamily() == 5){
				switch(getCPUModel()){
					case 0:
						return "K5/SSA5";
					case 1:
						return "K5";
					case 2:
						return "K5";
					case 3:
						return "K5";
					case 6:
						return "K6";
					case 7:
						return "K6";
					case 8:
						return "K6-2";
					case 9:
						return "K6-3";
					case 13:
						return "K6-2+ or K6-III+";
				}
			}
			if(getCPUFamily() == 6){
				switch(getCPUModel()){
					case 0:
                        return "Athlon (250 nm)";
					case 1:
                        return "Athlon (250 nm)";
					case 2:
                        return "Athlon (180 nm)";
					case 3:
						return "Duron";
					case 4:
						return "Athlon (Thunderbird)";
					case 6:
						return "Athlon (Palamino)";
					case 7:
						return "Duron (Morgan)";
					case 8:
						return "Athlon (Thoroughbred)";
					case 10:
						return "Athlon (Barton)";
				}
			}
			if(getCPUFamily() == 15){ // Must check Extended Family
				if(getCPUExtendedFamily() == 0){ // AMD K8
					// This just tells us socket type and chip die technology
					// see BrandID both the ID and NN portions
					// If you need to determine a specific chip brand
					switch(getCPUModel()){
						case 4:
							return "Athlon 64";
						case 5:
							return "Athlon 64 FX Opteron";
						case 7:
							return "Athlon 64 (0.13 um 939)";
						case 8:
							return "Athlon 64 (0.13 um 754)";
						case 11:
							return "Athlon 64 (0.13 um 939)";
						case 12:
							return "Athlon 64 (0.13 um 754)";
						case 15:
							return "Athlon 64 (0.13 um 939)";
					}
				}
			}
			throw new UnknownCPUException("Unknown AMD CPU; Family="+getCPUFamily()+", Model="+getCPUModel());
		}
	}

	protected static class IntelInfoImpl extends CPUIDCPUInfo implements IntelCPUInfo
	{
		public boolean IsPentiumCompatible()
		{
			return getCPUFamily() >= 5;
		}
		public boolean IsPentiumMMXCompatible()
		{
			return IsPentium2Compatible() || ((getCPUFamily() == 5) && ((getCPUModel() ==4) || (getCPUModel() == 8)));
		}
		public boolean IsPentium2Compatible()
		{
			return (getCPUFamily() > 6) || ((getCPUFamily() == 6) && (getCPUModel() >=3));
		}
		public boolean IsPentium3Compatible()
		{
			return (getCPUFamily() > 6) || ((getCPUFamily() == 6) && (getCPUModel() >=7));
		}
		public boolean IsPentium4Compatible()
		{
			return getCPUFamily() >= 15;
		}
		public String getCPUModelString() throws UnknownCPUException {
			if(getCPUFamily() == 4){
				switch(getCPUModel()){
					case 0:
						return "486 DX-25/33";
					case 1:
						return "486 DX-50";
					case 2:
						return "486 SX";
					case 3:
						return "486 DX/2";
					case 4:
						return "486 SL";
					case 5:
						return "486 SX/2";
					case 7:
						return "486 DX/2-WB";
					case 8:
						return "486 DX/4";
					case 9:
						return "486 DX/4-WB";
				}
			}
			if(getCPUFamily() == 5){
				switch(getCPUModel()){
					case 0:
						return "Pentium 60/66 A-step";
					case 1:
						return "Pentium 60/66";
					case 2:
						return "Pentium 75 - 200";
					case 3:
						return "OverDrive PODP5V83";
					case 4:
						return "Pentium MMX";
					case 7:
						return "Mobile Pentium 75 - 200";
					case 8:
						return "Mobile Pentium MMX";
				}
			}
			if(getCPUFamily() == 6){
				switch(getCPUModel()){
					case 0:
						return "Pentium Pro A-step";
					case 1:
						return "Pentium Pro";
					case 3:
						return "Pentium II (Klamath)";
					case 5:
						return "Pentium II (Deschutes), Celeron (Covington), Mobile Pentium II (Dixon)";
					case 6:
						return "Mobile Pentium II, Celeron (Mendocino)";
					case 7:
						return "Pentium III (Katmai)";
					case 8:
						return "Pentium III (Coppermine), Celeron w/SSE";
					case 9:
						return "Mobile Pentium III";
					case 10:
						return "Pentium III Xeon (Cascades)";
					case 11:
                        return "Pentium III (130 nm)";
				}
			}
			if(getCPUFamily() == 7){
				switch(getCPUModel()){
					//Itanium.. TODO
				}
			}
			if(getCPUFamily() == 15){
				if(getCPUExtendedFamily() == 0){
					switch(getCPUModel()){
						case 0:
                            return "Pentium IV (180 nm)";
						case 1:
                            return "Pentium IV (180 nm)";
						case 2:
                            return "Pentium IV (130 nm)";
						case 3:
                            return "Pentium IV (90 nm)";
					}
				}
				if(getCPUExtendedFamily() == 1){
					switch(getCPUModel()){
						//	Itanium 2.. TODO
					}	
				}
			}
			throw new UnknownCPUException("Unknown Intel CPU; Family="+getCPUFamily()+", Model="+getCPUModel());
		}
	}

	public static void main(String args[])
	{
		System.out.println(System.getProperty("os.arch").toLowerCase());
		if(!_nativeOk){
			System.out.println("**Failed to retrieve CPUInfo. Please verify the existence of jcpuid dll/so**");
		}
		System.out.println("**CPUInfo**");
		System.out.println("CPU Vendor: " + getCPUVendorID());
		System.out.println("CPU Family: " + getCPUFamily());
		System.out.println("CPU Extended Family: " + getCPUExtendedFamily());
		System.out.println("CPU Model: " + getCPUModel());
		System.out.println("CPU Stepping: " + getCPUStepping());
		System.out.println("CPU Flags: " + getCPUFlags());
		
		CPUInfo c = getInfo();
		System.out.println(" **More CPUInfo**");
		System.out.println(" CPU model string: " + c.getCPUModelString());
		System.out.println(" CPU has MMX: " + c.hasMMX());
		System.out.println(" CPU has SSE: " + c.hasSSE());
		System.out.println(" CPU has SSE2: " + c.hasSSE2());
		if(c instanceof IntelCPUInfo){
			System.out.println("  **Intel-info**");
			System.out.println("  Is pII-compatible: "+((IntelCPUInfo)c).IsPentium2Compatible());
			System.out.println("  Is pIII-compatible: "+((IntelCPUInfo)c).IsPentium3Compatible());
			System.out.println("  Is pIV-compatible: "+((IntelCPUInfo)c).IsPentium4Compatible());
		}
		if(c instanceof AMDCPUInfo){
			System.out.println("  **AMD-info**");
			System.out.println("  Is Athlon-compatible: "+((AMDCPUInfo)c).IsAthlonCompatible());
                        System.out.println("  Is Athlon64-compatible: "+((AMDCPUInfo)c).IsAthlon64Compatible());

		}
		
	}
	
	   /**
     * <p>Do whatever we can to load up the native library.
     * If it can find a custom built jcpuid.dll / libjcpuid.so, it'll use that.  Otherwise
     * it'll try to look in the classpath for the correct library (see loadFromResource).
     * If the user specifies -Djcpuid.enable=false it'll skip all of this.</p>
     *
     */
    private static final void loadNative() {
    	try{
        String wantedProp = System.getProperty("jcpuid.enable", "true");
        boolean wantNative = "true".equalsIgnoreCase(wantedProp);
        if (wantNative) {
            boolean loaded = loadFromResource();
            if (loaded) {
                _nativeOk = true;
                if (_doLog)
                    System.err.println("INFO: Native CPUID library '"+getResourceName()+"' loaded from resource");
            } else {
                loaded = loadGeneric();
                if (loaded) {
                    _nativeOk = true;
                    if (_doLog)
                        System.err.println("INFO: Native CPUID library '"+getLibraryMiddlePart()+"' loaded from somewhere in the path");
                } else {
                    _nativeOk = false;
                    if (_doLog)
                        System.err.println("WARN: Native CPUID library jcpuid not loaded - will not be able to read CPU information using CPUID");
                }
            }
        } else {
            if (_doLog)
                System.err.println("INFO: Native CPUID library jcpuid not loaded - will not be able to read CPU information using CPUID");
        }
    	}catch(Exception e){
            if (_doLog)
            	System.err.println("INFO: Native CPUID library jcpuid not loaded, reason: '"+e.getMessage()+"' - will not be able to read CPU information using CPUID");
        }
    }
    
    /** 
     * <p>Try loading it from an explictly built jcpuid.dll / libjcpuid.so</p>
     *
     * @return true if it was loaded successfully, else false
     *
     */
    private static final boolean loadGeneric() {
        try {
            System.loadLibrary(getLibraryMiddlePart());
            return true;
        } catch (UnsatisfiedLinkError ule) {
            return false;
        }
    }
    
    /**
     * <p>Check all of the jars in the classpath for the jcpuid dll/so.
     * This file should be stored in the resource in the same package as this class.
     * 
     * <p>This is a pretty ugly hack, using the general technique illustrated by the
     * onion FEC libraries.  It works by pulling the resource, writing out the 
     * byte stream to a temporary file, loading the native library from that file,
     * then deleting the file.</p>
     *
     * @return true if it was loaded successfully, else false
     *
     */
    private static final boolean loadFromResource() {
        String resourceName = getResourceName();
        if (resourceName == null) return false;
        URL resource = CPUID.class.getClassLoader().getResource(resourceName);
        
        if (resource == null) {
            if (_doLog)
                System.err.println("ERROR: Resource name [" + resourceName + "] was not found");
            return false;
        }

        File outFile = null;
        try {
            InputStream libStream = resource.openStream();
            try{
            	outFile = File.createTempFile("jcpuid", "lib.tmp");
                }catch (IOException e){
            	System.err.println("freenet.support.CPUInformation.CPUID: Can't create the temporary file in "+System.getProperty("java.io.tmpdir")+" trying something else now.");
            	outFile = new File("cpuid-lib.tmp");
            }
            FileOutputStream fos = new FileOutputStream(outFile);
            byte buf[] = new byte[4096*1024];
            while (true) {
                int read = libStream.read(buf);
                if (read < 0) break;
                fos.write(buf, 0, read);
            }
            fos.close();
            System.load(outFile.getAbsolutePath());//System.load requires an absolute path to the lib
            return true;
        } catch (UnsatisfiedLinkError ule) {
            if (_doLog) {
                System.err.println("ERROR: The resource " + resourceName 
                                   + " was not a valid library for this platform");
                ule.printStackTrace();
            }
            return false;
        } catch (IOException ioe) {
            if (_doLog) {
                System.err.println("ERROR: Problem writing out the temporary native library data");
                ioe.printStackTrace();
            }
            return false;
        } finally {
            if (outFile != null) {
                outFile.deleteOnExit();
            }
        }
    }
    
    private static final String getResourceName()
    {
    	String pname = CPUID.class.getPackage().getName().replace('.','/');
    	return pname+ '/' +getLibraryPrefix()+getLibraryMiddlePart()+ '.' +getLibrarySuffix();
    }
    
    private static final String getLibraryPrefix()
    {
    	boolean isWindows =System.getProperty("os.name").toLowerCase().indexOf("windows") != -1;
    	if(isWindows)
    		return "";
    	else
    		return "lib";
    }

    private static final String getLibraryMiddlePart(){
    	boolean isWindows =(System.getProperty("os.name").toLowerCase().indexOf("windows") != -1);
    	boolean isLinux =(System.getProperty("os.name").toLowerCase().indexOf("linux") != -1);
    	boolean isFreebsd =(System.getProperty("os.name").toLowerCase().indexOf("freebsd") != -1);
    	boolean isMacOS =(System.getProperty("os.name").toLowerCase().indexOf("mac os x") != -1);
    	if(isWindows)
		 	return "jcpuid-x86-windows"; // The convention on Windows
		if(isLinux)
			return "jcpuid-x86-linux"; // The convention on linux...
		if(isFreebsd)
			return "jcpuid-x86-freebsd"; // The convention on freebsd...
		if(isMacOS)
			return "jcpuid-x86-osx"; // The convention on Mac OS X...
		throw new RuntimeException("Dont know jcpuid library name for os type '"+System.getProperty("os.name")+ '\'');
    }
    
    private static final String getLibrarySuffix()
    {
    	boolean isWindows =System.getProperty("os.name").toLowerCase().indexOf("windows") != -1;
    	if(isWindows)
    		return "dll";
		else
    		return "so";
    }
}
