/*
 * Copyright (C) 2012 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.google.android.vending.expansion.downloader.impl;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDoneException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteStatement;
import android.provider.BaseColumns;
import android.util.Log;

public class DownloadsDB {
	private static final String DATABASE_NAME = "DownloadsDB";
	private static final int DATABASE_VERSION = 7;
	public static final String LOG_TAG = DownloadsDB.class.getName();
	final SQLiteOpenHelper mHelper;
	SQLiteStatement mGetDownloadByIndex;
	SQLiteStatement mUpdateCurrentBytes;
	private static DownloadsDB mDownloadsDB;
	long mMetadataRowID = -1;
	int mVersionCode = -1;
	int mStatus = -1;
	int mFlags;

	static public synchronized DownloadsDB getDB(Context paramContext) {
		if (null == mDownloadsDB) {
			return new DownloadsDB(paramContext);
		}
		return mDownloadsDB;
	}

	private SQLiteStatement getDownloadByIndexStatement() {
		if (null == mGetDownloadByIndex) {
			mGetDownloadByIndex = mHelper.getReadableDatabase().compileStatement(
					"SELECT " + BaseColumns._ID + " FROM " + DownloadColumns.TABLE_NAME + " WHERE " + DownloadColumns.INDEX + " = ?");
		}
		return mGetDownloadByIndex;
	}

	private SQLiteStatement getUpdateCurrentBytesStatement() {
		if (null == mUpdateCurrentBytes) {
			mUpdateCurrentBytes = mHelper.getReadableDatabase().compileStatement(
					"UPDATE " + DownloadColumns.TABLE_NAME + " SET " + DownloadColumns.CURRENTBYTES + " = ?"
					+
					" WHERE " + DownloadColumns.INDEX + " = ?");
		}
		return mUpdateCurrentBytes;
	}

	private DownloadsDB(Context paramContext) {
		this.mHelper = new DownloadsContentDBHelper(paramContext);
		final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
		// Query for the version code, the row ID of the metadata (for future
		// updating) the status and the flags
		Cursor cur = sqldb.rawQuery("SELECT " +
											MetadataColumns.APKVERSION + "," +
											BaseColumns._ID + "," +
											MetadataColumns.DOWNLOAD_STATUS + "," +
											MetadataColumns.FLAGS +
											" FROM " + MetadataColumns.TABLE_NAME + " LIMIT 1",
				null);
		if (null != cur && cur.moveToFirst()) {
			mVersionCode = cur.getInt(0);
			mMetadataRowID = cur.getLong(1);
			mStatus = cur.getInt(2);
			mFlags = cur.getInt(3);
			cur.close();
		}
		mDownloadsDB = this;
	}

	protected DownloadInfo getDownloadInfoByFileName(String fileName) {
		final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
		Cursor itemcur = null;
		try {
			itemcur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION,
					DownloadColumns.FILENAME + " = ?",
					new String[] {
							fileName },
					null, null, null);
			if (null != itemcur && itemcur.moveToFirst()) {
				return getDownloadInfoFromCursor(itemcur);
			}
		} finally {
			if (null != itemcur)
				itemcur.close();
		}
		return null;
	}

	public long getIDForDownloadInfo(final DownloadInfo di) {
		return getIDByIndex(di.mIndex);
	}

	public long getIDByIndex(int index) {
		SQLiteStatement downloadByIndex = getDownloadByIndexStatement();
		downloadByIndex.clearBindings();
		downloadByIndex.bindLong(1, index);
		try {
			return downloadByIndex.simpleQueryForLong();
		} catch (SQLiteDoneException e) {
			return -1;
		}
	}

	public void updateDownloadCurrentBytes(final DownloadInfo di) {
		SQLiteStatement downloadCurrentBytes = getUpdateCurrentBytesStatement();
		downloadCurrentBytes.clearBindings();
		downloadCurrentBytes.bindLong(1, di.mCurrentBytes);
		downloadCurrentBytes.bindLong(2, di.mIndex);
		downloadCurrentBytes.execute();
	}

	public void close() {
		this.mHelper.close();
	}

	protected static class DownloadsContentDBHelper extends SQLiteOpenHelper {
		DownloadsContentDBHelper(Context paramContext) {
			super(paramContext, DATABASE_NAME, null, DATABASE_VERSION);
		}

		private String createTableQueryFromArray(String paramString,
				String[][] paramArrayOfString) {
			StringBuilder localStringBuilder = new StringBuilder();
			localStringBuilder.append("CREATE TABLE ");
			localStringBuilder.append(paramString);
			localStringBuilder.append(" (");
			int i = paramArrayOfString.length;
			for (int j = 0;; j++) {
				if (j >= i) {
					localStringBuilder
							.setLength(localStringBuilder.length() - 1);
					localStringBuilder.append(");");
					return localStringBuilder.toString();
				}
				String[] arrayOfString = paramArrayOfString[j];
				localStringBuilder.append(' ');
				localStringBuilder.append(arrayOfString[0]);
				localStringBuilder.append(' ');
				localStringBuilder.append(arrayOfString[1]);
				localStringBuilder.append(',');
			}
		}

		/**
         * These two arrays must match and have the same order. For every Schema
         * there must be a corresponding table name.
         */
		static final private String[][][] sSchemas = {
			DownloadColumns.SCHEMA, MetadataColumns.SCHEMA
		};

		static final private String[] sTables = {
			DownloadColumns.TABLE_NAME, MetadataColumns.TABLE_NAME
		};

		/**
         * Goes through all of the tables in sTables and drops each table if it
         * exists. Altered to no longer make use of reflection.
         */
		private void dropTables(SQLiteDatabase paramSQLiteDatabase) {
			for (String table : sTables) {
				try {
					paramSQLiteDatabase.execSQL("DROP TABLE IF EXISTS " + table);
				} catch (Exception localException) {
					localException.printStackTrace();
				}
			}
		}

		/**
         * Goes through all of the tables in sTables and creates a database with
         * the corresponding schema described in sSchemas. Altered to no longer
         * make use of reflection.
         */
		public void onCreate(SQLiteDatabase paramSQLiteDatabase) {
			int numSchemas = sSchemas.length;
			for (int i = 0; i < numSchemas; i++) {
				try {
					String[][] schema = (String[][])sSchemas[i];
					paramSQLiteDatabase.execSQL(createTableQueryFromArray(
							sTables[i], schema));
				} catch (Exception localException) {
					while (true)
						localException.printStackTrace();
				}
			}
		}

		public void onUpgrade(SQLiteDatabase paramSQLiteDatabase,
				int paramInt1, int paramInt2) {
			Log.w(DownloadsContentDBHelper.class.getName(),
					"Upgrading database from version " + paramInt1 + " to " + paramInt2 + ", which will destroy all old data");
			dropTables(paramSQLiteDatabase);
			onCreate(paramSQLiteDatabase);
		}
	}

	public static class MetadataColumns implements BaseColumns {
		public static final String APKVERSION = "APKVERSION";
		public static final String DOWNLOAD_STATUS = "DOWNLOADSTATUS";
		public static final String FLAGS = "DOWNLOADFLAGS";

		public static final String[][] SCHEMA = {
			{ BaseColumns._ID, "INTEGER PRIMARY KEY" },
			{ APKVERSION, "INTEGER" }, { DOWNLOAD_STATUS, "INTEGER" },
			{ FLAGS, "INTEGER" }
		};
		public static final String TABLE_NAME = "MetadataColumns";
		public static final String _ID = "MetadataColumns._id";
	}

	public static class DownloadColumns implements BaseColumns {
		public static final String INDEX = "FILEIDX";
		public static final String URI = "URI";
		public static final String FILENAME = "FN";
		public static final String ETAG = "ETAG";

		public static final String TOTALBYTES = "TOTALBYTES";
		public static final String CURRENTBYTES = "CURRENTBYTES";
		public static final String LASTMOD = "LASTMOD";

		public static final String STATUS = "STATUS";
		public static final String CONTROL = "CONTROL";
		public static final String NUM_FAILED = "FAILCOUNT";
		public static final String RETRY_AFTER = "RETRYAFTER";
		public static final String REDIRECT_COUNT = "REDIRECTCOUNT";

		public static final String[][] SCHEMA = {
			{ BaseColumns._ID, "INTEGER PRIMARY KEY" },
			{ INDEX, "INTEGER UNIQUE" }, { URI, "TEXT" },
			{ FILENAME, "TEXT UNIQUE" }, { ETAG, "TEXT" },
			{ TOTALBYTES, "INTEGER" }, { CURRENTBYTES, "INTEGER" },
			{ LASTMOD, "INTEGER" }, { STATUS, "INTEGER" },
			{ CONTROL, "INTEGER" }, { NUM_FAILED, "INTEGER" },
			{ RETRY_AFTER, "INTEGER" }, { REDIRECT_COUNT, "INTEGER" }
		};
		public static final String TABLE_NAME = "DownloadColumns";
		public static final String _ID = "DownloadColumns._id";
	}

	private static final String[] DC_PROJECTION = {
		DownloadColumns.FILENAME,
		DownloadColumns.URI, DownloadColumns.ETAG,
		DownloadColumns.TOTALBYTES, DownloadColumns.CURRENTBYTES,
		DownloadColumns.LASTMOD, DownloadColumns.STATUS,
		DownloadColumns.CONTROL, DownloadColumns.NUM_FAILED,
		DownloadColumns.RETRY_AFTER, DownloadColumns.REDIRECT_COUNT,
		DownloadColumns.INDEX
	};

	private static final int FILENAME_IDX = 0;
	private static final int URI_IDX = 1;
	private static final int ETAG_IDX = 2;
	private static final int TOTALBYTES_IDX = 3;
	private static final int CURRENTBYTES_IDX = 4;
	private static final int LASTMOD_IDX = 5;
	private static final int STATUS_IDX = 6;
	private static final int CONTROL_IDX = 7;
	private static final int NUM_FAILED_IDX = 8;
	private static final int RETRY_AFTER_IDX = 9;
	private static final int REDIRECT_COUNT_IDX = 10;
	private static final int INDEX_IDX = 11;

	/**
     * This function will add a new file to the database if it does not exist.
     *
     * @param di DownloadInfo that we wish to store
     * @return the row id of the record to be updated/inserted, or -1
     */
	public boolean updateDownload(DownloadInfo di) {
		ContentValues cv = new ContentValues();
		cv.put(DownloadColumns.INDEX, di.mIndex);
		cv.put(DownloadColumns.FILENAME, di.mFileName);
		cv.put(DownloadColumns.URI, di.mUri);
		cv.put(DownloadColumns.ETAG, di.mETag);
		cv.put(DownloadColumns.TOTALBYTES, di.mTotalBytes);
		cv.put(DownloadColumns.CURRENTBYTES, di.mCurrentBytes);
		cv.put(DownloadColumns.LASTMOD, di.mLastMod);
		cv.put(DownloadColumns.STATUS, di.mStatus);
		cv.put(DownloadColumns.CONTROL, di.mControl);
		cv.put(DownloadColumns.NUM_FAILED, di.mNumFailed);
		cv.put(DownloadColumns.RETRY_AFTER, di.mRetryAfter);
		cv.put(DownloadColumns.REDIRECT_COUNT, di.mRedirectCount);
		return updateDownload(di, cv);
	}

	public boolean updateDownload(DownloadInfo di, ContentValues cv) {
		long id = di == null ? -1 : getIDForDownloadInfo(di);
		try {
			final SQLiteDatabase sqldb = mHelper.getWritableDatabase();
			if (id != -1) {
				if (1 != sqldb.update(DownloadColumns.TABLE_NAME,
								 cv, DownloadColumns._ID + " = " + id, null)) {
					return false;
				}
			} else {
				return -1 != sqldb.insert(DownloadColumns.TABLE_NAME,
									 DownloadColumns.URI, cv);
			}
		} catch (android.database.sqlite.SQLiteException ex) {
			ex.printStackTrace();
		}
		return false;
	}

	public int getLastCheckedVersionCode() {
		return mVersionCode;
	}

	public boolean isDownloadRequired() {
		final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
		Cursor cur = sqldb.rawQuery("SELECT Count(*) FROM " + DownloadColumns.TABLE_NAME + " WHERE " + DownloadColumns.STATUS + " <> 0", null);
		try {
			if (null != cur && cur.moveToFirst()) {
				return 0 == cur.getInt(0);
			}
		} finally {
			if (null != cur)
				cur.close();
		}
		return true;
	}

	public int getFlags() {
		return mFlags;
	}

	public boolean updateFlags(int flags) {
		if (mFlags != flags) {
			ContentValues cv = new ContentValues();
			cv.put(MetadataColumns.FLAGS, flags);
			if (updateMetadata(cv)) {
				mFlags = flags;
				return true;
			} else {
				return false;
			}
		} else {
			return true;
		}
	};

	public boolean updateStatus(int status) {
		if (mStatus != status) {
			ContentValues cv = new ContentValues();
			cv.put(MetadataColumns.DOWNLOAD_STATUS, status);
			if (updateMetadata(cv)) {
				mStatus = status;
				return true;
			} else {
				return false;
			}
		} else {
			return true;
		}
	};

	public boolean updateMetadata(ContentValues cv) {
		final SQLiteDatabase sqldb = mHelper.getWritableDatabase();
		if (-1 == this.mMetadataRowID) {
			long newID = sqldb.insert(MetadataColumns.TABLE_NAME,
					MetadataColumns.APKVERSION, cv);
			if (-1 == newID)
				return false;
			mMetadataRowID = newID;
		} else {
			if (0 == sqldb.update(MetadataColumns.TABLE_NAME, cv,
							 BaseColumns._ID + " = " + mMetadataRowID, null))
				return false;
		}
		return true;
	}

	public boolean updateMetadata(int apkVersion, int downloadStatus) {
		ContentValues cv = new ContentValues();
		cv.put(MetadataColumns.APKVERSION, apkVersion);
		cv.put(MetadataColumns.DOWNLOAD_STATUS, downloadStatus);
		if (updateMetadata(cv)) {
			mVersionCode = apkVersion;
			mStatus = downloadStatus;
			return true;
		} else {
			return false;
		}
	};

	public boolean updateFromDb(DownloadInfo di) {
		final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
		Cursor cur = null;
		try {
			cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION,
					DownloadColumns.FILENAME + "= ?",
					new String[] {
							di.mFileName },
					null, null, null);
			if (null != cur && cur.moveToFirst()) {
				setDownloadInfoFromCursor(di, cur);
				return true;
			}
			return false;
		} finally {
			if (null != cur) {
				cur.close();
			}
		}
	}

	public void setDownloadInfoFromCursor(DownloadInfo di, Cursor cur) {
		di.mUri = cur.getString(URI_IDX);
		di.mETag = cur.getString(ETAG_IDX);
		di.mTotalBytes = cur.getLong(TOTALBYTES_IDX);
		di.mCurrentBytes = cur.getLong(CURRENTBYTES_IDX);
		di.mLastMod = cur.getLong(LASTMOD_IDX);
		di.mStatus = cur.getInt(STATUS_IDX);
		di.mControl = cur.getInt(CONTROL_IDX);
		di.mNumFailed = cur.getInt(NUM_FAILED_IDX);
		di.mRetryAfter = cur.getInt(RETRY_AFTER_IDX);
		di.mRedirectCount = cur.getInt(REDIRECT_COUNT_IDX);
	}

	public DownloadInfo getDownloadInfoFromCursor(Cursor cur) {
		DownloadInfo di = new DownloadInfo(cur.getInt(INDEX_IDX),
				cur.getString(FILENAME_IDX), this.getClass().getPackage().getName());
		setDownloadInfoFromCursor(di, cur);
		return di;
	}

	public DownloadInfo[] getDownloads() {
		final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
		Cursor cur = null;
		try {
			cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, null,
					null, null, null, null);
			if (null != cur && cur.moveToFirst()) {
				DownloadInfo[] retInfos = new DownloadInfo[cur.getCount()];
				int idx = 0;
				do {
					DownloadInfo di = getDownloadInfoFromCursor(cur);
					retInfos[idx++] = di;
				} while (cur.moveToNext());
				return retInfos;
			}
			return null;
		} finally {
			if (null != cur) {
				cur.close();
			}
		}
	}
}
