/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.cf.direct;

import com.android.dx.cf.attrib.AttSourceFile;
import com.android.dx.cf.cst.ConstantPoolParser;
import com.android.dx.cf.iface.Attribute;
import com.android.dx.cf.iface.AttributeList;
import com.android.dx.cf.iface.ClassFile;
import com.android.dx.cf.iface.FieldList;
import com.android.dx.cf.iface.MethodList;
import com.android.dx.cf.iface.ParseException;
import com.android.dx.cf.iface.ParseObserver;
import com.android.dx.cf.iface.StdAttributeList;
import com.android.dx.rop.code.AccessFlags;
import com.android.dx.rop.cst.ConstantPool;
import com.android.dx.rop.cst.CstType;
import com.android.dx.rop.cst.CstUtf8;
import com.android.dx.rop.cst.StdConstantPool;
import com.android.dx.rop.type.StdTypeList;
import com.android.dx.rop.type.Type;
import com.android.dx.rop.type.TypeList;
import com.android.dx.util.ByteArray;
import com.android.dx.util.Hex;

/**
 * Class file with info taken from a <code>byte[]</code> or slice thereof.
 */
public class DirectClassFile implements ClassFile {
    /** the expected value of the ClassFile.magic field */
    private static final int CLASS_FILE_MAGIC = 0xcafebabe;

    /**
     * minimum <code>.class</code> file major version
     * 
     * The class file definition (vmspec/2nd-edition) says:
     * 
     *     "Implementations of version 1.2 of the
     *     Java 2 platform can support class file
     *     formats of versions in the range 45.0
     *     through 46.0 inclusive."
     * 
     * The class files generated by the build are currently
     * (as of 11/2006) reporting version 49.0 (0x31.0x00),
     * however, so we use that as our upper bound.
     * 
     * Valid ranges are typically of the form
     * "A.0 through B.C inclusive" where A <= B and C >= 0,
     * which is why we don't have a CLASS_FILE_MIN_MINOR_VERSION.
     */
    private static final int CLASS_FILE_MIN_MAJOR_VERSION = 45;

    /** maximum <code>.class</code> file major version */
    private static final int CLASS_FILE_MAX_MAJOR_VERSION = 50;

    /** maximum <code>.class</code> file minor version */
    private static final int CLASS_FILE_MAX_MINOR_VERSION = 0;

    /**
     * non-null; the file path for the class, excluding any base directory
     * specification 
     */
    private final String filePath;

    /** non-null; the bytes of the file */
    private final ByteArray bytes;

    /**
     * whether to be strict about parsing; if
     * <code>false</code>, this avoids doing checks that only exist
     * for purposes of verification (such as magic number matching and
     * path-package consistency checking) 
     */
    private final boolean strictParse;

    /**
     * null-ok; the constant pool; only ever <code>null</code>
     * before the constant pool is successfully parsed 
     */
    private StdConstantPool pool;

    /**
     * the class file field <code>access_flags</code>; will be <code>-1</code>
     * before the file is successfully parsed 
     */
    private int accessFlags;

    /**
     * null-ok; the class file field <code>this_class</code>,
     * interpreted as a type constant; only ever <code>null</code>
     * before the file is successfully parsed
     */
    private CstType thisClass;

    /**
     * null-ok; the class file field <code>super_class</code>, interpreted
     * as a type constant if non-zero 
     */
    private CstType superClass;

    /**
     * null-ok; the class file field <code>interfaces</code>; only
     * ever <code>null</code> before the file is successfully
     * parsed 
     */
    private TypeList interfaces;

    /**
     * null-ok; the class file field <code>fields</code>; only ever
     * <code>null</code> before the file is successfully parsed 
     */
    private FieldList fields;

    /**
     * null-ok; the class file field <code>methods</code>; only ever
     * <code>null</code> before the file is successfully parsed 
     */
    private MethodList methods;

    /**
     * null-ok; the class file field <code>attributes</code>; only
     * ever <code>null</code> before the file is successfully
     * parsed 
     */
    private StdAttributeList attributes;

    /** null-ok; attribute factory, if any */
    private AttributeFactory attributeFactory;

    /** null-ok; parse observer, if any */
    private ParseObserver observer;

    /**
     * Returns the string form of an object or <code>"(none)"</code>
     * (rather than <code>"null"</code>) for <code>null</code>.
     * 
     * @param obj null-ok; the object to stringify
     * @return non-null; the appropriate string form
     */
    public static String stringOrNone(Object obj) {
        if (obj == null) {
            return "(none)";
        }

        return obj.toString();
    }

