/Users/petercappello/NetBeansProjects/56-2014/56-2014-5-Gala/src/Instruction.java
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JTextArea;

/**
 * A GVM instruction.
 * @author Peter Cappello
 */
final class Instruction 
{
    private static final int DEFINE = -1;
    private static final int OPCODE_NOT_SET = Integer.MAX_VALUE;
    private static final String numberRegex = "^-?\\d*$";
    private static final String identifierRegex = "[a-zA-Z_][\\w_]*";
    private static final String opcodeRegex = "set|load|store|add|zero|goto|drawline|drawRect|drawOval|fillRect|fillOval";
    private static final Pattern identifierPattern  = Pattern.compile( identifierRegex );
    private static final Map<String, Integer> stringToOpcode = new HashMap<>();
    
    private static int programMemoryIndex = 0; // The next instruction's program location.
    private static Map<String, Integer> identifierToValue;
    private static Map<String, List<Instruction>> undefinedIdentifierToInstructionList;
     
    static boolean areAllIdentifiersDefined( JTextArea console )
    {
        if ( undefinedIdentifierToInstructionList.isEmpty() )
        {
            return true;
        }
        for ( String undefinedIdentifier : undefinedIdentifierToInstructionList.keySet() )
        {
            console.append( undefinedIdentifier );
            console.append( " is undefined in statements with the following numbers:" );
            for ( Instruction instruction : undefinedIdentifierToInstructionList.get( undefinedIdentifier ) )
            {
                console.append( String.valueOf( instruction.lineNum ) );
            }
            console.append( "\n" );
        }
        return false;
    }
    
    /**
     * Initializes static variable that cannot be initialized in declaration.
     * Should use Google guava ImmutableMap for stringToOpcode.
     */
    static void setClass()
    {
        // opcodes
        stringToOpcode.put( "define", DEFINE );
        stringToOpcode.put( "stop", GVM.STOP );
        stringToOpcode.put( "set", GVM.SET );
        stringToOpcode.put( "load", GVM.LOAD );
        stringToOpcode.put( "store", GVM.STORE );
        stringToOpcode.put( "add", GVM.ADD );
        stringToOpcode.put( "zero", GVM.ZERO );
        stringToOpcode.put( "goto", GVM.GOTO );
        stringToOpcode.put( "setcolor", GVM.SETCOLOR );
        stringToOpcode.put( "drawline", GVM.DRAWLINE );
        stringToOpcode.put( "drawrect", GVM.DRAWRECT );
        stringToOpcode.put( "fillrect", GVM.FILLRECT );
        stringToOpcode.put( "drawoval", GVM.DRAWOVAL );
        stringToOpcode.put( "filloval", GVM.FILLOVAL );
        Collections.unmodifiableMap( stringToOpcode );
        
        // keywords
        initStringToInteger();
    }
    
    /**
     * Initializes identifierToValue: the symbol table
     */
    static void initStringToInteger()
    {
        // keywords
        identifierToValue = new HashMap<>();
        identifierToValue.put( "ACC", GVM.ACC );
        identifierToValue.put( "X", GVM.X );
        identifierToValue.put( "Y", GVM.Y );
        identifierToValue.put( "WIDTH", GVM.WIDTH );
        identifierToValue.put( "HEIGHT", GVM.HEIGHT );
        identifierToValue.put( "RED", GVM.RED );
        identifierToValue.put( "GREEN", GVM.GREEN );
        identifierToValue.put( "BLUE", GVM.BLUE );
    }
    
    /**
     * Reinitializes static variables for a new assembly.
     */
    static void resetClass()
    { 
        programMemoryIndex = 0;
        undefinedIdentifierToInstructionList = new HashMap<>();
        initStringToInteger();
    }
    
    private final String line;
    private final int lineNum;
    private final int location = programMemoryIndex;
    private int opcode = OPCODE_NOT_SET;
    private int operand1;
    private int operand2;
    private String message;
    
    // scratchpad variables
    private int tokenIndex;
    String[] tokens;
    
