/*
    JPC: A x86 PC Hardware Emulator for a pure Java Virtual Machine
    Release Version 2.0

    A project from the Physics Dept, The University of Oxford

    Copyright (C) 2007 Isis Innovation Limited

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License version 2 as published by
    the Free Software Foundation.

    This program 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 for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
    Details (including contact information) can be found at: 

    www.physics.ox.ac.uk/jpc
*/

package org.jpc.sourcecompiler;

import org.jpc.emulator.memory.codeblock.optimised.*;
import org.jpc.emulator.memory.codeblock.*;
import javax.tools.*;
import java.util.*;
import java.nio.*;
import java.io.*;

//this needs BytesHolder.java, JavaSourceFromString.java, StringJavaFileManager.java, RealModeUBlockSourceArray.java, RealModeUBlockSource.java in the same directory

public class JavaSourceCompiler extends ClassLoader implements CodeBlockCompiler
{
    private static final String source1, source2, source3, source4, source5, source6, source7;
    private static final int[] IBMicrocodes;
    private static final String[][] IBMSource;

    private final JavaCompiler compiler;
    private final DiagnosticCollector<JavaFileObject> diagnostics;
    private final StringJavaFileManager fileManager;
    private final Hashtable classes;
    private final long startTime = System.currentTimeMillis();
    private final SourceArray sourceArray;

    private static int sourceCounter = 0;

    static
    {
        //set up strings
        source1 =  "package org.jpc.sourcecompiler;\n\n"
            +"import org.jpc.emulator.memory.codeblock.optimised.*;\n"
            +"import org.jpc.emulator.processor.*;\n"
            +"import org.jpc.emulator.processor.fpu64.*;\n"
            +"import org.jpc.emulator.memory.codeblock.*;\n"
            +"import org.jpc.emulator.memory.*;\n" 
            +"\n"
            +"public class ";
        source2 = " extends RealModeTemplateBlock\n"
            +"{\n"
            +"\n"
            +"   public int execute(Processor cpu)\n"
            +"   {\n"
            +"\n"
            +"      boolean eipUpdated = false;\n"
            +"      int executeCount = 0;\n"
            +"      Segment seg0 = null;\n"
            +"      int addr0 =0, addr1 =0;\n"
            +"      int reg0 =0, reg1=0, reg2=0, reg3=0, reg4=0, reg5=0;\n"
            +"      long reg0l =0, reg1l=0;\n"
            +"      try {\n";
        source3 = "         return executeCount + ";
        source4 = ";\n"
            +"      } catch (ProcessorException e)\n"
            +"      { System.out.println(\"threw an exception\");\n"
            +"         if (eipUpdated) cpu.eip -= ";
        source5 = "; /*undo the eipUpdate*/ \n"
            +"         cpu.handleRealModeException(e.getVector());\n"
            +"         return 0;\n"
            +"      }\n"
            +"   }\n"
            +"   public int getX86Length()\n"
            +"   {\n"
            +"      return ";
        source6 = ";\n"
            +"   }\n"
            +"   public int getX86Count()\n"
            +"   {\n"
            +"      return ";
        source7 = ";\n"
            +"    }\n"
            +"    public boolean handleMemoryRegionChange(int startAddress, int endAddress)"
            +"    {\n"
            +"       return false;"
            +"    }\n"
            +"}\n";

        IBMicrocodes = RealModeUBlockSourceArray.IBMicrocodes;
        IBMSource = RealModeUBlockSourceArray.IBMSource;
    }

    static PrintStream statsOutput;
    static
    {
        try
        {
            statsOutput = new PrintStream(new FileOutputStream("CompileStats"));
        }
        catch (Exception e)
        {
            System.out.println("NO STATS: "+e);
        }
    }

    private String className, classTitle;

