/* FJBG -- Fast Java Bytecode Generator
 * Copyright 2002-2013 LAMP/EPFL
 * @author  Michel Schinz
 */

package ch.epfl.lamp.fjbg;

import java.util.*;
import java.io.*;

/**
 * Representation of a Java class.
 *
 * @author Michel Schinz, Stephane Micheloud
 * @version 1.1
 */
public class JClass extends JMember {

    /** Magic number for Java class files. */
    public final static int MAGIC_NUMBER = 0xCAFEBABE;

    protected final JAttributeFactory attributeFactory;

    protected final String superclassName;
    protected final String[] interfaceNames;
    protected final String sourceFileName;
    protected final JConstantPool pool;

    public final static String[] NO_INTERFACES = new String[0];

    protected final LinkedList/*<JMethod>*/ methods = new LinkedList();
    protected final LinkedList/*<JField>*/ fields = new LinkedList();

    protected JInnerClassesAttribute innerClasses;

    protected int major;
    protected int minor;

    /**
     * Creates a new class with its access flags, name, superclass name,
     * interfaces names and source file name initialized to a given value.
     * The constructor also initializes the pool and adds a sourceFileName
     * attribute to the class.
     * @param accessFlags the int representing the access flags of the class.
     * @param name the string representing the name of the class.
     * @param superclassName the string representing the name of the class'
     * superclass.
     * @param interfaceNames the list of strings representing the names of the
     * interfaces implemented by the class.
     * @param sourceFileName name of the file from which the class was compiled.
     */
    protected JClass(FJBGContext context,
                     int accessFlags,
                     String name,
                     String superclassName,
                     String[] interfaceNames,
                     String sourceFileName) {
        super(context, accessFlags, name);
        this.attributeFactory = context.getJAttributeFactory();

        this.major = context.MAJOR_VERSION;
        this.minor = context.MINOR_VERSION;

        this.superclassName = superclassName;
        this.interfaceNames = interfaceNames;
        this.sourceFileName = sourceFileName;
        this.pool = context.JConstantPool();
        if (sourceFileName != null)
            addAttribute(context.JSourceFileAttribute(this, sourceFileName));
    }

    protected JClass(FJBGContext context, DataInputStream stream)
        throws IOException {
        super(context);
        this.attributeFactory = context.getJAttributeFactory();

        int magic = stream.readInt();
        if (magic != MAGIC_NUMBER)
            throw new IllegalArgumentException("invalid magic number: "+magic);

        minor = stream.readShort();
        major = stream.readShort();
        pool = context.JConstantPool(stream);
        accessFlags = stream.readShort();

        // This class, super class and interfaces
        name = pool.lookupClass(stream.readShort());
        superclassName = pool.lookupClass(stream.readShort());
        interfaceNames = new String[stream.readShort()];
        for (int i = 0; i < interfaceNames.length; ++i)
            interfaceNames[i] = pool.lookupClass(stream.readShort());

        // Fields, methods and attributes
        int fieldsCount = stream.readShort();
        for (int i = 0; i < fieldsCount; ++i)
            addField(context.JField(this, stream));

        int methodsCount = stream.readShort();
        for (int i = 0; i < methodsCount; ++i)
            addMethod(context.JMethod(this, stream));

        String fileName = null;
        int attributesCount = stream.readShort();
        for (int i = 0; i < attributesCount; ++i) {
            JAttribute attr = attributeFactory.newInstance(this, this, stream);
            if (attr instanceof JSourceFileAttribute)
                fileName = ((JSourceFileAttribute)attr).getFileName();
            else if (attr instanceof JInnerClassesAttribute)
                innerClasses = (JInnerClassesAttribute)attr;
            addAttribute(attr);
        }
        sourceFileName = fileName;
    }

    /**
     * Gets the name of the class' superclass.
     * @return The string representing the name of the class' superclass.
     */
    public String getSuperclassName() { return superclassName; }

    /**
     * Gets the names of the interfaces implemented by the class.
     * @return The array containing the string representations of the
     * names of the interfaces implemented by the class.
     */
    public String[] getInterfaceNames() { return interfaceNames; }

