/*******************************************************************************

 * Copyright (c) 2006 IONA Technologies PLC

 * All rights reserved. This program and the accompanying materials

 * are made available under the terms of the Eclipse Public License v1.0

 * which accompanies this distribution, and is available at

 * http://www.eclipse.org/legal/epl-v10.html

 * 

 * Contributors:

 *     IONA Technologies PLC - initial API and implementation

 *******************************************************************************/
package org.eclipse.stp.sc.xmlvalidator.classbuilder;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.List;

import org.eclipse.stp.common.logging.LoggingProxy;
import org.eclipse.stp.sc.xmlvalidator.classbuilder.inst.JInstruction;

/**
 * @author jma
 * 
 */
class JByteCodeVisitor implements JVisitor {

	private static final LoggingProxy LOG = LoggingProxy.getlogger(JByteCodeVisitor.class);

	ByteArrayOutputStream methodOS;

	ByteArrayOutputStream classOS;
	
	ByteArrayOutputStream constantPoolOS;
	
	ByteArrayOutputStream fieldOS;
	
	//point to the constant pool defined in JClass
	JConstantPool pool = null;
	
	ByteArrayOutputStream codeOS;

	public JByteCodeVisitor() {
		methodOS = new ByteArrayOutputStream();
		classOS = new ByteArrayOutputStream();
		constantPoolOS = new ByteArrayOutputStream();
		fieldOS = new ByteArrayOutputStream();
		codeOS = new ByteArrayOutputStream();
	}

	public void setConstantPool(JConstantPool value) {
	    pool = value;
	}
	
	public void visitClass(JClass cls) throws Exception {
		LOG.debug("visit cls:" + cls.getName());
		DataOutputStream dout = new DataOutputStream(classOS);

        //u4 magic
		dout.writeInt(0xCAFEBABE);
        //u2 minor_version
		dout.writeShort(0); 
		//u2 major_version (Java 1.0.2)
		dout.writeShort(48);

		byte[] clsBytes = buildClass(cls);
		
		LOG.debug("constant pool bytes:" + ByteCodeUtils.toHexString(constantPoolOS.toByteArray()));
		cls.pool.accept(this);
		dout.write(constantPoolOS.toByteArray());
		
		//write class bytes
		dout.write(clsBytes);
		
		//fields related bytes
		// u2 fields_count
		dout.writeShort(cls.getFields().size());
		//write fields bytes
		//field_info fields[fields_count];
		dout.write(fieldOS.toByteArray());
		
		//methods related bytes
		// u2 methods_count
		dout.writeShort(cls.getMethods().size()); 
        //write methods bytes
		//method_info methods[methods_count];
		dout.write(methodOS.toByteArray());
		
		//u2 attributes_count;
		dout.writeShort(0);
		
		LOG.debug("method bytecode:"
				+ ByteCodeUtils.toHexString(methodOS.toByteArray()));
		LOG.debug("cls bytecode:"
				+ ByteCodeUtils.toHexString(classOS.toByteArray()));

	}

	/**
	 * write the method to java byte code format.
	 * 
	 * @param method,
	 *            the method to visit
	 * @param bos,
	 *            output stream to write
	 * @return
	 */
	public void visitMethod(JMethod method) throws Exception {

		LOG.debug("Visit Method:" + method.name);
		LOG.debug(method);
		//int initLength = methodOS.size();
		DataOutputStream dos = new DataOutputStream(methodOS);
		// u2 access_flags;
		dos.writeShort(method.getModifiers());

		// u2 name_index;
		dos.writeShort(pool.stringUTFConstant(method.getName()));
		// u2 descriptor_index;
		dos.writeShort(pool.stringUTFConstant(ByteCodeUtils.getMethodDescriptor(method)));

		boolean hasException = method.getExceptions().size() > 0;
		boolean hasCode = method.getCode().size() > 0;
		if (hasCode && hasException) {
			//has both exception attr and code attr
			dos.writeShort(2);
		} else if (hasCode | hasException) {
			dos.writeShort(1);
		} else {
			dos.writeShort(0);
		}
		 
		writeExceptionAttribute(dos, method);
		writeCodeAttribute(dos, method);

		/*
		byte[] bytes = methodOS.toByteArray();
		int length = bytes.length - initLength;
		byte[] methodBytes = new byte[length];
		System.arraycopy(bytes, initLength, methodBytes, 0, length);
		System.out.println("bytes(" + methodBytes.length + "):"
				+ ByteCodeUtils.toHexString(methodBytes));
		System.out.println("********Visit Method end********");*/

	}
	
	private void writeCodeAttribute(DataOutputStream dos, JMethod method) throws Exception {
		int codeCount = method.getCode().size();
		if (codeCount == 0) {
			//it should be interface method
			return;
		}
		byte[] codes = codeOS.toByteArray();
		//u2 attribute_name_index;
		dos.writeShort(pool.stringUTFConstant("Code"));
    	//u4 attribute_length;
		int attribute_Length = 0;
		attribute_Length += 2 + 2 + 4 + codes.length + 2 + 2;
		dos.writeInt(attribute_Length);
    	//u2 max_stack;
		dos.writeShort(0xff); //@TODO, the max stack should calculated from code steps
    	//u2 max_locals;
	    //int maxLocal = method.getParameters().size();
	    //***** Johnson need to revisit the maxLocal later !!
		dos.writeShort(0xff);
    	//u4 code_length;
		dos.writeInt(codes.length);
    	//u1 code[code_length];
		dos.write(codes);
    	//u2 exception_table_length;
		dos.writeShort(0); //let's handle the exception tomorrow
    	//{    	u2 start_pc;
    	//      	u2 end_pc;
    	//      	u2  handler_pc;
    	//      	u2  catch_type;
    	//}	exception_table[exception_table_length];
    	//u2 attributes_count;
		dos.writeShort(0);
    	//attribute_info attributes[attributes_count];
		
		//reset codeOS for next method
		codeOS.reset();
	}
	
