/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * See LICENSE.txt included in this distribution for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at LICENSE.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright (c) 2005, 2013, Oracle and/or its affiliates. All rights reserved.
 */
package org.opensolaris.opengrok.analysis.executables;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.bcel.classfile.Attribute;
import org.apache.bcel.classfile.ClassFormatException;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.Constant;
import org.apache.bcel.classfile.ConstantCP;
import org.apache.bcel.classfile.ConstantClass;
import org.apache.bcel.classfile.ConstantDouble;
import org.apache.bcel.classfile.ConstantFloat;
import org.apache.bcel.classfile.ConstantInteger;
import org.apache.bcel.classfile.ConstantLong;
import org.apache.bcel.classfile.ConstantNameAndType;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.ConstantString;
import org.apache.bcel.classfile.ConstantUtf8;
import org.apache.bcel.classfile.ExceptionTable;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.LocalVariable;
import org.apache.bcel.classfile.LocalVariableTable;
import org.apache.bcel.classfile.Utility;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.TextField;
import org.opensolaris.opengrok.analysis.FileAnalyzer;
import org.opensolaris.opengrok.analysis.FileAnalyzerFactory;
import org.opensolaris.opengrok.analysis.IteratorReader;
import org.opensolaris.opengrok.analysis.StreamSource;
import org.opensolaris.opengrok.configuration.RuntimeEnvironment;

/**
 * Analyzes Java Class files Created on September 23, 2005
 *
 * @author Chandan
 * @author Lubos Kosco , January 2010 , updated bcel, comment on thread safety
 */
public class JavaClassAnalyzer extends FileAnalyzer {

    private final String urlPrefix = RuntimeEnvironment.getInstance().getUrlPrefix();

    /**
     * Creates a new instance of JavaClassAnalyzer
     *
     * @param factory The factory that creates JavaClassAnalyzers
     */
    protected JavaClassAnalyzer(FileAnalyzerFactory factory) {
        super(factory);
    }

    @Override
    public void analyze(Document doc, StreamSource src, Writer xrefOut) throws IOException {
        try (InputStream in = src.getStream()) {
            analyze(doc, in, xrefOut);
        }
    }

    void analyze(Document doc, InputStream in, Writer xrefOut) throws IOException {
        List<String> defs = new ArrayList<>();
        List<String> refs = new ArrayList<>();
        List<String> full = new ArrayList<>();

        ClassParser classparser = new ClassParser(in, doc.get("path"));
        StringWriter out = new StringWriter();
        StringWriter fout = new StringWriter();
        getContent(out, fout, classparser.parse(), defs, refs, full);
        String fullt = fout.toString();
        String xref = out.toString();

        if (xrefOut != null) {
            xrefOut.append(xref);
            try { 
                xrefOut.flush();
            } catch (IOException ex) {
                Logger.getLogger(JavaClassAnalyzer.class.getName()).log(Level.WARNING, "Couldn't flush xref, will retry once added to doc", ex);
            }
        }        
        xref = null; //flush the xref        

        StringWriter cout=new StringWriter();
        for (String fl : full) {
            cout.write(fl);
            cout.write('\n');
        }
        String constants = cout.toString();
        
        StringReader fullout=new StringReader(fullt);

        doc.add(new TextField("defs", new IteratorReader(defs)));
        doc.add(new TextField("refs", new IteratorReader(refs)));
        doc.add(new TextField("full", fullout));
        doc.add(new TextField("full", constants, Store.NO));
    }

    
    private static final String AHREF="<a href=\"";
    private static final String AHREFT_END="\">";
    private static final String AHREFEND="</a>";
    private static final String ADEFS="defs=";
    private static final String APATH="path=";
    private static final String AIHREF="\" href=\"";
    private static final String ADHREF="<a class=\"d\" name=\"";
    private final StringBuffer rstring=new StringBuffer(512);
    protected String linkPath(String path) {
        rstring.setLength(0);
        return rstring.append(AHREF).append(urlPrefix).append(APATH).append(path).append(AHREFT_END).append(path).append(AHREFEND).toString();
    }

    protected String linkDef(String def) {
        rstring.setLength(0);
        return rstring.append(AHREF).append(urlPrefix).append(ADEFS).append(def).append(AHREFT_END).append(def).append(AHREFEND).toString();
    }

