/**
 *
 * Copyright (c) 2009-2012,
 *
 *  Galois, Inc. (Aaron Tomb <atomb@galois.com>, 
 *                Rogan Creswick <creswick@galois.com>, 
 *                Adam Foltzer <acfoltzer@galois.com>)
 *  Steve Suh    <suhsteve@gmail.com>
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * 3. The names of the contributors may not be used to endorse or promote
 * products derived from this software without specific prior written
 * permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 *
 */
package org.scandroid.synthmethod;

import java.io.UTFDataFormatException;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.ibm.wala.ssa.SSAArrayLengthInstruction;
import com.ibm.wala.ssa.SSAArrayLoadInstruction;
import com.ibm.wala.ssa.SSAArrayStoreInstruction;
import com.ibm.wala.ssa.SSABinaryOpInstruction;
import com.ibm.wala.ssa.SSACheckCastInstruction;
import com.ibm.wala.ssa.SSAComparisonInstruction;
import com.ibm.wala.ssa.SSAConditionalBranchInstruction;
import com.ibm.wala.ssa.SSAConversionInstruction;
import com.ibm.wala.ssa.SSAGetCaughtExceptionInstruction;
import com.ibm.wala.ssa.SSAGetInstruction;
import com.ibm.wala.ssa.SSAGotoInstruction;
import com.ibm.wala.ssa.SSAInstanceofInstruction;
import com.ibm.wala.ssa.SSAInstruction;
import com.ibm.wala.ssa.SSAInvokeInstruction;
import com.ibm.wala.ssa.SSALoadMetadataInstruction;
import com.ibm.wala.ssa.SSAMonitorInstruction;
import com.ibm.wala.ssa.SSANewInstruction;
import com.ibm.wala.ssa.SSAPhiInstruction;
import com.ibm.wala.ssa.SSAPiInstruction;
import com.ibm.wala.ssa.SSAPutInstruction;
import com.ibm.wala.ssa.SSAReturnInstruction;
import com.ibm.wala.ssa.SSASwitchInstruction;
import com.ibm.wala.ssa.SSAThrowInstruction;
import com.ibm.wala.ssa.SSAUnaryOpInstruction;
import com.ibm.wala.types.MethodReference;
import com.ibm.wala.types.TypeReference;
import com.ibm.wala.util.strings.Atom;

public class SSAtoXMLVisitor implements SSAInstruction.IVisitor {
	private static final Logger logger = LoggerFactory.getLogger(SSAtoXMLVisitor.class);

    /**
     * A counter to use for generating unique local definition names.
     */
    private int defCounter = 0;

    /**
     * Map the known defNum to local def names.
     */
    private Map<Integer, String> localDefs = Maps.newHashMap();

    /**
     * XML document to use for creating elements.
     */
    private final Document doc;

    /**
     * XML elements that represent the ssa instructions
     */
    private final List<Element> summary = Lists.newArrayList();

    public SSAtoXMLVisitor(Document doc, int argCount) {
        this.doc = doc;
        for (int i=0; i < argCount; i++) {
            localDefs.put(i+1, "arg"+i);
        }
    }

    @Override
    public void visitGoto(SSAGotoInstruction instruction) {
        throw new SSASerializationException("Unsupported.");
    }

    /**
     * Load from an array ref, at specified index, and store in def.
     * 
     *   <aaload ref="x" index="0" def="y" />
     */
    @Override
    public void visitArrayLoad(SSAArrayLoadInstruction instruction) {
        try {
            Element elt = doc.createElement(XMLSummaryWriter.E_AALOAD);
            
            String refStr = getLocalName(instruction.getArrayRef());
            elt.setAttribute(XMLSummaryWriter.A_REF, refStr);
            
            String defStr = getLocalName(instruction.getDef());
            elt.setAttribute(XMLSummaryWriter.A_VALUE, defStr);
            
            elt.setAttribute(XMLSummaryWriter.A_INDEX, ""+instruction.getIndex());
            summary.add(elt);
        } catch (Exception e) {
            throw new SSASerializationException(e);
        }
    }

