/*
 * 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.dex.file;

import com.android.dx.util.AnnotatedOutput;
import com.android.dx.util.ExceptionWithContext;
import com.android.dx.util.Hex;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.TreeMap;

/**
 * A section of a {@code .dex} file which consists of a sequence of
 * {@link OffsettedItem} objects, which may each be of a different concrete
 * class and/or size.
 *
 * <b>Note:</b> It is invalid for an item in an instance of this class to
 * have a larger alignment requirement than the alignment of this instance.
 */
public final class MixedItemSection extends Section {
    static enum SortType {
        /** no sorting */
        NONE,

        /** sort by type only */
        TYPE,

        /** sort in class-major order, with instances sorted per-class */
        INSTANCE;
    };

    /** {@code non-null;} sorter which sorts instances by type */
    private static final Comparator<OffsettedItem> TYPE_SORTER =
        new Comparator<OffsettedItem>() {
        public int compare(OffsettedItem item1, OffsettedItem item2) {
            ItemType type1 = item1.itemType();
            ItemType type2 = item2.itemType();
            return type1.compareTo(type2);
        }
    };

    /** {@code non-null;} the items in this part */
    private final ArrayList<OffsettedItem> items;

    /** {@code non-null;} items that have been explicitly interned */
    private final HashMap<OffsettedItem, OffsettedItem> interns;

    /** {@code non-null;} how to sort the items */
    private final SortType sort;

    /**
     * {@code >= -1;} the current size of this part, in bytes, or {@code -1}
     * if not yet calculated
     */
    private int writeSize;

    /**
     * Constructs an instance. The file offset is initially unknown.
     *
     * @param name {@code null-ok;} the name of this instance, for annotation
     * purposes
     * @param file {@code non-null;} file that this instance is part of
     * @param alignment {@code > 0;} alignment requirement for the final output;
     * must be a power of 2
     * @param sort how the items should be sorted in the final output
     */
    public MixedItemSection(String name, DexFile file, int alignment,
            SortType sort) {
        super(name, file, alignment);

        this.items = new ArrayList<OffsettedItem>(100);
        this.interns = new HashMap<OffsettedItem, OffsettedItem>(100);
        this.sort = sort;
        this.writeSize = -1;
    }

    /** {@inheritDoc} */
    @Override
    public Collection<? extends Item> items() {
        return items;
    }

    /** {@inheritDoc} */
    @Override
    public int writeSize() {
        throwIfNotPrepared();
        return writeSize;
    }

    /** {@inheritDoc} */
    @Override
    public int getAbsoluteItemOffset(Item item) {
        OffsettedItem oi = (OffsettedItem) item;
        return oi.getAbsoluteOffset();
    }

    /**
     * Gets the size of this instance, in items.
     *
     * @return {@code >= 0;} the size
     */
    public int size() {
        return items.size();
    }

    /**
     * Writes the portion of the file header that refers to this instance.
     *
     * @param out {@code non-null;} where to write
     */
    public void writeHeaderPart(AnnotatedOutput out) {
        throwIfNotPrepared();

        if (writeSize == -1) {
            throw new RuntimeException("write size not yet set");
        }

        int sz = writeSize;
        int offset = (sz == 0) ? 0 : getFileOffset();
        String name = getName();

        if (name == null) {
            name = "<unnamed>";
        }

        int spaceCount = 15 - name.length();
        char[] spaceArr = new char[spaceCount];
        Arrays.fill(spaceArr, ' ');
        String spaces = new String(spaceArr);

        if (out.annotates()) {
            out.annotate(4, name + "_size:" + spaces + Hex.u4(sz));
            out.annotate(4, name + "_off: " + spaces + Hex.u4(offset));
        }

        out.writeInt(sz);
        out.writeInt(offset);
    }

    /**
     * Adds an item to this instance. This will in turn tell the given item
     * that it has been added to this instance. It is invalid to add the
     * same item to more than one instance, nor to add the same items
     * multiple times to a single instance.
     *
     * @param item {@code non-null;} the item to add
     */
    public void add(OffsettedItem item) {
        throwIfPrepared();

        try {
            if (item.getAlignment() > getAlignment()) {
                throw new IllegalArgumentException(
                        "incompatible item alignment");
            }
        } catch (NullPointerException ex) {
            // Elucidate the exception.
            throw new NullPointerException("item == null");
        }

        items.add(item);
    }