    protected String tagDef(String def) {
        rstring.setLength(0);
        return rstring.append(ADHREF).append(def).append(AIHREF).append(urlPrefix).append(ADEFS).append(def).append(AHREFT_END).append(def).append(AHREFEND).toString();
    }

private static final String PACKAGE="package ";
private static final char EOL='\n';
private static final char TAB='\t';
private static final char SPACE=' ';
private static final String EXTENDS=" extends ";
private static final String IMPLEMENTS=" implements ";
private static final String THROWS=" throws ";
private static final String THIS="this";
private static final String LCBREOL=" {\n";
private static final String LBRA=" (";
private static final String COMMA=", ";
private static final String RBRA=") ";
private static final String RCBREOL="}\n";
    
//TODO this class needs to be thread safe to avoid bug 13364, which was fixed by just updating bcel to 5.2
    private void getContent(Writer out, Writer fout, JavaClass c,
            List<String> defs, List<String> refs, List<String> full)
            throws IOException {        
        String t;
        ConstantPool cp = c.getConstantPool();
        int[] v = new int[cp.getLength() + 1];
        out.write(linkPath(t = c.getSourceFileName()));
        defs.add(t);
        refs.add(t);
        fout.write(t);
        out.write(EOL);
        fout.write(EOL);

        out.write(PACKAGE);
        fout.write(PACKAGE);
        out.write(linkDef(t = c.getPackageName()));
        defs.add(t);
        refs.add(t);
        fout.write(t);

        out.write(EOL);
        fout.write(EOL);
        String aflg;
        out.write(aflg = Utility.accessToString(c.getAccessFlags(), true));        
        if (aflg != null) {            
            out.write(SPACE);
            fout.write(aflg);
            fout.write(SPACE);
        }

        v[c.getClassNameIndex()] = 1;
        out.write(tagDef(t = c.getClassName()));
        defs.add(t);
        refs.add(t);
        fout.write(t);
        out.write(EXTENDS);
        fout.write(EXTENDS);

        v[c.getSuperclassNameIndex()] = 1;
        out.write(linkDef(t = c.getSuperclassName()));
        refs.add(t);
        fout.write(t);
        for (int i : c.getInterfaceIndices()) {
            v[i] = 1;
        }
        String ins[] = c.getInterfaceNames();
        if (ins != null && ins.length > 0) {
            out.write(IMPLEMENTS);
            fout.write(IMPLEMENTS);
            for (String in : ins) {
                out.write(linkDef(t = in));
                refs.add(t);
                fout.write(t);
                out.write(SPACE);
                fout.write(SPACE);
            }
        }
        out.write(LCBREOL);
        fout.write(LCBREOL);

        for (Attribute a : c.getAttributes()) {
            if (a.getTag() == org.apache.bcel.Constants.ATTR_CODE) {
                for (Attribute ca : ((Code) a).getAttributes()) {
                    if (ca.getTag() == org.apache.bcel.Constants.ATTR_LOCAL_VARIABLE_TABLE) {
                        for (LocalVariable l : ((LocalVariableTable) ca).getLocalVariableTable()) {
                            printLocal(out, fout, l, v, defs, refs);
                        }
                    }
                }
            } else if (a.getTag() == org.apache.bcel.Constants.ATTR_SOURCE_FILE) {
                v[a.getNameIndex()] = 1;
                break;
            }
        }

        String aflgs;
        String fldsig;
        String tdef;
        for (org.apache.bcel.classfile.Field fld : c.getFields()) {
            out.write(TAB);
            fout.write(TAB);
            aflgs = Utility.accessToString(fld.getAccessFlags());            
            if (aflgs != null && aflgs.length() > 0) {
                out.write(aflgs);
                fout.write(aflgs);
                fout.write(SPACE);
                out.write(SPACE);
            }
            fldsig=Utility.signatureToString(fld.getSignature());
            out.write(fldsig);
            fout.write(fldsig);
            out.write(SPACE);
            fout.write(SPACE);
            tdef=tagDef(t = fld.getName());
            out.write(tdef);
            fout.write(tdef);
            defs.add(t);
            refs.add(t);
            out.write(EOL);
            fout.write(EOL);
            //TODO show Attributes
        }

        String sig;
        String msig;
        String ltdef;
        for (org.apache.bcel.classfile.Method m : c.getMethods()) {
            out.write(TAB);
            fout.write(TAB);
            aflgs = Utility.accessToString(m.getAccessFlags());            
            if (aflgs != null && aflgs.length() > 0) {
                out.write(aflgs);
                fout.write(aflgs);
                out.write(SPACE);
                fout.write(SPACE);
            }
            sig = m.getSignature();
            msig=Utility.methodSignatureReturnType(sig, false);
            out.write(msig);
            fout.write(msig);
            out.write(SPACE);
            fout.write(SPACE);
            ltdef=tagDef(t = m.getName());
            out.write(ltdef);
            fout.write(ltdef);
            defs.add(t);
            refs.add(t);
            out.write(LBRA);
            fout.write(LBRA);
            String[] args = Utility.methodSignatureArgumentTypes(sig, false);
            for (int i = 0; i < args.length; i++) {
                t = args[i];
                out.write(t);
                fout.write(t);
                int spi = t.indexOf(SPACE);
                if (spi > 0) {
                    refs.add(t.substring(0, spi));
                    defs.add(t.substring(spi + 1));
                }
                if (i < args.length - 1) {
                    out.write(COMMA);
                    fout.write(COMMA);
                }
            }
            out.write(RBRA);
            fout.write(RBRA);
            ArrayList<LocalVariable[]> locals = new ArrayList<>();
            for (Attribute a : m.getAttributes()) {
                if (a.getTag() == org.apache.bcel.Constants.ATTR_EXCEPTIONS) {
                    for (int i : ((ExceptionTable) a).getExceptionIndexTable()) {
                        v[i] = 1;
                    }
                    String[] exs = ((ExceptionTable) a).getExceptionNames();
                    if (exs != null && exs.length > 0) {
                        out.write(THROWS);
                        fout.write(THROWS);
                        for (String ex : exs) {
                            out.write(linkDef(ex));
                            fout.write(ex);
                            refs.add(ex);
                            out.write(SPACE);
                            fout.write(SPACE);
                        }
                    }
                } else if (a.getTag() == org.apache.bcel.Constants.ATTR_CODE) {
                    for (Attribute ca : ((Code) a).getAttributes()) {
                        if (ca.getTag() == org.apache.bcel.Constants.ATTR_LOCAL_VARIABLE_TABLE) {
                            locals.add(((LocalVariableTable) ca).getLocalVariableTable());
                        }
                    }
                }
            }
            out.write(EOL);
            fout.write(EOL);
            if (!locals.isEmpty()) {
                for (LocalVariable[] ls : locals) {
                    for (LocalVariable l : ls) {
                        printLocal(out, fout, l, v, defs, refs);
                    }
                }
            }
        }
        out.write(RCBREOL);
        fout.write(RCBREOL);
        for (int i = 0; i < v.length - 1; i++) {
            if (v[i] != 1) {
                Constant constant = cp.getConstant(i);
                if (constant != null) {
                    full.add(constantToString(constant, cp, v));
                }
            }
        }
    }

