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

import com.android.annotations.NonNull;

import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.text.ParseException;

/**
 * Describes the types and locations of objects in a segment of a heap.
 */
public final class HeapSegment implements Comparable<HeapSegment> {

    /**
     * Describes an object/region encoded in the HPSG data.
     */
    public static class HeapSegmentElement implements Comparable<HeapSegmentElement> {

        /*
         * Solidity values, which must match the values in
         * the HPSG data.
         */

        /** The element describes a free block. */
        public static final int SOLIDITY_FREE = 0;

        /** The element is strongly-reachable. */
        public static final int SOLIDITY_HARD = 1;

        /** The element is softly-reachable. */
        public static final int SOLIDITY_SOFT = 2;

        /** The element is weakly-reachable. */
        public static final int SOLIDITY_WEAK = 3;

        /** The element is phantom-reachable. */
        public static final int SOLIDITY_PHANTOM = 4;

        /** The element is pending finalization. */
        public static final int SOLIDITY_FINALIZABLE = 5;

        /** The element is not reachable, and is about to be swept/freed. */
        public static final int SOLIDITY_SWEEP = 6;

        /** The reachability of the object is unknown. */
        public static final int SOLIDITY_INVALID = -1;


        /*
         * Kind values, which must match the values in
         * the HPSG data.
         */

        /** The element describes a data object. */
        public static final int KIND_OBJECT = 0;

        /** The element describes a class object. */
        public static final int KIND_CLASS_OBJECT = 1;

        /** The element describes an array of 1-byte elements. */
        public static final int KIND_ARRAY_1 = 2;

        /** The element describes an array of 2-byte elements. */
        public static final int KIND_ARRAY_2 = 3;

        /** The element describes an array of 4-byte elements. */
        public static final int KIND_ARRAY_4 = 4;

        /** The element describes an array of 8-byte elements. */
        public static final int KIND_ARRAY_8 = 5;

        /** The element describes an unknown type of object. */
        public static final int KIND_UNKNOWN = 6;

        /** The element describes a native object. */
        public static final int KIND_NATIVE = 7;

        /** The object kind is unknown or unspecified. */
        public static final int KIND_INVALID = -1;


        /**
         * A bit in the HPSG data that indicates that an element should
         * be combined with the element that follows, typically because
         * an element is too large to be described by a single element.
         */
        private static final int PARTIAL_MASK = 1 << 7;


        /**
         * Describes the reachability/solidity of the element.  Must
         * be set to one of the SOLIDITY_* values.
         */
        private int mSolidity;

        /**
         * Describes the type/kind of the element.  Must be set to one
         * of the KIND_* values.
         */
        private int mKind;

        /**
         * Describes the length of the element, in bytes.
         */
        private int mLength;


        /**
         * Creates an uninitialized element.
         */
        public HeapSegmentElement() {
            setSolidity(SOLIDITY_INVALID);
            setKind(KIND_INVALID);
            setLength(-1);
        }

        /**
         * Create an element describing the entry at the current
         * position of hpsgData.
         *
         * @param hs The heap segment to pull the entry from.
         * @throws BufferUnderflowException if there is not a whole entry
         *                                  following the current position
         *                                  of hpsgData.
         * @throws ParseException           if the provided data is malformed.
         */
        public HeapSegmentElement(HeapSegment hs)
                throws BufferUnderflowException, ParseException {
            set(hs);
        }

        /**
         * Replace the element with the entry at the current position of
         * hpsgData.
         *
         * @param hs The heap segment to pull the entry from.
         * @return this object.
         * @throws BufferUnderflowException if there is not a whole entry
         *                                  following the current position of
         *                                  hpsgData.
         * @throws ParseException           if the provided data is malformed.
         */
        public HeapSegmentElement set(HeapSegment hs)
                throws BufferUnderflowException, ParseException {

            /* TODO: Maybe keep track of the virtual address of each element
             *       so that they can be examined independently.
             */
            ByteBuffer data = hs.mUsageData;
            int eState = data.get() & 0x000000ff;
            int eLen = (data.get() & 0x000000ff) + 1;

            while ((eState & PARTIAL_MASK) != 0) {

                /* If the partial bit was set, the next byte should describe
                 * the same object as the current one.
                 */
                int nextState = data.get() & 0x000000ff;
                if ((nextState & ~PARTIAL_MASK) != (eState & ~PARTIAL_MASK)) {
                    throw new ParseException("State mismatch", data.position());
                }
                eState = nextState;
                eLen += (data.get() & 0x000000ff) + 1;
            }

            setSolidity(eState & 0x7);
            setKind((eState >> 3) & 0x7);
            setLength(eLen * hs.mAllocationUnitSize);

            return this;
        }

