/*
 * Copyright (C) 2013 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.internal.app.procstats;

import android.os.Build;
import android.os.Parcel;
import android.util.Slog;
import libcore.util.EmptyArray;

import java.util.ArrayList;
import java.util.Arrays;

import com.android.internal.util.GrowingArrayUtils;

/**
 * Class that contains a set of tables mapping byte ids to long values.
 *
 * This class is used to store the ProcessStats data.  This data happens to be
 * a set of very sparse tables, that is mostly append or overwrite, with infrequent
 * resets of the data.
 *
 * Data is stored as a list of large long[] arrays containing the actual values.  There are a
 * set of Table objects that each contain a small array mapping the byte IDs to a position
 * in the larger arrays.
 *
 * The data itself is either a single long value or a range of long values which are always
 * stored continguously in one of the long arrays. When the caller allocates a slot with
 * getOrAddKey, an int key is returned.  That key can be re-retreived with getKey without
 * allocating the value.  The data can then be set or retrieved with that key.
 */
public class SparseMappingTable {
    private static final String TAG = "SparseMappingTable";

    // How big each array is.
    public static final int ARRAY_SIZE = 4096;

    public static final int INVALID_KEY = 0xffffffff;

    // Where the "type"/"state" part of the data appears in an offset integer.
    private static final int ID_SHIFT = 0;
    private static final int ID_MASK = 0xff;
    // Where the "which array" part of the data appears in an offset integer.
    private static final int ARRAY_SHIFT = 8;
    private static final int ARRAY_MASK = 0xff;
    // Where the "index into array" part of the data appears in an offset integer.
    private static final int INDEX_SHIFT = 16;
    private static final int INDEX_MASK = 0xffff;

    private int mSequence;
    private int mNextIndex;
    private final ArrayList<long[]> mLongs = new ArrayList<long[]>();

    /**
     * A table of data as stored in a SparseMappingTable.
     */
    public static class Table {
        private SparseMappingTable mParent;
        private int mSequence = 1;
        private int[] mTable;
        private int mSize;

        public Table(SparseMappingTable parent) {
            mParent = parent;
            mSequence = parent.mSequence;
        }

        /**
         * Pulls the data from 'copyFrom' and stores it in our own longs table.
         *
         * @param copyFrom   The Table to copy from
         * @param valueCount The number of values to copy for each key
         */
        public void copyFrom(Table copyFrom, int valueCount) {
            mTable = null;
            mSize = 0;

            final int N = copyFrom.getKeyCount();
            for (int i=0; i<N; i++) {
                final int theirKey = copyFrom.getKeyAt(i);
                final long[] theirLongs = copyFrom.mParent.mLongs.get(getArrayFromKey(theirKey));

                final byte id = SparseMappingTable.getIdFromKey(theirKey);

                final int myKey = this.getOrAddKey((byte)id, valueCount);
                final long[] myLongs = mParent.mLongs.get(getArrayFromKey(myKey));

                System.arraycopy(theirLongs, getIndexFromKey(theirKey),
                        myLongs, getIndexFromKey(myKey), valueCount);
            }
        }

        /**
         * Allocates data in the buffer, and stores that key in the mapping for this
         * table.
         *
         * @param id    The id of the item (will be used in making the key)
         * @param count The number of bytes to allocate.  Must be less than
         *              SparseMappingTable.ARRAY_SIZE.
         *
         * @return The 'key' for this data value, which contains both the id itself
         *         and the location in the long arrays that the data is actually stored
         *         but should be considered opaque to the caller.
         */
        public int getOrAddKey(byte id, int count) {
            assertConsistency();

            final int idx = binarySearch(id);
            if (idx >= 0) {
                // Found
                return mTable[idx];
            } else {
                // Not found. Need to allocate it.

                // Get an array with enough space to store 'count' values.
                final ArrayList<long[]> list = mParent.mLongs;
                int whichArray = list.size()-1;
                long[] array = list.get(whichArray);
                if (mParent.mNextIndex + count > array.length) {
                    // if it won't fit then make a new array.
                    array = new long[ARRAY_SIZE];
                    list.add(array);
                    whichArray++;
                    mParent.mNextIndex = 0;
                }

                // The key is a combination of whichArray, which index in that array, and
                // the table value itself, which will be used for lookup
                final int key = (whichArray << ARRAY_SHIFT)
                        | (mParent.mNextIndex << INDEX_SHIFT)
                        | (((int)id) << ID_SHIFT);

                mParent.mNextIndex += count;

                // Store the key in the sparse lookup table for this Table object.
                mTable = GrowingArrayUtils.insert(mTable != null ? mTable : EmptyArray.INT,
                        mSize, ~idx, key);
                mSize++;

                return key;
            }
        }