    /**
     * Gets the source file name of this class.
     * @return The string representing the source file name of this class.
     */
    public String getSourceFileName() { return sourceFileName; }

    /**
     * Gets the type of the objects that are instances of the class.
     * @return The type of the instances of the class.
     */
    public JType getType() { return new JObjectType(name); }

    public JClass getJClass() { return this; }

    public boolean isPublic() {
        return (accessFlags & JAccessFlags.ACC_PUBLIC) != 0;
    }

    public boolean isPrivate() {
        return (accessFlags & JAccessFlags.ACC_PRIVATE) != 0;
    }

    public boolean isProtected() {
        return (accessFlags & JAccessFlags.ACC_PROTECTED) != 0;
    }

    public boolean isStatic() {
        return (accessFlags & JAccessFlags.ACC_STATIC) != 0;
    }

    public boolean isFinal() {
        return (accessFlags & JAccessFlags.ACC_FINAL) != 0;
    }

    public boolean isAbstract() {
        return (accessFlags & JAccessFlags.ACC_ABSTRACT) != 0;
    }

    /**
     * Gets the version number of the class.
     * @param major The int representing the major part of the version number
     * of the class.
     * @param minor The int representing the minor part of the version number
     * of the class.
     */
    public void setVersion(int major, int minor) {
        assert !frozen;
        this.major = major;
        this.minor = minor;
    }

    /**
     * Gets the major part of the number describing the version of the class.
     * @return The int representing the major part of the version number of
     * the class.
     */
    public int getMajorVersion() { return major; }

    /**
     * Gets the minor part of the number describing the version of the class.
     * @return The int representing the minor part of the version number of
     * the class.
     */
    public int getMinorVersion() { return minor; }

    /**
     * Gets the constant pool of the class.
     * @return The constant pool of the class.
     */
    public JConstantPool getConstantPool() { return pool; }

    public JInnerClassesAttribute getInnerClasses() {
    	if (innerClasses == null) {
    		innerClasses = new JInnerClassesAttribute(context, this);
    		addAttribute(innerClasses);
    	}
    	return innerClasses;
    }

    /**
     * Decides if the class is an interface.
     * @return The boolean representing if the class is an interface or not.
     */
    public boolean isInterface() {
        return (accessFlags & JAccessFlags.ACC_INTERFACE) != 0;
    }

    public void addField(JField field) {
        assert !frozen;
        fields.add(field);
    }

    /**
     * Create and add a new field to the class.
     */
    public JField addNewField(int accessFlags, String name, JType type) {
        assert !frozen;
        JField f = context.JField(this, accessFlags, name, type);
        addField(f);
        return f;
    }

    protected void addMethod(JMethod method) {
        assert !frozen;
        methods.add(method);
    }

    /**
     * Create and add a new method to the class.
     */
    public JMethod addNewMethod(int accessFlags,
                                String name,
                                JType returnType,
                                JType[] argTypes,
                                String[] argNames) {
        assert !frozen;
        JMethod m = context.JMethod(this,
                                    accessFlags,
                                    name,
                                    returnType,
                                    argTypes,
                                    argNames);
        addMethod(m);
        return m;
    }

    /**
     * Remove a previously-added method. This makes no attempt at
     * minimising the constant pool by removing all constants which
     * were used only by this method.
     */
    public void removeMethod(JMethod m) {
        assert !frozen;
        methods.remove(m);
    }

    public JField[] getFields() {
        return (JField[])fields.toArray(new JField[fields.size()]);
    }

    public JMethod[] getMethods() {
        return (JMethod[])methods.toArray(new JMethod[methods.size()]);
    }

    /**
     * Freeze the contents of this class so that it can be written to
     * a file.
     */
    public void freeze() {
        assert !frozen;
        frozen = true;
    }

    /**
     * Writes the contents of the class to a file referenced by its name.
     * @param fileName The name of the file in which the class must be written.
     */
    public void writeTo(String fileName) throws IOException {
        writeTo(new File(fileName));
    }

