/*
 * 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 android.database.sqlite;

import android.annotation.NonNull;
import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Build;
import android.os.Process;
import android.os.SystemProperties;
import android.util.Log;
import android.util.Printer;
import java.util.ArrayList;

/**
 * Provides debugging info about all SQLite databases running in the current process.
 *
 * {@hide}
 */
@TestApi
public final class SQLiteDebug {
    private static native void nativeGetPagerStats(PagerStats stats);

    /**
     * Inner class to avoid getting the value frozen in zygote.
     *
     * {@hide}
     */
    public static final class NoPreloadHolder {
        /**
         * Controls the printing of informational SQL log messages.
         *
         * Enable using "adb shell setprop log.tag.SQLiteLog VERBOSE".
         */
        public static final boolean DEBUG_SQL_LOG =
                Log.isLoggable("SQLiteLog", Log.VERBOSE);

        /**
         * Controls the printing of SQL statements as they are executed.
         *
         * Enable using "adb shell setprop log.tag.SQLiteStatements VERBOSE".
         */
        public static final boolean DEBUG_SQL_STATEMENTS =
                Log.isLoggable("SQLiteStatements", Log.VERBOSE);

        /**
         * Controls the printing of wall-clock time taken to execute SQL statements
         * as they are executed.
         *
         * Enable using "adb shell setprop log.tag.SQLiteTime VERBOSE".
         */
        public static final boolean DEBUG_SQL_TIME =
                Log.isLoggable("SQLiteTime", Log.VERBOSE);


        /**
         * True to enable database performance testing instrumentation.
         */
        public static final boolean DEBUG_LOG_SLOW_QUERIES =
                Log.isLoggable("SQLiteSlowQueries", Log.VERBOSE);

        private static final String SLOW_QUERY_THRESHOLD_PROP = "db.log.slow_query_threshold";

        private static final String SLOW_QUERY_THRESHOLD_UID_PROP =
                SLOW_QUERY_THRESHOLD_PROP + "." + Process.myUid();

        /**
         * Whether to add detailed information to slow query log.
         */
        public static final boolean DEBUG_LOG_DETAILED = Build.IS_DEBUGGABLE
                && SystemProperties.getBoolean("db.log.detailed", false);
    }

    private SQLiteDebug() {
    }

    /**
     * Determines whether a query should be logged.
     *
     * Reads the "db.log.slow_query_threshold" system property, which can be changed
     * by the user at any time.  If the value is zero, then all queries will
     * be considered slow.  If the value does not exist or is negative, then no queries will
     * be considered slow.
     *
     * To enable it for a specific UID, "db.log.slow_query_threshold.UID" could also be used.
     *
     * This value can be changed dynamically while the system is running.
     * For example, "adb shell setprop db.log.slow_query_threshold 200" will
     * log all queries that take 200ms or longer to run.
     * @hide
     */
    public static boolean shouldLogSlowQuery(long elapsedTimeMillis) {
        final int slowQueryMillis = Math.min(
                SystemProperties.getInt(NoPreloadHolder.SLOW_QUERY_THRESHOLD_PROP,
                        Integer.MAX_VALUE),
                SystemProperties.getInt(NoPreloadHolder.SLOW_QUERY_THRESHOLD_UID_PROP,
                        Integer.MAX_VALUE));
        return elapsedTimeMillis >= slowQueryMillis;
    }

    /**
     * Contains statistics about the active pagers in the current process.
     *
     * @see #nativeGetPagerStats(PagerStats)
     */
    public static class PagerStats {

        @UnsupportedAppUsage
        public PagerStats() {
        }

        /** the current amount of memory checked out by sqlite using sqlite3_malloc().
         * documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html
         */
        @UnsupportedAppUsage
        public int memoryUsed;

        /** the number of bytes of page cache allocation which could not be sattisfied by the
         * SQLITE_CONFIG_PAGECACHE buffer and where forced to overflow to sqlite3_malloc().
         * The returned value includes allocations that overflowed because they where too large
         * (they were larger than the "sz" parameter to SQLITE_CONFIG_PAGECACHE) and allocations
         * that overflowed because no space was left in the page cache.
         * documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html
         */
        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
        public int pageCacheOverflow;

        /** records the largest memory allocation request handed to sqlite3.
         * documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html
         */
        @UnsupportedAppUsage
        public int largestMemAlloc;

        /** a list of {@link DbStats} - one for each main database opened by the applications
         * running on the android device
         */
        @UnsupportedAppUsage
        public ArrayList<DbStats> dbStats;
    }

    /**
     * contains statistics about a database
     */
    public static class DbStats {
        /** name of the database */
        @UnsupportedAppUsage
        public String dbName;

        /** the page size for the database */
        @UnsupportedAppUsage
        public long pageSize;

        /** the database size */
        @UnsupportedAppUsage
        public long dbSize;

        /**
         * Number of lookaside slots: http://www.sqlite.org/c3ref/c_dbstatus_lookaside_used.html */
        @UnsupportedAppUsage
        public int lookaside;

        /** @hide */
        final public int cacheHits;
        /** @hide */
        final public int cacheMisses;
        /** @hide */
        final public int cacheSize;

        /** true if connection specific stats or whole connection pool if false */
        public final boolean arePoolStats;

        public DbStats(@NonNull String dbName, long pageCount, long pageSize, int lookaside,
                int hits, int misses, int cachesize, boolean arePoolStats) {
            this.dbName = dbName;
            this.pageSize = pageSize / 1024;
            dbSize = (pageCount * pageSize) / 1024;
            this.lookaside = lookaside;
            this.cacheHits = hits;
            this.cacheMisses = misses;
            this.cacheSize = cachesize;
            this.arePoolStats = arePoolStats;
        }
    }

    /**
     * return all pager and database stats for the current process.
     * @return {@link PagerStats}
     */
    @UnsupportedAppUsage
    public static PagerStats getDatabaseInfo() {
        PagerStats stats = new PagerStats();
        nativeGetPagerStats(stats);
        stats.dbStats = SQLiteDatabase.getDbStats();
        return stats;
    }

    /**
     * Dumps detailed information about all databases used by the process.
     * @param printer The printer for dumping database state.
     * @param args Command-line arguments supplied to dumpsys dbinfo
     */
    public static void dump(Printer printer, String[] args) {
        dump(printer, args, false);
    }

    /** @hide */
    public static void dump(Printer printer, String[] args, boolean isSystem) {
        boolean verbose = false;
        for (String arg : args) {
            if (arg.equals("-v")) {
                verbose = true;
            }
        }

        SQLiteDatabase.dumpAll(printer, verbose, isSystem);
    }
}