        /**
         * Looks up a key in the table.
         *
         * @return The key from this table or INVALID_KEY if the id is not found.
         */
        public int getKey(byte id) {
            assertConsistency();

            final int idx = binarySearch(id);
            if (idx >= 0) {
                return mTable[idx];
            } else {
                return INVALID_KEY;
            }
        }

        /**
         * Get the value for the given key and offset from that key.
         *
         * @param key   A key as obtained from getKey or getOrAddKey.
         * @param value The value to set.
         */
        public long getValue(int key) {
            return getValue(key, 0);
        }

        /**
         * Get the value for the given key and offset from that key.
         *
         * @param key   A key as obtained from getKey or getOrAddKey.
         * @param index The offset from that key.  Must be less than the count
         *              provided to getOrAddKey when the space was allocated.
         * @param value The value to set.
         *
         * @return the value, or 0 in case of an error
         */
        public long getValue(int key, int index) {
            assertConsistency();

            try {
                final long[] array = mParent.mLongs.get(getArrayFromKey(key));
                return array[getIndexFromKey(key) + index];
            } catch (IndexOutOfBoundsException ex) {
                logOrThrow("key=0x" + Integer.toHexString(key)
                        + " index=" + index + " -- " + dumpInternalState(), ex);
                return 0;
            }
        }

        /**
         * Set the value for the given id at offset 0 from that id.
         * If the id is not found, return 0 instead.
         *
         * @param id    The id of the item.
         */
        public long getValueForId(byte id) {
            return getValueForId(id, 0);
        }

        /**
         * Set the value for the given id and index offset from that id.
         * If the id is not found, return 0 instead.
         *
         * @param id    The id of the item.
         * @param index The offset from that key.  Must be less than the count
         *              provided to getOrAddKey when the space was allocated.
         */
        public long getValueForId(byte id, int index) {
            assertConsistency();

            final int idx = binarySearch(id);
            if (idx >= 0) {
                final int key = mTable[idx];
                try {
                    final long[] array = mParent.mLongs.get(getArrayFromKey(key));
                    return array[getIndexFromKey(key) + index];
                } catch (IndexOutOfBoundsException ex) {
                    logOrThrow("id=0x" + Integer.toHexString(id) + " idx=" + idx
                            + " key=0x" + Integer.toHexString(key) + " index=" + index
                            + " -- " + dumpInternalState(), ex);
                    return 0;
                }
            } else {
                return 0;
            }
        }

        /**
         * Return the raw storage long[] for the given key.
         */
        public long[] getArrayForKey(int key) {
            assertConsistency();

            return mParent.mLongs.get(getArrayFromKey(key));
        }

        /**
         * Set the value for the given key and offset from that key.
         *
         * @param key   A key as obtained from getKey or getOrAddKey.
         * @param value The value to set.
         */
        public void setValue(int key, long value) {
            setValue(key, 0, value);
        }

        /**
         * Set the value for the given key and offset from that key.
         *
         * @param key   A key as obtained from getKey or getOrAddKey.
         * @param index The offset from that key.  Must be less than the count
         *              provided to getOrAddKey when the space was allocated.
         * @param value The value to set.
         */
        public void setValue(int key, int index, long value) {
            assertConsistency();

            if (value < 0) {
                logOrThrow("can't store negative values"
                        + " key=0x" + Integer.toHexString(key)
                        + " index=" + index + " value=" + value
                        + " -- " + dumpInternalState());
                return;
            }

            try {
                final long[] array = mParent.mLongs.get(getArrayFromKey(key));
                array[getIndexFromKey(key) + index] = value;
            } catch (IndexOutOfBoundsException ex) {
                logOrThrow("key=0x" + Integer.toHexString(key)
                        + " index=" + index + " value=" + value
                        + " -- " + dumpInternalState(), ex);
                return;
            }
        }

        /**
         * Clear out the table, and reset the sequence numbers so future writes
         * without allocations will assert.
         */
        public void resetTable() {
            // Clear out our table.
            mTable = null;
            mSize = 0;

            // Reset our sequence number.  This will make all read/write calls
            // start to fail, and then when we re-allocate it will be re-synced
            // to that of mParent.
            mSequence = mParent.mSequence;
        }

        /**
         * Write the keys stored in the table to the parcel. The parent must
         * be separately written. Does not save the actual data.
         */
        public void writeToParcel(Parcel out) {
            out.writeInt(mSequence);
            out.writeInt(mSize);
            for (int i=0; i<mSize; i++) {
                out.writeInt(mTable[i]);
            }
        }