    /**
     * Writes the contents of the class to a file.
     * @param file The file in which the class must be written.
     */
    public void writeTo(File file) throws IOException {
        File parent = file.getParentFile();
        if (parent != null && !parent.isDirectory())
            if (!parent.mkdirs())
                throw new IOException("cannot create directory " + parent);

        FileOutputStream fStream = new FileOutputStream(file);
        BufferedOutputStream bStream = new BufferedOutputStream(fStream);
        DataOutputStream dStream = new DataOutputStream(bStream);
        writeTo(dStream);
        dStream.close();
        bStream.close();
        fStream.close();
    }

    /**
     * Writes the contents of the class to a data stream.
     * @param stream The data stream in which the class must be written.
     */
    public void writeTo(DataOutputStream stream) throws IOException {
        if (!frozen) freeze();

        int thisClassIdx = pool.addClass(name);
        int superClassIdx = pool.addClass(superclassName);
        int[] interfacesIdx = new int[interfaceNames.length];

        for (int i = 0; i < interfaceNames.length; ++i)
            interfacesIdx[i] = pool.addClass(interfaceNames[i]);

        pool.freeze();

        // Magic number.
        stream.writeInt(MAGIC_NUMBER);
        // Version
        stream.writeShort(minor);
        stream.writeShort(major);
        // Constant pool
        pool.writeTo(stream);
        // Access flags
        stream.writeShort(accessFlags);

        // This class, super class and interfaces
        stream.writeShort(thisClassIdx);
        stream.writeShort(superClassIdx);
        stream.writeShort(interfacesIdx.length);
        for (int i = 0; i < interfacesIdx.length; ++i)
            stream.writeShort(interfacesIdx[i]);

        // Fields and methods
        stream.writeShort(fields.size());
        Iterator fieldsIt = fields.iterator();
        while (fieldsIt.hasNext())
            ((JField)fieldsIt.next()).writeTo(stream);

        stream.writeShort(methods.size());
        Iterator methodsIt = methods.iterator();
        while (methodsIt.hasNext())
            ((JMethod)methodsIt.next()).writeTo(stream);

        // Attributes
        JAttribute.writeTo(attributes, stream);
    }

    // Follows javap output format for ClassFile.
    /*@Override*/ public String toString() {
        StringBuffer buf = new StringBuffer();
        if (sourceFileName != null) {
            buf.append("Compiled from \"");
            buf.append(sourceFileName);
            buf.append("\"\n");
        }
        buf.append(getMemberName());
        buf.append(toExternalName(getName()));
        if (!isInterface()) {
            buf.append(" extends ");
            buf.append(toExternalName(getSuperclassName()));
        }
        if (interfaceNames.length > 0) {
            if (isInterface()) buf.append(" extends ");
            else buf.append(" implements ");
            for (int i = 0; i < interfaceNames.length; ++i) {
                if (i > 0) buf.append(",");
                buf.append(toExternalName(interfaceNames[i]));
            }
        }
        buf.append("\n");
        Iterator attrsIt = attributes.iterator();
        while (attrsIt.hasNext()) {
            JAttribute attr = (JAttribute)attrsIt.next();
            buf.append(attr);
        }
        buf.append("  minor version: ");
        buf.append(minor);
        buf.append("\n  major version: ");
        buf.append(major);
        buf.append("\n");
        buf.append(pool);
        buf.append("\n{\n");
        JField[] jfields = getFields();
        for (int i = 0; i < jfields.length; ++i) {
            if (i > 0) buf.append("\n");
            buf.append(jfields[i]);
        }
        buf.append("\n");
        JMethod[] jmethods = getMethods();
        for (int i = 0; i < jmethods.length; ++i) {
            if (i > 0) buf.append("\n");
            buf.append(jmethods[i]);
        }
        buf.append("\n}\n");
        return buf.toString();
    }

    private String getMemberName() {
        StringBuffer buf = new StringBuffer();
        if (isPublic()) buf.append("public ");
        else if (isProtected()) buf.append("protected ");
        else if (isPrivate()) buf.append("private ");
        if (isInterface())
            buf.append("interface ");
        else {
            if (isAbstract()) buf.append("abstract ");
            else if (isFinal()) buf.append("final ");
            buf.append("class ");
        }
        return buf.toString();
    }
}