    /**
     * Constructs an instance.
     * 
     * @param bytes non-null; the bytes of the file
     * @param filePath non-null; the file path for the class,
     * excluding any base directory specification
     * @param strictParse whether to be strict about parsing; if
     * <code>false</code>, this avoids doing checks that only exist
     * for purposes of verification (such as magic number matching and
     * path-package consistency checking)
     */
    public DirectClassFile(ByteArray bytes, String filePath,
                           boolean strictParse) {
        if (bytes == null) {
            throw new NullPointerException("bytes == null");
        }

        if (filePath == null) {
            throw new NullPointerException("filePath == null");
        }

        this.filePath = filePath;
        this.bytes = bytes;
        this.strictParse = strictParse;
        this.accessFlags = -1;
    }

    /**
     * Constructs an instance.
     * 
     * @param bytes non-null; the bytes of the file
     * @param filePath non-null; the file path for the class,
     * excluding any base directory specification
     * @param strictParse whether to be strict about parsing; if
     * <code>false</code>, this avoids doing checks that only exist
     * for purposes of verification (such as magic number matching and
     * path-package consistency checking)
     */
    public DirectClassFile(byte[] bytes, String filePath,
                           boolean strictParse) {
        this(new ByteArray(bytes), filePath, strictParse);
    }

    /**
     * Sets the parse observer for this instance.
     * 
     * @param observer null-ok; the observer
     */
    public void setObserver(ParseObserver observer) {
        this.observer = observer;
    }

    /**
     * Sets the attribute factory to use.
     * 
     * @param attributeFactory non-null; the attribute factory
     */
    public void setAttributeFactory(AttributeFactory attributeFactory) {
        if (attributeFactory == null) {
            throw new NullPointerException("attributeFactory == null");
        }

        this.attributeFactory = attributeFactory;
    }

    /**
     * Gets the {@link ByteArray} that this instance's data comes from.
     * 
     * @return non-null; the bytes
     */
    public ByteArray getBytes() {
        return bytes;
    }

    /** {@inheritDoc} */
    public int getMagic() {
        parseToInterfacesIfNecessary();
        return getMagic0();
    }

    /** {@inheritDoc} */
    public int getMinorVersion() {
        parseToInterfacesIfNecessary();
        return getMinorVersion0();
    }

    /** {@inheritDoc} */
    public int getMajorVersion() {
        parseToInterfacesIfNecessary();
        return getMajorVersion0();
    }

    /** {@inheritDoc} */
    public int getAccessFlags() {
        parseToInterfacesIfNecessary();
        return accessFlags;
    }

    /** {@inheritDoc} */
    public CstType getThisClass() {
        parseToInterfacesIfNecessary();
        return thisClass;
    }

    /** {@inheritDoc} */
    public CstType getSuperclass() {
        parseToInterfacesIfNecessary();
        return superClass;
    }

    /** {@inheritDoc} */
    public ConstantPool getConstantPool() {
        parseToInterfacesIfNecessary();
        return pool;
    }

    /** {@inheritDoc} */
    public TypeList getInterfaces() {
        parseToInterfacesIfNecessary();
        return interfaces;
    }

    /** {@inheritDoc} */
    public FieldList getFields() {
        parseToEndIfNecessary();
        return fields;
    }

    /** {@inheritDoc} */
    public MethodList getMethods() {
        parseToEndIfNecessary();
        return methods;
    }

    /** {@inheritDoc} */
    public AttributeList getAttributes() {
        parseToEndIfNecessary();
        return attributes;
    }

    /** {@inheritDoc} */
    public CstUtf8 getSourceFile() {
        AttributeList attribs = getAttributes();
        Attribute attSf = attribs.findFirst(AttSourceFile.ATTRIBUTE_NAME);

        if (attSf instanceof AttSourceFile) {
            return ((AttSourceFile) attSf).getSourceFile();
        }

        return null;
    }

    /**
     * Constructs and returns an instance of {@link TypeList} whose
     * data comes from the bytes of this instance, interpreted as a
     * list of constant pool indices for classes, which are in turn
     * translated to type constants. Instance construction will fail
     * if any of the (alleged) indices turn out not to refer to
     * constant pool entries of type <code>Class</code>.
     * 
     * @param offset offset into {@link #bytes} for the start of the
     * data
     * @param size number of elements in the list (not number of bytes)
     * @return non-null; an appropriately-constructed class list
     */
    public TypeList makeTypeList(int offset, int size) {
        if (size == 0) {
            return StdTypeList.EMPTY;
        }

        if (pool == null) {
            throw new IllegalStateException("pool not yet initialized");
        }
        
        return new DcfTypeList(bytes, offset, size, pool, observer);
    }