        public int getSolidity() {
            return mSolidity;
        }

        public void setSolidity(int solidity) {
            this.mSolidity = solidity;
        }

        public int getKind() {
            return mKind;
        }

        public void setKind(int kind) {
            this.mKind = kind;
        }

        public int getLength() {
            return mLength;
        }

        public void setLength(int length) {
            this.mLength = length;
        }

        @Override
        public int compareTo(@NonNull HeapSegmentElement other) {
            if (mLength != other.mLength) {
                return mLength < other.mLength ? -1 : 1;
            }
            return 0;
        }
    }

    //* The ID of the heap that this segment belongs to.
    protected int mHeapId;

    //* The size of an allocation unit, in bytes. (e.g., 8 bytes)
    protected int mAllocationUnitSize;

    //* The virtual address of the start of this segment.
    protected long mStartAddress;

    //* The offset of this pices from mStartAddress, in bytes.
    protected int mOffset;

    //* The number of allocation units described in this segment.
    protected int mAllocationUnitCount;

    //* The raw data that describes the contents of this segment.
    protected ByteBuffer mUsageData;

    //* mStartAddress is set to this value when the segment becomes invalid.
    private static final long INVALID_START_ADDRESS = -1;

    /**
     * Create a new HeapSegment based on the raw contents
     * of an HPSG chunk.
     *
     * @param hpsgData The raw data from an HPSG chunk.
     * @throws BufferUnderflowException if hpsgData is too small
     *                                  to hold the HPSG chunk header data.
     */
    public HeapSegment(ByteBuffer hpsgData) throws BufferUnderflowException {
        /* Read the HPSG chunk header.
         * These get*() calls may throw a BufferUnderflowException
         * if the underlying data isn't big enough.
         */
        hpsgData.order(ByteOrder.BIG_ENDIAN);
        mHeapId = hpsgData.getInt();
        mAllocationUnitSize = hpsgData.get();
        mStartAddress = hpsgData.getInt() & 0x00000000ffffffffL;
        mOffset = hpsgData.getInt();
        mAllocationUnitCount = hpsgData.getInt();

        // Hold onto the remainder of the data.
        mUsageData = hpsgData.slice();
        mUsageData.order(ByteOrder.BIG_ENDIAN);   // doesn't actually matter

        // Validate the data.
//xxx do it
//xxx make sure the number of elements matches mAllocationUnitCount.
//xxx make sure the last element doesn't have P set
    }

    /**
     * See if this segment still contains data, and has not been
     * appended to another segment.
     *
     * @return true if this segment has not been appended to
     *         another segment.
     */
    public boolean isValid() {
        return mStartAddress != INVALID_START_ADDRESS;
    }

    /**
     * See if <code>other</code> comes immediately after this segment.
     *
     * @param other The HeapSegment to check.
     * @return true if <code>other</code> comes immediately after this
     *         segment.
     */
    public boolean canAppend(HeapSegment other) {
        return isValid() && other.isValid() && mHeapId == other.mHeapId &&
                mAllocationUnitSize == other.mAllocationUnitSize &&
                getEndAddress() == other.getStartAddress();
    }