    /**
     * Interns an item in this instance, returning the interned instance
     * (which may not be the one passed in). This will add the item if no
     * equal item has been added.
     *
     * @param item {@code non-null;} the item to intern
     * @return {@code non-null;} the equivalent interned instance
     */
    public <T extends OffsettedItem> T intern(T item) {
        throwIfPrepared();

        OffsettedItem result = interns.get(item);

        if (result != null) {
            return (T) result;
        }

        add(item);
        interns.put(item, item);
        return item;
    }

    /**
     * Gets an item which was previously interned.
     *
     * @param item {@code non-null;} the item to look for
     * @return {@code non-null;} the equivalent already-interned instance
     */
    public <T extends OffsettedItem> T get(T item) {
        throwIfNotPrepared();

        OffsettedItem result = interns.get(item);

        if (result != null) {
            return (T) result;
        }

        throw new NoSuchElementException(item.toString());
    }

    /**
     * Writes an index of contents of the items in this instance of the
     * given type. If there are none, this writes nothing. If there are any,
     * then the index is preceded by the given intro string.
     *
     * @param out {@code non-null;} where to write to
     * @param itemType {@code non-null;} the item type of interest
     * @param intro {@code non-null;} the introductory string for non-empty indices
     */
    public void writeIndexAnnotation(AnnotatedOutput out, ItemType itemType,
            String intro) {
        throwIfNotPrepared();

        TreeMap<String, OffsettedItem> index =
            new TreeMap<String, OffsettedItem>();

        for (OffsettedItem item : items) {
            if (item.itemType() == itemType) {
                String label = item.toHuman();
                index.put(label, item);
            }
        }

        if (index.size() == 0) {
            return;
        }

        out.annotate(0, intro);

        for (Map.Entry<String, OffsettedItem> entry : index.entrySet()) {
            String label = entry.getKey();
            OffsettedItem item = entry.getValue();
            out.annotate(0, item.offsetString() + ' ' + label + '\n');
        }
    }

    /** {@inheritDoc} */
    @Override
    protected void prepare0() {
        DexFile file = getFile();

        /*
         * It's okay for new items to be added as a result of an
         * addContents() call; we just have to deal with the possibility.
         */

        int i = 0;
        for (;;) {
            int sz = items.size();
            if (i >= sz) {
                break;
            }

            for (/*i*/; i < sz; i++) {
                OffsettedItem one = items.get(i);
                one.addContents(file);
            }
        }
    }

    /**
     * Places all the items in this instance at particular offsets. This
     * will call {@link OffsettedItem#place} on each item. If an item
     * does not know its write size before the call to {@code place},
     * it is that call which is responsible for setting the write size.
     * This method may only be called once per instance; subsequent calls
     * will throw an exception.
     */
    public void placeItems() {
        throwIfNotPrepared();

        switch (sort) {
            case INSTANCE: {
                Collections.sort(items);
                break;
            }
            case TYPE: {
                Collections.sort(items, TYPE_SORTER);
                break;
            }
        }

        int sz = items.size();
        int outAt = 0;
        for (int i = 0; i < sz; i++) {
            OffsettedItem one = items.get(i);
            try {
                int placedAt = one.place(this, outAt);

                if (placedAt < outAt) {
                    throw new RuntimeException("bogus place() result for " +
                            one);
                }

                outAt = placedAt + one.writeSize();
            } catch (RuntimeException ex) {
                throw ExceptionWithContext.withContext(ex,
                        "...while placing " + one);
            }
        }

        writeSize = outAt;
    }

    /** {@inheritDoc} */
    @Override
    protected void writeTo0(AnnotatedOutput out) {
        boolean annotates = out.annotates();
        boolean first = true;
        DexFile file = getFile();
        int at = 0;

        for (OffsettedItem one : items) {
            if (annotates) {
                if (first) {
                    first = false;
                } else {
                    out.annotate(0, "\n");
                }
            }

            int alignMask = one.getAlignment() - 1;
            int writeAt = (at + alignMask) & ~alignMask;

            if (at != writeAt) {
                out.writeZeroes(writeAt - at);
                at = writeAt;
            }

            one.writeTo(file, out);
            at += one.writeSize();
        }

        if (at != writeSize) {
            throw new RuntimeException("output size mismatch");
        }
    }
}