    /**
     * Gets the class file field <code>magic</code>, but without doing any
     * checks or parsing first.
     * 
     * @return the magic value
     */
    public int getMagic0() {
        return bytes.getInt(0);
    }

    /**
     * Gets the class file field <code>minor_version</code>, but
     * without doing any checks or parsing first.
     * 
     * @return the minor version
     */
    public int getMinorVersion0() {
        return bytes.getUnsignedShort(4);
    }

    /**
     * Gets the class file field <code>major_version</code>, but
     * without doing any checks or parsing first.
     * 
     * @return the major version
     */
    public int getMajorVersion0() {
        return bytes.getUnsignedShort(6);
    }

    /**
     * Runs {@link #parse} if it has not yet been run to cover up to
     * the interfaces list.
     */
    private void parseToInterfacesIfNecessary() {
        if (accessFlags == -1) {
            parse();
        }
    }

    /**
     * Runs {@link #parse} if it has not yet been run successfully.
     */
    private void parseToEndIfNecessary() {
        if (attributes == null) {
            parse();
        }
    }

    /**
     * Does the parsing, handing exceptions.
     */
    private void parse() {
        try {
            parse0();
        } catch (ParseException ex) {
            ex.addContext("...while parsing " + filePath);
            throw ex;
        } catch (RuntimeException ex) {
            ParseException pe = new ParseException(ex);
            pe.addContext("...while parsing " + filePath);
            throw pe;
        }
    }

    /**
     * Sees if the .class file header magic/version are within
     * range.
     * 
     * @param magic the value of a classfile "magic" field
     * @param minorVersion the value of a classfile "minor_version" field
     * @param majorVersion the value of a classfile "major_version" field
     * @return true iff the parameters are valid and within range
     */
    private boolean isGoodVersion(int magic, int minorVersion,
            int majorVersion) {
        /* Valid version ranges are typically of the form
         * "A.0 through B.C inclusive" where A <= B and C >= 0,
         * which is why we don't have a CLASS_FILE_MIN_MINOR_VERSION.
         */
        if (magic == CLASS_FILE_MAGIC && minorVersion >= 0) {
            /* Check against max first to handle the case where
             * MIN_MAJOR == MAX_MAJOR.
             */
            if (majorVersion == CLASS_FILE_MAX_MAJOR_VERSION) {
                if (minorVersion <= CLASS_FILE_MAX_MINOR_VERSION) {
                    return true;
                }
            } else if (majorVersion < CLASS_FILE_MAX_MAJOR_VERSION &&
                       majorVersion >= CLASS_FILE_MIN_MAJOR_VERSION) {
                return true;
            }
        }

        return false;
    }