    public Instruction( String line, int lineNum )
    {
        this.line = line;
        this.lineNum = lineNum;
        tokens = line.trim().split( "\\s+" );
        assert tokens.length > 0; // blank/comment lines are not passed in.
        processLabel( tokens[ tokenIndex ] );
        if ( ! isValid() )
        {
            return;
        }
        processOpcode( tokens[ tokenIndex ] );
        if ( ! isValid() || ! correctNumOperands() )
        {
            return;
        }
        switch ( opcode )
        {
            case DEFINE: case GVM.ADD: case GVM.GOTO: case GVM.LOAD: case GVM.SET: case GVM.STORE: case GVM.ZERO:
                processOperand1( tokens[ tokenIndex ]);
        }
        if ( opcode != DEFINE )
        {
            programMemoryIndex++;
        }
    }
    
    @Override
    public String toString()
    {
        StringBuilder instructionString = new StringBuilder();
        instructionString.append( "\t line: ").append( line ).append( "\n\t" );
        instructionString.append( "\t statementNum: ").append( lineNum ).append( '\t' );
        instructionString.append( "\t location: ").append( location ).append( '\t' );
        instructionString.append( "\t opcode: ").append( opcode ).append( '\t' );
        instructionString.append( "\t operand1: ").append( operand1 ).append( '\t' );
        instructionString.append( "\t operand2: ").append( operand2 ).append( '\t' );
        return instructionString.toString();
    }
    
    String getErrorMessage() { return message; }
    
    int getOpcode() { return opcode; }
    
    boolean isMachineInstruction() { return opcode != DEFINE; }
    
    boolean isValid() { return message == null; }
        
    int[] toArray()
    {
        switch( opcode )
        {
            case GVM.SET: case GVM.LOAD: case GVM.ADD: case GVM.GOTO: case GVM.STORE: case GVM.ZERO:
                return new int[]{ opcode, operand1 };
            default:
                return new int[]{ opcode };     
        }
    }
    
    private void processLabel( String string )
    {
        Integer prospectiveOpcode = stringToOpcode.get( string );
        if ( prospectiveOpcode != null )
        {
            opcode = prospectiveOpcode;
            return;
        }
        // process label
        Matcher matcher = identifierPattern.matcher( string );
        if ( matcher.matches() )
        {
            // Has identifier been defined previously?
            if ( identifierToValue.get( string ) == null )
            {
                // label value is current insrruction location.
                identifierToValue.put( string, location );
                resolvePriorReferences( string, location );
                if ( ++tokenIndex >= tokens.length )
                {
                    message = "labeled statement is missing an opcode.";
                }
                return;
            }
            message = string + " has been defined previously. ";
            return;
        }
        message= string + " is an invalid statement label.";
    }
    
    private void processOpcode( String string )
    {
        if ( opcode != OPCODE_NOT_SET )
        {
            tokenIndex++;
            return; // statement already found to be in error
        }
        Integer prospectiveOpcode = stringToOpcode.get( string );
        if ( prospectiveOpcode != null )
        {
            opcode = prospectiveOpcode;
            tokenIndex++;
            return;
        }
        message = string + " is an invalid opcode.";
    }
    
    private boolean correctNumOperands()
    {
        switch ( opcode )
        {
            case GVM.DRAWLINE: case GVM.DRAWOVAL: case GVM.DRAWRECT: case GVM.FILLOVAL: case GVM.FILLRECT: case GVM.SETCOLOR: case GVM.STOP:
                if ( tokenIndex == tokens.length )
                {
                    return true;
                }
                break;
            
            case GVM.ADD: case GVM.GOTO: case GVM.LOAD: case GVM.SET: case GVM.STORE: case GVM.ZERO:
                if ( tokenIndex + 1 == tokens.length )
                {
                    return true;
                }
                break;
                
            case DEFINE: 
                if ( tokenIndex + 2 == tokens.length )
                {
                    return true;
                }
                break;
        }
        message = "The number of operands for this opcode is incorrect.";
        return false;
    }
    