    private void printLocal(Writer out, Writer fout, LocalVariable l,
            int[] v, List<String> defs, List<String> refs) throws IOException {
        v[l.getIndex()] = 1;
        v[l.getNameIndex()] = 1;
        v[l.getSignatureIndex()] = 1;
        if (!THIS.equals(l.getName())) {
            out.write(TAB);out.write(TAB);
            fout.write(TAB);fout.write(TAB);
            String sig=Utility.signatureToString(l.getSignature());
            out.write(sig);
            fout.write(sig);
            out.write(SPACE);
            fout.write(SPACE);
            String t;
            out.write(t = l.getName());
            defs.add(t);
            refs.add(t);
            fout.write(t);
            out.write(EOL);
            fout.write(EOL);
        }
    }

    public String constantToString(Constant c, ConstantPool cp, int[] v)
            throws ClassFormatException {
        String str;
        int i, j;
        byte tag = c.getTag();

        switch (tag) {
            case org.apache.bcel.Constants.CONSTANT_Class:
                i = ((ConstantClass) c).getNameIndex();
                v[i] = 1;
                Constant con = cp.getConstant(i, org.apache.bcel.Constants.CONSTANT_Utf8);
                str = Utility.compactClassName(((ConstantUtf8) con).getBytes(), false);
                break;

            case org.apache.bcel.Constants.CONSTANT_String:
                i = ((ConstantString) c).getStringIndex();
                v[i] = 1;
                Constant con2 = cp.getConstant(i, org.apache.bcel.Constants.CONSTANT_Utf8);
                str = ((ConstantUtf8) con2).getBytes();
                break;

            case org.apache.bcel.Constants.CONSTANT_Utf8:
                str = ((ConstantUtf8) c).toString();
                break;
            case org.apache.bcel.Constants.CONSTANT_Double:
                str = ((ConstantDouble) c).toString();
                break;
            case org.apache.bcel.Constants.CONSTANT_Float:
                str = ((ConstantFloat) c).toString();
                break;
            case org.apache.bcel.Constants.CONSTANT_Long:
                str = ((ConstantLong) c).toString();
                break;
            case org.apache.bcel.Constants.CONSTANT_Integer:
                str = ((ConstantInteger) c).toString();
                break;

            case org.apache.bcel.Constants.CONSTANT_NameAndType:
                i = ((ConstantNameAndType) c).getNameIndex();
                v[i] = 1;
                j = ((ConstantNameAndType) c).getSignatureIndex();
                v[j] = 1;
                String sig = constantToString(cp.getConstant(j), cp, v);
                if (sig.charAt(0) == '(') {
                    str = Utility.methodSignatureToString(sig,
                            constantToString(cp.getConstant(i), cp, v), " ");
                } else {
                    str = Utility.signatureToString(sig) + ' ' +
                            constantToString(cp.getConstant(i), cp, v);
                }
                //str = constantToString(cp.getConstant(i)) +' ' + sig;

                break;

            case org.apache.bcel.Constants.CONSTANT_InterfaceMethodref:
            case org.apache.bcel.Constants.CONSTANT_Methodref:
            case org.apache.bcel.Constants.CONSTANT_Fieldref:
                i = ((ConstantCP) c).getClassIndex();
                v[i] = 1;
                j = ((ConstantCP) c).getNameAndTypeIndex();
                v[j] = 1;
                str = (constantToString(cp.getConstant(i), cp, v) + ' ' +
                        constantToString(cp.getConstant(j), cp, v));
                break;

            default: // Never reached
                throw new ClassFormatException("Unknown constant type " + tag);
        }
        return str;
    }
}