    /**
     * Does the actual parsing.
     */
    private void parse0() {
        if (bytes.size() < 10) {
            throw new ParseException("severely truncated class file");
        }

        if (observer != null) {
            observer.parsed(bytes, 0, 0, "begin classfile");
            observer.parsed(bytes, 0, 4, "magic: " + Hex.u4(getMagic0()));
            observer.parsed(bytes, 4, 2,
                            "minor_version: " + Hex.u2(getMinorVersion0()));
            observer.parsed(bytes, 6, 2,
                            "major_version: " + Hex.u2(getMajorVersion0()));
        }

        if (strictParse) {
            /* Make sure that this looks like a valid class file with a
             * version that we can handle.
             */
            if (!isGoodVersion(getMagic0(), getMinorVersion0(),
                               getMajorVersion0())) {
                throw new ParseException("bad class file magic (" +
                                         Hex.u4(getMagic0()) +
                                         ") or version (" +
                                         Hex.u2(getMajorVersion0()) + "." +
                                         Hex.u2(getMinorVersion0()) + ")");
            }
        }

        ConstantPoolParser cpParser = new ConstantPoolParser(bytes);
        cpParser.setObserver(observer);
        pool = cpParser.getPool();
        pool.setImmutable();

        int at = cpParser.getEndOffset();
        int accessFlags = bytes.getUnsignedShort(at); // u2 access_flags;
        int cpi = bytes.getUnsignedShort(at + 2); // u2 this_class;
        thisClass = (CstType) pool.get(cpi);
        cpi = bytes.getUnsignedShort(at + 4); // u2 super_class;
        superClass = (CstType) pool.get0Ok(cpi);
        int count = bytes.getUnsignedShort(at + 6); // u2 interfaces_count

        if (observer != null) {
            observer.parsed(bytes, at, 2,
                            "access_flags: " + 
                            AccessFlags.classString(accessFlags));
            observer.parsed(bytes, at + 2, 2, "this_class: " + thisClass);
            observer.parsed(bytes, at + 4, 2, "super_class: " +
                            stringOrNone(superClass));
            observer.parsed(bytes, at + 6, 2,
                            "interfaces_count: " + Hex.u2(count));
            if (count != 0) {
                observer.parsed(bytes, at + 8, 0, "interfaces:");
            }
        }

        at += 8;
        interfaces = makeTypeList(at, count);
        at += count * 2;

        if (strictParse) {
            /*
             * Make sure that the file/jar path matches the declared
             * package/class name.
             */
            String thisClassName = thisClass.getClassType().getClassName();
            if (!(filePath.endsWith(".class") &&
                  filePath.startsWith(thisClassName) &&
                  (filePath.length() == (thisClassName.length() + 6)))) {
                throw new ParseException("class name (" + thisClassName +
                                         ") does not match path (" +
                                         filePath + ")");
            }
        }

        /*
         * Only set the instance variable accessFlags here, since
         * that's what signals a successful parse of the first part of
         * the file (through the interfaces list).
         */
        this.accessFlags = accessFlags;

        FieldListParser flParser =
            new FieldListParser(this, thisClass, at, attributeFactory);
        flParser.setObserver(observer);
        fields = flParser.getList();
        at = flParser.getEndOffset();

        MethodListParser mlParser =
            new MethodListParser(this, thisClass, at, attributeFactory);
        mlParser.setObserver(observer);
        methods = mlParser.getList();
        at = mlParser.getEndOffset();

        AttributeListParser alParser =
            new AttributeListParser(this, AttributeFactory.CTX_CLASS, at,
                                    attributeFactory);
        alParser.setObserver(observer);
        attributes = alParser.getList();
        attributes.setImmutable();
        at = alParser.getEndOffset();

        if (at != bytes.size()) {
            throw new ParseException("extra bytes at end of class file, " +
                                     "at offset " + Hex.u4(at));
        }

        if (observer != null) {
            observer.parsed(bytes, at, 0, "end classfile");
        }
    }

    /**
     * Implementation of {@link TypeList} whose data comes directly
     * from the bytes of an instance of this (outer) class,
     * interpreted as a list of constant pool indices for classes
     * which are in turn returned as type constants. Instance
     * construction will fail if any of the (alleged) indices turn out
     * not to refer to constant pool entries of type
     * <code>Class</code>.
     */
    private static class DcfTypeList implements TypeList {
        /** non-null; array containing the data */
        private final ByteArray bytes;
        
        /** number of elements in the list (not number of bytes) */
        private final int size;

        /** non-null; the constant pool */
        private final StdConstantPool pool;

        /**
         * Constructs an instance.
         * 
         * @param bytes non-null; original classfile's bytes
         * @param offset offset into {@link #bytes} for the start of the
         * data
         * @param size number of elements in the list (not number of bytes)
         * @param pool non-null; the constant pool to use
         * @param observer null-ok; parse observer to use, if any
         */
        public DcfTypeList(ByteArray bytes, int offset, int size,
                StdConstantPool pool, ParseObserver observer) {
            if (size < 0) {
                throw new IllegalArgumentException("size < 0");
            }

            bytes = bytes.slice(offset, offset + size * 2);
            this.bytes = bytes;
            this.size = size;
            this.pool = pool;

            for (int i = 0; i < size; i++) {
                offset = i * 2;
                int idx = bytes.getUnsignedShort(offset);
                CstType type;
                try {
                    type = (CstType) pool.get(idx);
                } catch (ClassCastException ex) {
                    // Translate the exception.
                    throw new RuntimeException("bogus class cpi", ex);
                }
                if (observer != null) {
                    observer.parsed(bytes, offset, 2, "  " + type);
                }
            }
        }

        /** {@inheritDoc} */
        public boolean isMutable() {
            return false;
        }

        /** {@inheritDoc} */
        public int size() {
            return size;
        }

        /** {@inheritDoc} */
        public int getWordCount() {
            // It is the same as size because all elements are classes.
            return size;
        }

        /** {@inheritDoc} */
        public Type getType(int n) {
            int idx = bytes.getUnsignedShort(n * 2);
            return ((CstType) pool.get(idx)).getClassType();
        }

        /** {@inheritDoc} */
        public TypeList withAddedType(Type type) {
            throw new UnsupportedOperationException("unsupported");
        }        
    }
}