    public JavaSourceCompiler()
    {
        super(ClassLoader.getSystemClassLoader());
        classes = new Hashtable();
        compiler = ToolProvider.getSystemJavaCompiler();
        diagnostics = new DiagnosticCollector<JavaFileObject>();
        fileManager = new StringJavaFileManager(compiler.getStandardFileManager(diagnostics, null, null), this);
        sourceArray = new SourceArray(1024);
    }

    public String getLastClassName()
    {
        return className;
    }

    public String getLastClassTitle()
    {
        return classTitle;
    }

    public String getLastSourceCode()
    {
        return new String(new StringBuffer(sourceArray));
    }

    public RealModeCodeBlock getRealModeCodeBlock(InstructionSource source)
    {
        //we have an instruction source, try to compile it       
        //long starttime = System.currentTimeMillis();

        classTitle = "ZZ_" + startTime+"_"+(sourceCounter++);
        className = "org.jpc.sourcecompiler."+classTitle;
        sourceArray.clear();
        fillOutSourceCode(sourceArray, source, classTitle);
        //System.out.println(sourceArray.toString());

        //load the class and instantiate it into an object
        fileManager.setContents(className, sourceArray);
        List<JavaFileObject> codeInStrings = new ArrayList();
        codeInStrings.add(new JavaSourceFromString(className, sourceArray));
        Iterable<? extends JavaFileObject> compilationUnits = codeInStrings;
        
        // reuse the same file manager to allow caching of jar files
        //compile class from string
        compiler.getTask(null, fileManager, diagnostics, null, null, compilationUnits).call();
        //doesn't suppress them
        //compiler.getTask(null, fileManager, null, null, null, compilationUnits).call();

        BytesHolder result = fileManager.getBytesHolder();
	byte[] rawBytes = result.getBytes();

	Class codeBlockClass = defineClass(className, rawBytes, 0, rawBytes.length);
        classes.put(className, codeBlockClass);

        RealModeCodeBlock compiledBlock = null;
        try
        {
            compiledBlock = (RealModeCodeBlock) codeBlockClass.newInstance();
        }
        catch (InstantiationException e)
        {
            throw new IllegalStateException("Could not instantiate class", e);
        }
        catch (IllegalAccessException e)
        {
            throw new IllegalStateException("Could not instantiate class", e);
        }
        
        //RealModeCodeBlockBytes thisBlock = new RealModeCodeBlockBytes();
        //thisBlock.setBlock(compiledBlock);
        //thisBlock.setBytes(result.getBytes());
        //thisBlock.setClassName(className);
        //long endtime = System.currentTimeMillis();
        //System.out.println("block compiled in " + (endtime-starttime));
        return compiledBlock;
    }