    /**
     * Append the contents of <code>other</code> to this segment
     * if it describes the segment immediately after this one.
     *
     * @param other The segment to append to this segment, if possible.
     *              If appended, <code>other</code> will be invalid
     *              when this method returns.
     * @return true if <code>other</code> was successfully appended to
     *         this segment.
     */
    public boolean append(HeapSegment other) {
        if (canAppend(other)) {
            /* Preserve the position.  The mark is not preserved,
             * but we don't use it anyway.
             */
            int pos = mUsageData.position();

            // Guarantee that we have enough room for the new data.
            if (mUsageData.capacity() - mUsageData.limit() <
                    other.mUsageData.limit()) {
                /* Grow more than necessary in case another append()
                 * is about to happen.
                 */
                int newSize = mUsageData.limit() + other.mUsageData.limit();
                ByteBuffer newData = ByteBuffer.allocate(newSize * 2);

                mUsageData.rewind();
                newData.put(mUsageData);
                mUsageData = newData;
            }

            // Copy the data from the other segment and restore the position.
            other.mUsageData.rewind();
            mUsageData.put(other.mUsageData);
            mUsageData.position(pos);

            // Fix this segment's header to cover the new data.
            mAllocationUnitCount += other.mAllocationUnitCount;

            // Mark the other segment as invalid.
            other.mStartAddress = INVALID_START_ADDRESS;
            other.mUsageData = null;

            return true;
        } else {
            return false;
        }
    }

    public long getStartAddress() {
        return mStartAddress + mOffset;
    }

    public int getLength() {
        return mAllocationUnitSize * mAllocationUnitCount;
    }

    public long getEndAddress() {
        return getStartAddress() + getLength();
    }

    public void rewindElements() {
        if (mUsageData != null) {
            mUsageData.rewind();
        }
    }

    public HeapSegmentElement getNextElement(HeapSegmentElement reuse) {
        try {
            if (reuse != null) {
                return reuse.set(this);
            } else {
                return new HeapSegmentElement(this);
            }
        } catch (BufferUnderflowException ex) {
            /* Normal "end of buffer" situation.
             */
        } catch (ParseException ex) {
            /* Malformed data.
             */
//TODO: we should catch this in the constructor
        }
        return null;
    }

    /*
     * Method overrides for Comparable
     */
    @Override
    public boolean equals(Object o) {
        if (o instanceof HeapSegment) {
            return compareTo((HeapSegment) o) == 0;
        }
        return false;
    }

    @Override
    public int hashCode() {
        return mHeapId * 31 +
                mAllocationUnitSize * 31 +
                (int) mStartAddress * 31 +
                mOffset * 31 +
                mAllocationUnitCount * 31 +
                mUsageData.hashCode();
    }

    @Override
    public String toString() {
        StringBuilder str = new StringBuilder();

        str.append("HeapSegment { heap ").append(mHeapId)
                .append(", start 0x")
                .append(Integer.toHexString((int) getStartAddress()))
                .append(", length ").append(getLength())
                .append(" }");

        return str.toString();
    }

    @Override
    public int compareTo(@NonNull HeapSegment other) {
        if (mHeapId != other.mHeapId) {
            return mHeapId < other.mHeapId ? -1 : 1;
        }
        if (getStartAddress() != other.getStartAddress()) {
            return getStartAddress() < other.getStartAddress() ? -1 : 1;
        }

        /* If two segments have the same start address, the rest of
         * the fields should be equal.  Go through the motions, though.
         * Note that we re-check the components of getStartAddress()
         * (mStartAddress and mOffset) to make sure that all fields in
         * an equal segment are equal.
         */

        if (mAllocationUnitSize != other.mAllocationUnitSize) {
            return mAllocationUnitSize < other.mAllocationUnitSize ? -1 : 1;
        }
        if (mStartAddress != other.mStartAddress) {
            return mStartAddress < other.mStartAddress ? -1 : 1;
        }
        if (mOffset != other.mOffset) {
            return mOffset < other.mOffset ? -1 : 1;
        }
        if (mAllocationUnitCount != other.mAllocationUnitCount) {
            return mAllocationUnitCount < other.mAllocationUnitCount ? -1 : 1;
        }
        if (mUsageData != other.mUsageData) {
            return mUsageData.compareTo(other.mUsageData);
        }
        return 0;
    }
}