        /**
         * Read the keys from the parcel. The parent (with its long array) must
         * have been previously initialized.
         */
        public boolean readFromParcel(Parcel in) {
            // Read the state
            mSequence = in.readInt();
            mSize = in.readInt();
            if (mSize != 0) {
                mTable = new int[mSize];
                for (int i=0; i<mSize; i++) {
                    mTable[i] = in.readInt();
                }
            } else {
                mTable = null;
            }

            // Make sure we're all healthy
            if (validateKeys(true)) {
                return true;
            } else {
                // Clear it out
                mSize = 0;
                mTable = null;
                return false;
            }
        }

        /**
         * Return the number of keys that have been added to this Table.
         */
        public int getKeyCount() {
            return mSize;
        }

        /**
         * Get the key at the given index in our table.
         */
        public int getKeyAt(int i) {
            return mTable[i];
        }

        /**
         * Throw an exception if one of a variety of internal consistency checks fails.
         */
        private void assertConsistency() {
            // Something with this checking isn't working and is triggering
            // more problems than it's helping to debug.
            //   Original bug: b/27045736
            //   New bug: b/27960286
            if (false) {
                // Assert that our sequence number matches mParent's.  If it isn't that means
                // we have been reset and our.  If our sequence is UNITIALIZED_SEQUENCE, then 
                // it's possible that everything is working fine and we just haven't been
                // written to since the last resetTable().
                if (mSequence != mParent.mSequence) {
                    if (mSequence < mParent.mSequence) {
                        logOrThrow("Sequence mismatch. SparseMappingTable.reset()"
                                + " called but not Table.resetTable() -- "
                                + dumpInternalState());
                        return;
                    } else if (mSequence > mParent.mSequence) {
                        logOrThrow("Sequence mismatch. Table.resetTable()"
                                + " called but not SparseMappingTable.reset() -- "
                                + dumpInternalState());
                        return;
                    }
                }
            }
        }

        /**
         * Finds the 'id' inside the array of length size (physical size of the array
         * is not used).
         *
         * @return The index of the array, or the bitwise not (~index) of where it
         * would go if you wanted to insert 'id' into the array.
         */
        private int binarySearch(byte id) {
            int lo = 0;
            int hi = mSize - 1;

            while (lo <= hi) {
                int mid = (lo + hi) >>> 1;
                byte midId = (byte)((mTable[mid] >> ID_SHIFT) & ID_MASK);

                if (midId < id) {
                    lo = mid + 1;
                } else if (midId > id) {
                    hi = mid - 1;
                } else {
                    return mid;  // id found
                }
            }
            return ~lo;  // id not present
        }

        /**
         * Check that all the keys are valid locations in the long arrays.
         *
         * If any aren't, log it and return false. Else return true.
         */
        private boolean validateKeys(boolean log) {
            ArrayList<long[]> longs = mParent.mLongs;
            final int longsSize = longs.size();

            final int N = mSize;
            for (int i=0; i<N; i++) {
                final int key = mTable[i];
                final int arrayIndex = getArrayFromKey(key);
                final int index = getIndexFromKey(key);
                if (arrayIndex >= longsSize || index >= longs.get(arrayIndex).length) {
                    if (log) {
                        Slog.w(TAG, "Invalid stats at index " + i + " -- " + dumpInternalState());
                    }
                    return false;
                }
            }

            return true;
        }

        public String dumpInternalState() {
            StringBuilder sb = new StringBuilder();
            sb.append("SparseMappingTable.Table{mSequence=");
            sb.append(mSequence);
            sb.append(" mParent.mSequence=");
            sb.append(mParent.mSequence);
            sb.append(" mParent.mLongs.size()=");
            sb.append(mParent.mLongs.size());
            sb.append(" mSize=");
            sb.append(mSize);
            sb.append(" mTable=");
            if (mTable == null) {
                sb.append("null");
            } else {
                final int N = mTable.length;
                sb.append('[');
                for (int i=0; i<N; i++) {
                    final int key = mTable[i];
                    sb.append("0x");
                    sb.append(Integer.toHexString((key >> ID_SHIFT) & ID_MASK));
                    sb.append("/0x");
                    sb.append(Integer.toHexString((key >> ARRAY_SHIFT) & ARRAY_MASK));
                    sb.append("/0x");
                    sb.append(Integer.toHexString((key >> INDEX_SHIFT) & INDEX_MASK));
                    if (i != N-1) {
                        sb.append(", ");
                    }
                }
                sb.append(']');
            }
            sb.append(" clazz=");
            sb.append(getClass().getName());
            sb.append('}');

            return sb.toString();
        }
    }

    public SparseMappingTable() {
        mLongs.add(new long[ARRAY_SIZE]);
    }