    /**
     *    <aastore ref="x" value="y" index="0" />
     */
    @Override
    public void visitArrayStore(SSAArrayStoreInstruction instruction) {
        try {
            Element elt = doc.createElement(XMLSummaryWriter.E_AASTORE);
            
            String refStr = getLocalName(instruction.getArrayRef());
            elt.setAttribute(XMLSummaryWriter.A_REF, refStr);
            
            String valueStr = getLocalName(instruction.getValue());
            elt.setAttribute(XMLSummaryWriter.A_VALUE, valueStr);
            
            elt.setAttribute(XMLSummaryWriter.A_INDEX, ""+instruction.getIndex());
            summary.add(elt);
        } catch (Exception e) {
            throw new SSASerializationException(e);
        }
    }

    @Override
    public void visitBinaryOp(SSABinaryOpInstruction instruction) {
        throw new SSASerializationException("Unsupported.");
    }

    @Override
    public void visitUnaryOp(SSAUnaryOpInstruction instruction) {
        throw new SSASerializationException("Unsupported.");
    }

    @Override
    public void visitConversion(SSAConversionInstruction instruction) {
        throw new SSASerializationException("Unsupported.");
    }

    @Override
    public void visitComparison(SSAComparisonInstruction instruction) {
        throw new SSASerializationException("Unsupported.");
    }

    @Override
    public void visitConditionalBranch(
            SSAConditionalBranchInstruction instruction) {
        throw new SSASerializationException("Unsupported.");
    }

    @Override
    public void visitSwitch(SSASwitchInstruction instruction) {
        throw new SSASerializationException("Unsupported.");
    }

    @Override
    public void visitReturn(SSAReturnInstruction instruction) {
        try {
        	Element elt = doc.createElement(XMLSummaryWriter.E_RETURN);
            if (!instruction.returnsVoid()) {
            	String localName = getLocalName(instruction.getResult());
            	elt.setAttribute(XMLSummaryWriter.A_VALUE, localName);
            }
            summary.add(elt);
        } catch (Exception e) {
            throw new SSASerializationException(e);
        }
    }

    /**
     * eg:
     * 
     * <getfield class="Ljava/lang/Thread" field="runnable"
     * fieldType="Ljava/lang/Runnable" def="x" ref="arg0" />
     * 
     * I think the get statics look like this:
     * 1007g 9.1g  12m S 237.9  0.9   4:27.32 java
     * <getstatic class="Ljava/lang/Thread" field="runnable"
     * fieldType="Ljava/lang/Runnable" def="x" />
     */
    @Override
    public void visitGet(SSAGetInstruction instruction) {
        try {
            String eltName;

            if (instruction.isStatic()) {
                eltName = XMLSummaryWriter.E_GETSTATIC;
            } else {
                eltName = XMLSummaryWriter.E_GETFIELD;
            }
            Element elt = doc.createElement(eltName);

            if (!instruction.isStatic()) {
                String refName = getLocalName(instruction.getRef());
                elt.setAttribute(XMLSummaryWriter.A_REF, refName);
            }

            String def = newLocalDef(instruction.getDef());
            TypeReference fieldType = instruction.getDeclaredFieldType();
            TypeReference classType = instruction.getDeclaredField()
                    .getDeclaringClass();

            String fieldName = instruction.getDeclaredField().getName()
                    .toUnicodeString();

            elt.setAttribute(XMLSummaryWriter.A_CLASS, classType.getName().toUnicodeString());
            elt.setAttribute(XMLSummaryWriter.A_FIELD, fieldName);
            elt.setAttribute(XMLSummaryWriter.A_FIELD_TYPE,
                    fieldType.getName().toUnicodeString());
            elt.setAttribute(XMLSummaryWriter.A_DEF, def);

            summary.add(elt);
        } catch (Exception e) {
            throw new SSASerializationException(e);
        }
    }