    private static void fillOutSourceCode(SourceArray classCode, InstructionSource source, String className)
    {
        int x86Length = 0, x86Count = 0 ;
        //first put all the code together for the execute method
        //RealModeUBlockSourceArray.microcodeMethods contains source for each method

        classCode.append(source1);
        classCode.append(className);
        classCode.append(source2);

        while (source.getNext())
        {
            int operationLength = source.getLength();
            x86Length += source.getX86Length();
            x86Count++;
            for (int i =0; i < operationLength; i++)
            {
                int microcode = source.getMicrocode();
                if (RealModeUBlockSourceArray.microcodeMethods[microcode] == null)
                    throw new IllegalStateException("cannot do microcode " + microcode);

                //test for immediate byte microcodes
                char[] methodSource = RealModeUBlockSourceArray.microcodeMethods[microcode];
                if (contains(IBMicrocodes, microcode))
                {
                    classCode.append(IBMSource[microcode][0]);
                    classCode.append(Integer.toString(source.getMicrocode()));
                    classCode.append(IBMSource[microcode][1]);
                    i++;
                }
                else //not an immediate byte microcode
                {
                    //remove a reference to microcodes array if there is one
                    if ((microcode == MicrocodeSet.POP_O16_A16) || (microcode == MicrocodeSet.POP_O32_A16))
                    {
                        int nextMicrocode = source.getMicrocode();
                        i++;
                        classCode.append(IBMSource[microcode][0]);
                        classCode.append(Boolean.toString(nextMicrocode == MicrocodeSet.STORE0_SS));
                        classCode.append(IBMSource[microcode][1]);
                        if (RealModeUBlockSourceArray.microcodeMethods[nextMicrocode] == null)
                            throw new IllegalStateException("cannot do microcode " + nextMicrocode);

                        //test for immediate byte microcodes
                        methodSource = RealModeUBlockSourceArray.microcodeMethods[nextMicrocode];
                        if (contains(IBMicrocodes, nextMicrocode))
                        {
                            classCode.append(IBMSource[microcode][0]);
                            classCode.append(Integer.toString(source.getMicrocode()));
                            classCode.append(IBMSource[microcode][1]);
                            i++;
                        }
                        else //not an immediate byte microcode
                            classCode.append(methodSource);
                    }
                    else
                        classCode.append(methodSource);
                }
                classCode.append("\n");
            }
        }

        String length = Integer.toString(x86Length);
        String count = Integer.toString(x86Count);
        //wrap this code in a method and a class implementing RealModeCodeBlock
        
        classCode.append(source3);
        classCode.append(count);
        classCode.append(source4);
        classCode.append(length);
        classCode.append(source5);
        classCode.append(length);
        classCode.append(source6);
        classCode.append(count);
        classCode.append(source7);

        //statsOutput.print(count);
    }

    public ProtectedModeCodeBlock getProtectedModeCodeBlock(InstructionSource source)
    {
        throw new IllegalArgumentException();
    }

    private static boolean contains(int[] array, int num)
    {
        for (int i =0; i < array.length; i++)
        {
            if (array[i] == num)
                return true;
        }
        return false;
    }
   
    public Class findClass(String name) throws ClassNotFoundException
    {
	Class myClass = (Class)classes.get(name);
        if (myClass != null)
            return myClass;
        else
	    throw new ClassNotFoundException(name);
    }

    private static class SourceArray implements CharSequence
    {
        char[] chars;
        int validLength;

        public SourceArray(char[] chs)
        {
            chars = chs;
            validLength = chs.length;
        }

        public SourceArray(int size)
        {
            chars = new char[size];
            validLength = 0;
        }

        public void clear()
        {
            validLength = 0;
        }

        public void append(char[] tail)
        {
            //business end of things
            int newLength = tail.length + validLength;
            if (newLength > chars.length)
            {
                //make it a bit longer at the same time
                char[] temp = new char[Math.max(2, 2*newLength)];
                System.arraycopy(chars, 0, temp, 0, validLength);
                chars = temp;
            }

            System.arraycopy(tail, 0, chars, validLength, tail.length);
            validLength = newLength;
        }

        public void append(CharSequence tail)
        {
            //business end of things
            int newLength = tail.length() + validLength;
            if (newLength > chars.length)
            {
                //make it a bit longer at the same time
                char[] temp = new char[Math.max(2, 2*newLength)];
                System.arraycopy(chars, 0, temp, 0, validLength);
                chars = temp;
            }

            for (int i=0; i < tail.length(); i++)
                chars[validLength + i] = tail.charAt(i);
            validLength = newLength;
        }

        public char charAt(int index)
        {
            if ((index < 0) || (index >= validLength))
                throw new IndexOutOfBoundsException();
            
            return chars[index];    
        }

        public int length()
        {
            return validLength;
        }

        public CharSequence subSequence(int start, int end)
        {
            if ((start < 0) || (end < 0))
                throw new IndexOutOfBoundsException();
            if ((start > end) || (end > validLength))
                throw new IndexOutOfBoundsException();
            
            char[] temp = new char[end - start];
            System.arraycopy(chars, start, temp, 0, end - start);
            return new SourceArray(temp);
        }

        public String toString()
        {
            return new String(chars, 0, validLength);
        }
    }
}