    /**
     * Wipe out all the data.
     */
    public void reset() {
        // Clear out mLongs, and prime it with a new array of data
        mLongs.clear();
        mLongs.add(new long[ARRAY_SIZE]);
        mNextIndex = 0;

        // Increment out sequence counter, because all of the tables will
        // now be out of sync with the data.
        mSequence++;
    }

    /**
     * Write the data arrays to the parcel.
     */
    public void writeToParcel(Parcel out) {
        out.writeInt(mSequence);
        out.writeInt(mNextIndex);
        final int N = mLongs.size();
        out.writeInt(N);
        for (int i=0; i<N-1; i++) {
            final long[] array = mLongs.get(i);
            out.writeInt(array.length);
            writeCompactedLongArray(out, array, array.length);
        }
        // save less for the last one. upon re-loading they'll just start a new array.
        final long[] lastLongs = mLongs.get(N-1);
        out.writeInt(mNextIndex);
        writeCompactedLongArray(out, lastLongs, mNextIndex);
    }

    /**
     * Read the data arrays from the parcel.
     */
    public void readFromParcel(Parcel in) {
        mSequence = in.readInt();
        mNextIndex = in.readInt();

        mLongs.clear();
        final int N = in.readInt();
        for (int i=0; i<N; i++) {
            final int size = in.readInt();
            final long[] array = new long[size];
            readCompactedLongArray(in, array, size);
            mLongs.add(array);
        }
    }

    /**
     * Return a string for debugging.
     */
    public String dumpInternalState(boolean includeData) {
        final StringBuilder sb = new StringBuilder();
        sb.append("SparseMappingTable{");
        sb.append("mSequence=");
        sb.append(mSequence);
        sb.append(" mNextIndex=");
        sb.append(mNextIndex);
        sb.append(" mLongs.size=");
        final int N = mLongs.size();
        sb.append(N);
        sb.append("\n");
        if (includeData) {
            for (int i=0; i<N; i++) {
                final long[] array = mLongs.get(i);
                for (int j=0; j<array.length; j++) {
                    if (i == N-1 && j == mNextIndex) {
                        break;
                    }
                    sb.append(String.format(" %4d %d 0x%016x %-19d\n", i, j, array[j], array[j]));
                }
            }
        }
        sb.append("}");
        return sb.toString();
    }

    /**
     * Write the long array to the parcel in a compacted form.  Does not allow negative
     * values in the array.
     */
    private static void writeCompactedLongArray(Parcel out, long[] array, int num) {
        for (int i=0; i<num; i++) {
            long val = array[i];
            if (val < 0) {
                Slog.w(TAG, "Time val negative: " + val);
                val = 0;
            }
            if (val <= Integer.MAX_VALUE) {
                out.writeInt((int)val);
            } else {
                int top = ~((int)((val>>32)&0x7fffffff));
                int bottom = (int)(val&0x0ffffffffL);
                out.writeInt(top);
                out.writeInt(bottom);
            }
        }
    }

    /**
     * Read the compacted array into the long[].
     */
    private static void readCompactedLongArray(Parcel in, long[] array, int num) {
        final int alen = array.length;
        if (num > alen) {
            logOrThrow("bad array lengths: got " + num + " array is " + alen);
            return;
        }
        int i;
        for (i=0; i<num; i++) {
            int val = in.readInt();
            if (val >= 0) {
                array[i] = val;
            } else {
                int bottom = in.readInt();
                array[i] = (((long)~val)<<32) | bottom;
            }
        }
        while (i < alen) {
            array[i] = 0;
            i++;
        }
    }

    /**
     * Extract the id from a key.
     */
    public static byte getIdFromKey(int key) {
        return (byte)((key >> ID_SHIFT) & ID_MASK);
    }

    /**
     * Gets the index of the array in the list of arrays.
     *
     * Not to be confused with getIndexFromKey.
     */
    public static int getArrayFromKey(int key) {
        return (key >> ARRAY_SHIFT) & ARRAY_MASK;
    }

    /**
     * Gets the index of a value in a long[].
     *
     * Not to be confused with getArrayFromKey.
     */
    public static int getIndexFromKey(int key) {
        return (key >> INDEX_SHIFT) & INDEX_MASK;
    }

    /**
     * Do a Slog.wtf or throw an exception (thereby crashing the system process if
     * this is a debug build.)
     */
    private static void logOrThrow(String message) {
        logOrThrow(message, new RuntimeException("Stack trace"));
    }

    /**
     * Do a Slog.wtf or throw an exception (thereby crashing the system process if
     * this is an eng build.)
     */
    private static void logOrThrow(String message, Throwable th) {
        Slog.e(TAG, message, th);
        if (Build.TYPE.equals("eng")) {
            throw new RuntimeException(message, th);
        }
    }
}