    /**
     * <putstatic class="Ljava/lang/System" field="security"
     * fieldType="Ljava/lang/SecurityManager" value="secure" />
     * 
     * <putfield class="Ljava/lang/Thread" field="runnable"
     * fieldType="Ljava/lang/Runnable" ref="arg0" value="arg0" />
     * 
     */
    @Override
    public void visitPut(SSAPutInstruction instruction) {
        try {
            String eltName;

            if (instruction.isStatic()) {
                eltName = XMLSummaryWriter.E_PUTSTATIC;
            } else {
                eltName = XMLSummaryWriter.E_PUTFIELD;
            }
            Element elt = doc.createElement(eltName);

            if (!instruction.isStatic()) {
                String refName = getLocalName(instruction.getRef());
                elt.setAttribute(XMLSummaryWriter.A_REF, refName);
            }

            String value = getLocalName(instruction.getVal());
            TypeReference fieldType = instruction.getDeclaredFieldType();
            TypeReference classType = instruction.getDeclaredField()
                    .getDeclaringClass();

            String fieldName = instruction.getDeclaredField().getName()
                    .toUnicodeString();

            elt.setAttribute(XMLSummaryWriter.A_CLASS, classType.getName().toUnicodeString());
            elt.setAttribute(XMLSummaryWriter.A_FIELD, fieldName);
            elt.setAttribute(XMLSummaryWriter.A_FIELD_TYPE,
                    fieldType.getName().toUnicodeString());
            elt.setAttribute(XMLSummaryWriter.A_VALUE, value);

            summary.add(elt);
        } catch (Exception e) {
            throw new SSASerializationException(e);
        }

    }

    /**
     * 	  <call type="virtual" name="put"
	 *          class="Ljava/util/Hashtable"
	 *          descriptor="(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"
	 *          arg0="x" arg1="key" arg2="value" def="local_def" />
     */
    @Override
    public void visitInvoke(SSAInvokeInstruction instruction) {
        try {
            Element elt = doc.createElement(XMLSummaryWriter.E_CALL);
            
            MethodReference callee = instruction.getDeclaredTarget();
            
            String descString = callee.getDescriptor().toUnicodeString();
            elt.setAttribute(XMLSummaryWriter.A_DESCRIPTOR, descString);
            
            String typeString = 
                instruction.getCallSite().getInvocationString();
            elt.setAttribute(XMLSummaryWriter.A_TYPE, typeString);
            
            String nameString = callee.getName().toUnicodeString();
            elt.setAttribute(XMLSummaryWriter.A_NAME, nameString);
            
            String classString = instruction.getDeclaredTarget().getDeclaringClass().getName().toUnicodeString();
            elt.setAttribute(XMLSummaryWriter.A_CLASS, classString);

            if (! instruction.getDeclaredResultType().equals(TypeReference.Void) ) {
                int defNum = instruction.getDef();
                String localName = newLocalDef(defNum);
                elt.setAttribute(XMLSummaryWriter.A_DEF, localName);
            }

            int paramCount = instruction.getNumberOfParameters();
            for (int i=0; i < paramCount; i++) {
            	String argName = getLocalName(instruction.getUse(i));
            	elt.setAttribute(XMLSummaryWriter.A_ARG+i, argName);
            }
            
            summary.add(elt);
        } catch (Exception e) {
            throw new SSASerializationException(e);
        }
        
    }

    @Override
    public void visitNew(SSANewInstruction instruction) {
        try {
            int defNum = instruction.getDef();
            String localName = newLocalDef(defNum);

            TypeReference type = instruction.getConcreteType();

            String className = type.getName().toUnicodeString();

            Element elt = doc.createElement(XMLSummaryWriter.E_NEW);
            elt.setAttribute(XMLSummaryWriter.A_DEF, localName);
            elt.setAttribute(XMLSummaryWriter.A_CLASS, className);
            
            if (type.isArrayType()) {
            	// array allocations need a size value
            	Element sizeElt = doc.createElement(XMLSummaryWriter.E_CONSTANT);
            	final String sizeName = "sizeOf$allocAt" + instruction.getNewSite().getProgramCounter();
				sizeElt.setAttribute(XMLSummaryWriter.A_NAME, sizeName);
				sizeElt.setAttribute(XMLSummaryWriter.A_TYPE, "int");
				sizeElt.setAttribute(XMLSummaryWriter.A_VALUE, "1");
				summary.add(sizeElt);
				
            	elt.setAttribute(XMLSummaryWriter.A_SIZE, sizeName);
            }
            
            summary.add(elt);
        } catch (Exception e) {
            throw new SSASerializationException(e);
        }
    }