	private void writeExceptionAttribute(DataOutputStream dos, JMethod method) throws Exception {
		int exceptionCount = method.getExceptions().size();
		if (exceptionCount == 0) {
			return;
		}
        //attribute_info attributes[attributes_count];
		// u2 attribute_name_index
		dos.writeShort(pool.stringUTFConstant("Exceptions"));
		// u4 attribute_length:
		dos.writeInt(2 + 2 * exceptionCount);
		// u2 number_of_exceptions
		dos.writeShort(exceptionCount);
		// u2 exception_index
		for (Class<?> expClass : method.getExceptions()) {
			dos.writeShort(pool.classConstant(expClass.getName()));
		}
	}

	public void visitField(JField field) throws Exception {
		LOG.debug("Visit field" + field.getName());
		DataOutputStream dos = new DataOutputStream(fieldOS);
        //u2 access_flags;
		dos.writeShort(field.getModifiers());
        //u2 name_index;
		dos.writeShort(pool.stringUTFConstant(field.getName()));
        //u2 descriptor_index;
		dos.writeShort(pool.stringUTFConstant(ByteCodeUtils.getFieldDescriptor(field)));
		//u2 attributes_count;
		dos.writeShort(0);
		return;
	}

	
	public byte[] buildClass(JClass cls) throws Exception {
		ByteArrayOutputStream clsDefOS = new ByteArrayOutputStream();
		DataOutputStream cldDefOut = new DataOutputStream(clsDefOS);
		// u2 access_flags
		cldDefOut.writeShort(cls.getModifiers());
		// u2 this_class
		cldDefOut.writeShort(pool.classConstant(cls.getName()));
		// u2 super_class
		cldDefOut.writeShort(pool.classConstant(cls.getSuperClass()));
        //u2 interfaces_count
		cldDefOut.writeShort(0);
		return clsDefOS.toByteArray();
	}

	

    public void visitConstantPool(JConstantPool pool) throws IOException {
    	LOG.debug("visit constant pool.");
		DataOutputStream dout = new DataOutputStream(constantPoolOS);
		dout.writeShort(pool.poolIndex);
		int i = 1;
			
		for (List<?> data : pool.poolMap.keySet()) {
			assert (pool.poolMap.get(data).equals(i++));
			int tag = (Integer) data.get(0);
			dout.writeByte(tag); // u1 tag
			switch (tag) {
			case JConstants.UTF8:
				LOG.debug("write utf:" + (String) data.get(1));
				dout.writeUTF((String) data.get(1));
				break; // u2 length + u1 bytes[length]
			case JConstants.CLASS:
				LOG.debug("write cls index:" + (Integer) data.get(1));
				dout.writeShort((Integer) data.get(1));
				break; // u2 name_index
			case JConstants.FIELDREF:
			case JConstants.Methodref:
		    	//u2 class_index;
				dout.writeShort((Integer) data.get(1));
		    	//u2 name_and_type_index;
				dout.writeShort((Integer) data.get(2));
				break;
			case JConstants.NameAndType:
				//u2 name_index;
				dout.writeShort((Integer) data.get(1));
				//u2 descriptor_index;
				dout.writeShort((Integer) data.get(2));
				break;
             case JConstants.String:
				//u2 string_index;
				dout.writeShort((Integer) data.get(1));
                               break;
			default:
				throw new AssertionError();
			}
		}
		return;
	}
    
    private boolean isOffsetInst(JInstruction inst) {
    	if (inst == JInstruction.ALOAD_) {
    		return true;
    	} else if (inst == JInstruction.ASTORE_) {
    		return true;
    	}  
    	return false;
    }
    
    public void visitCodeStep(JCodeStep code) throws Exception {
    	LOG.debug("visit code:" + code.opCode + " " + code.opData);
    	//visit one code step
    	DataOutputStream dos = new DataOutputStream(codeOS);
    	if (isOffsetInst(code.opCode)) {
    		//deal with offset instruction, like aload_N
            byte inst= (byte)code.opCode.value();
            inst += ((Integer)code.opData);
            dos.writeByte(inst);
    		return;
    	} 
    	dos.writeByte(code.opCode.value());
    	if (code.opData != null) {
    		if (code.opData instanceof Short) {
    			dos.writeShort((Short)code.opData);
    		} else if (code.opData instanceof Byte) {
    			dos.writeByte((Byte)code.opData);
                } else {
    			Exception ee = new Exception();
    			LOG.error("op data type does support, need to fix:" + 
    					code.opData.getClass().getName());
    			ee.printStackTrace();
    		}
    	}
    }

}
