// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.database;

import android.database.AbstractCursor;
import android.database.CursorWindow;
import android.util.Log;

import org.chromium.base.annotations.CalledByNative;

import java.sql.Types;

/**
 * This class exposes the query result from native side.
 */
public class SQLiteCursor extends AbstractCursor {
    private static final String TAG = "SQLiteCursor";
    // Used by JNI.
    private long mNativeSQLiteCursor;

    // The count of result rows.
    private int mCount = -1;

    private int[] mColumnTypes;

    private final Object mColumnTypeLock = new Object();
    private final Object mDestoryNativeLock = new Object();

    // The belows are the locks for those methods that need wait for
    // the callback result in native side.
    private final Object mMoveLock = new Object();
    private final Object mGetBlobLock = new Object();

    private SQLiteCursor(long nativeSQLiteCursor) {
        mNativeSQLiteCursor = nativeSQLiteCursor;
    }

    @CalledByNative
    private static SQLiteCursor create(long nativeSQLiteCursor) {
        return new SQLiteCursor(nativeSQLiteCursor);
    }

    @Override
    public int getCount() {
        synchronized (mMoveLock) {
            if (mCount == -1) mCount = nativeGetCount(mNativeSQLiteCursor);
        }
        return mCount;
    }

    @Override
    public String[] getColumnNames() {
        return nativeGetColumnNames(mNativeSQLiteCursor);
    }

    @Override
    public String getString(int column) {
        return nativeGetString(mNativeSQLiteCursor, column);
    }

    @Override
    public short getShort(int column) {
        return (short) nativeGetInt(mNativeSQLiteCursor, column);
    }

    @Override
    public int getInt(int column) {
        return nativeGetInt(mNativeSQLiteCursor, column);
    }

    @Override
    public long getLong(int column) {
        return nativeGetLong(mNativeSQLiteCursor, column);
    }

    @Override
    public float getFloat(int column) {
        return (float) nativeGetDouble(mNativeSQLiteCursor, column);
    }

    @Override
    public double getDouble(int column) {
        return nativeGetDouble(mNativeSQLiteCursor, column);
    }

    @Override
    public boolean isNull(int column) {
        return nativeIsNull(mNativeSQLiteCursor, column);
    }

    @Override
    public void close() {
        super.close();
        synchronized (mDestoryNativeLock) {
            if (mNativeSQLiteCursor != 0) {
                nativeDestroy(mNativeSQLiteCursor);
                mNativeSQLiteCursor = 0;
            }
        }
    }

    @Override
    public boolean onMove(int oldPosition, int newPosition) {
        synchronized (mMoveLock) {
            nativeMoveTo(mNativeSQLiteCursor, newPosition);
        }
        return super.onMove(oldPosition, newPosition);
    }

    @Override
    public byte[] getBlob(int column) {
        synchronized (mGetBlobLock) {
            return nativeGetBlob(mNativeSQLiteCursor, column);
        }
    }

    @Deprecated
    public boolean supportsUpdates() {
        return false;
    }

    @Override
    protected void finalize() {
        super.finalize();
        if (!isClosed()) {
            Log.w(TAG, "Cursor hasn't been closed");
            close();
        }
    }

    @Override
    public void fillWindow(int position, CursorWindow window) {
        if (position < 0 || position > getCount()) {
            return;
        }
        window.acquireReference();
        try {
            int oldpos = getPosition();
            moveToPosition(position - 1);
            window.clear();
            window.setStartPosition(position);
            int columnNum = getColumnCount();
            window.setNumColumns(columnNum);
            while (moveToNext() && window.allocRow()) {
                int pos = getPosition();
                for (int i = 0; i < columnNum; i++) {
                    boolean hasRoom = true;
                    switch (getColumnType(i)) {
                        case Types.DOUBLE:
                            hasRoom = fillRow(window, Double.valueOf(getDouble(i)), pos, i);
                            break;
                        case Types.NUMERIC:
                            hasRoom = fillRow(window, Long.valueOf(getLong(i)), pos, i);
                            break;
                        case Types.BLOB:
                            hasRoom = fillRow(window, getBlob(i), pos, i);
                            break;
                        case Types.LONGVARCHAR:
                            hasRoom = fillRow(window, getString(i), pos, i);
                            break;
                        case Types.NULL:
                            hasRoom = fillRow(window, null, pos, i);
                            break;
                        default:
                            // Ignore an unknown type.
                    }
                    if (!hasRoom) {
                        break;
                    }
                }
            }
            moveToPosition(oldpos);
        } catch (IllegalStateException e) {
            // simply ignore it
        } finally {
            window.releaseReference();
        }
    }

    /**
     * Fill row with the given value. If the value type is other than Long,
     * String, byte[] or Double, the NULL will be filled.
     *
     * @return true if succeeded, false if window is full.
     */
    private boolean fillRow(CursorWindow window, Object value, int pos, int column) {
        if (putValue(window, value, pos, column)) {
            return true;
        } else {
            window.freeLastRow();
            return false;
        }
    }

    /**
     * Put the value in given window. If the value type is other than Long,
     * String, byte[] or Double, the NULL will be filled.
     *
     * @return true if succeeded.
     */
    private boolean putValue(CursorWindow window, Object value, int pos, int column) {
        if (value == null) {
            return window.putNull(pos, column);
        } else if (value instanceof Long) {
            return window.putLong((Long) value, pos, column);
        } else if (value instanceof String) {
            return window.putString((String) value, pos, column);
        } else if (value instanceof byte[] && ((byte[]) value).length > 0) {
            return window.putBlob((byte[]) value, pos, column);
        } else if (value instanceof Double) {
            return window.putDouble((Double) value, pos, column);
        } else {
            return window.putNull(pos, column);
        }
    }

    /**
     * @param index the column index.
     * @return the column type from cache or native side.
     */
    private int getColumnType(int index) {
        synchronized (mColumnTypeLock) {
            if (mColumnTypes == null) {
                int columnCount = getColumnCount();
                mColumnTypes = new int[columnCount];
                for (int i = 0; i < columnCount; i++) {
                    mColumnTypes[i] = nativeGetColumnType(mNativeSQLiteCursor, i);
                }
            }
        }
        return mColumnTypes[index];
    }

    private native void nativeDestroy(long nativeSQLiteCursor);
    private native int nativeGetCount(long nativeSQLiteCursor);
    private native String[] nativeGetColumnNames(long nativeSQLiteCursor);
    private native int nativeGetColumnType(long nativeSQLiteCursor, int column);
    private native String nativeGetString(long nativeSQLiteCursor, int column);
    private native byte[] nativeGetBlob(long nativeSQLiteCursor, int column);
    private native boolean nativeIsNull(long nativeSQLiteCursor, int column);
    private native long nativeGetLong(long nativeSQLiteCursor, int column);
    private native int nativeGetInt(long nativeSQLiteCursor, int column);
    private native double nativeGetDouble(long nativeSQLiteCursor, int column);
    private native int nativeMoveTo(long nativeSQLiteCursor, int newPosition);
}