    @Override
    public void visitArrayLength(SSAArrayLengthInstruction instruction) {
        throw new SSASerializationException("Unsupported.");
    }

    /**
     * Serialiaze a throw to XML.
     * 
     * Something like this?
     * 
     * <throw value="val_localDef" /> 
     */
    @Override
    public void visitThrow(SSAThrowInstruction instruction) {
        throw new SSASerializationException("Exceptions not currently supported.");
//        try {
//            int exValNo = instruction.getException();
//            String value = getLocalName(exValNo);
//            
//            Element elt = doc.createElement(XMLSummaryWriter.E_ATHROW);
//            elt.setAttribute(XMLSummaryWriter.A_VALUE, value);
//            summary.add(elt);
//        } catch (Exception e) {
//            throw new SSASerializationException(e);
//        }
    }

    @Override
    public void visitMonitor(SSAMonitorInstruction instruction) {
        throw new SSASerializationException("Unsupported.");
    }

    @Override
    public void visitCheckCast(SSACheckCastInstruction instruction) {
        throw new SSASerializationException("Unsupported.");
    }

    @Override
    public void visitInstanceof(SSAInstanceofInstruction instruction) {
        throw new SSASerializationException("Unsupported.");
    }

    @Override
    public void visitPhi(SSAPhiInstruction instruction) {
        throw new SSASerializationException("Unsupported.");
    }

    @Override
    public void visitPi(SSAPiInstruction instruction) {
        throw new SSASerializationException("Unsupported.");
    }

    @Override
    public void visitGetCaughtException(
            SSAGetCaughtExceptionInstruction instruction) {
        throw new SSASerializationException("Unsupported.");
    }

    @Override
    public void visitLoadMetadata(SSALoadMetadataInstruction instruction) {
        throw new SSASerializationException("Unsupported.");
    }

    /**
     * Add a new defNum, creating a name for that defnum.
     * 
     * @param defNum
     */
    private String newLocalDef(int defNum) {
        String newName = "localdef_" + defCounter;
        localDefs.put(defNum, newName);
        defCounter++;

        return newName;
    }

    /**
     * Get a local name for the provided defNum.
     * 
     * If, for some reason, the defNum has not yet been seen (and, thus, has no
     * local name associated with it) then this will throw an illegal state
     * exception.
     *
     * TODO needs to return 'arg0' -> 'argN' for those value numbers...
     *
     * @param defNum
     *
     * @return
     * @throws IllegalStateException
     */
    private String getLocalName(int defNum) throws IllegalStateException {
    	if (0 == defNum) {
    		return "unknown";
    	}
        if (localDefs.containsKey(defNum)) {
            return localDefs.get(defNum);
        }
        return XMLSummaryWriter.A_ARG + (defNum - 1);
//        throw new IllegalStateException("defNum: " + defNum
//                + " is not defined.");
    }


    public List<Element> getInstSummary() {
        return summary;
    }

    private String typeRefToStr(TypeReference fieldType)
            throws UTFDataFormatException {
        Atom className = fieldType.getName().getClassName();
        Atom pkgName = fieldType.getName().getPackage();
        if ( null == pkgName && null != className ) {
        	logger.debug("pkg name null for type ref: "+fieldType);
        	return className.toUnicodeString();
        }

        if (null == className ) {
        	logger.debug("className null for type ref: "+fieldType);
        }
        
        return pkgName.toUnicodeString() + "/" + className.toUnicodeString();
    }

}