    private void processOperand1( String string )
    {
        Matcher matcher = identifierPattern.matcher( string );
        if ( matcher.matches() )
        {
            Integer value = identifierToValue.get( string );
            if ( value == null )
            {
                processUndefinedIdentifier( string );
                return;
            }
            processDefinedIdentifier( string, value );
            return;
        }
        if ( opcode == DEFINE )
        {
            message = "define opcode's 1st operand must be an identifier.";
        }
        operand1 = processNumber( string );
    }
    
    private void processUndefinedIdentifier( String string )
    {
        switch ( opcode )
        {
            case DEFINE:
                int value = processNumber( tokens[ ++tokenIndex ] );
                identifierToValue.put( string, value );
                return;

            case GVM.GOTO: case GVM.ZERO:
                addToUndefinedLabelList( string );
                return;

            case GVM.SET: case GVM.LOAD: case GVM.STORE: case GVM.ADD:
                // identifier is expected to be defined. 
                message = string + " is undefined.";
                return;

            default: 
                assert false; // error in assembler
        }
    }
    
    private void processDefinedIdentifier( String string, int value )
    {
        switch ( opcode )
        {
            case DEFINE:
                message = string + " already is defined.";
                return;

            case GVM.GOTO: case GVM.ZERO:
                    operand1 = value;
                    return;

            case GVM.SET: case GVM.LOAD: case GVM.STORE: case GVM.ADD:
                operand1 = value;
                return;

            default: 
                assert false; // error in assembler
        }
    }
    
    private int processNumber( String string ) 
    {
        try
        {
            return Integer.parseInt( string );
        }
        catch ( NumberFormatException exception )
        {
            message = string + " is not interpretable as a number.";
        }
        return 0; // error return
    }
    
    private void addToUndefinedLabelList( String string )
    {
        List<Instruction> patchInstructions = undefinedIdentifierToInstructionList.get( string );
        if ( patchInstructions == null )
        {
            patchInstructions = new ArrayList<>();
            undefinedIdentifierToInstructionList.put( string, patchInstructions );
        }
        patchInstructions.add( this );
    }
    
    /**
     * Patch Instruction objects that referred to this label.
     * @param string statement label
     * @param location current instruction location 
     */
    private void resolvePriorReferences( String string, int location )
    {
        List<Instruction> instructionList = undefinedIdentifierToInstructionList.remove( string );
        if ( instructionList == null )
        {
            return; // no instructions to patch
        }
        for ( Instruction instruction : instructionList )
        {
            instruction.setOperand1( location );
        }
    }
    
    private void setOperand1( int location ) { operand1 = location; }
    
    public static void main( String[] args )
    {
        // Run pattern tests
        System.out.println("identifierRegex: define " + Pattern.matches( identifierRegex, "define" ) );
        System.out.println("opcdoePattern: \" \"" + Pattern.matches( numberRegex, " " ) );
        System.out.println("whiteSpacesPattern:             SET -1 " + Pattern.matches( "\\s+", "              SET -1" ) );
        System.out.println("opcdoePattern: \"#\"" + Pattern.matches( numberRegex, "#" ) );
        System.out.println("opcdoePattern: \"a\"" + Pattern.matches( numberRegex, "a" ) );
        System.out.println("opcdoePattern: \"7\"" + Pattern.matches( numberRegex, "7" ) );
        System.out.println("opcdoePattern: \"-7\"" + Pattern.matches( numberRegex, "-7" ) );
        System.out.println("opcdoePattern: \"--7\"" + Pattern.matches( numberRegex, "--7" ) );
        
        System.out.println("opcdoePattern: \"set\"" + Pattern.matches( opcodeRegex, "set" ) );
        System.out.println("opcdoePattern: \"fillOval\"" + Pattern.matches( opcodeRegex, "fillOval" ) );
        System.out.println("opcdoePattern: \"fillwhat\"" + Pattern.matches( opcodeRegex, "fillwhat" ) );
    }
}