package github.yaa110.memento.db;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

import org.json.JSONArray;
import org.json.JSONObject;

import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Locale;

import github.yaa110.memento.model.DatabaseModel;
import github.yaa110.memento.model.Note;

@SuppressWarnings("TryFinallyCanBeTryWithResources")
public class Controller {
	public static final int SORT_TITLE_ASC = 0;
	public static final int SORT_TITLE_DESC = 1;
	public static final int SORT_DATE_ASC = 2;
	public static final int SORT_DATE_DESC = 3;

	/**
	 * The singleton instance of Controller class
	 */
	public static Controller instance = null;

	private SQLiteOpenHelper helper;
	private String[] sorts = {
		OpenHelper.COLUMN_TITLE + " ASC",
		OpenHelper.COLUMN_TITLE + " DESC",
		OpenHelper.COLUMN_ID + " ASC",
		OpenHelper.COLUMN_ID + " DESC",
	};

	private Controller(Context context) {
		helper = new OpenHelper(context);
	}

	/**
	 * Instantiates the singleton instance of Controller class
	 * @param context the application context
	 */
	public static void create(Context context) {
		instance = new Controller(context);
	}

	/**
	 * Reads data from json array
	 * @param json an array of json objects
	 * @throws Exception
	 */
	public void readBackup(JSONArray json) throws Exception {
		SQLiteDatabase db = helper.getReadableDatabase();

		try {
			int length = json.length();
			for (int i = 0; i < length; i++) {
				JSONObject item = json.getJSONObject(i);

				ContentValues values = new ContentValues();
				values.put(OpenHelper.COLUMN_ID, item.getLong(OpenHelper.COLUMN_ID));
				values.put(OpenHelper.COLUMN_TITLE, item.getString(OpenHelper.COLUMN_TITLE));
				values.put(OpenHelper.COLUMN_BODY, item.getString(OpenHelper.COLUMN_BODY));
				values.put(OpenHelper.COLUMN_TYPE, item.getInt(OpenHelper.COLUMN_TYPE));
				values.put(OpenHelper.COLUMN_DATE, item.getString(OpenHelper.COLUMN_DATE));
				values.put(OpenHelper.COLUMN_ARCHIVED, item.getInt(OpenHelper.COLUMN_ARCHIVED));
				values.put(OpenHelper.COLUMN_THEME, item.getInt(OpenHelper.COLUMN_THEME));
				values.put(OpenHelper.COLUMN_COUNTER, item.getInt(OpenHelper.COLUMN_COUNTER));
				values.put(OpenHelper.COLUMN_PARENT_ID, item.getLong(OpenHelper.COLUMN_PARENT_ID));
				values.put(OpenHelper.COLUMN_EXTRA, item.getString(OpenHelper.COLUMN_EXTRA));

				db.replace(
					OpenHelper.TABLE_NOTES,
					null,
					values
				);
			}
		} finally {
			db.close();
		}
	}

	/**
	 * Writes data to file
	 * @param fos an object of FileOutputStream
	 * @throws Exception
	 */
	public void writeBackup(FileOutputStream fos) throws Exception {
		SQLiteDatabase db = helper.getReadableDatabase();

		try {
			Cursor c = db.query(
				OpenHelper.TABLE_NOTES,
				null, null, null, null, null, null
			);

			if (c != null) {
				boolean needComma = false;
				while (c.moveToNext()) {
					if (needComma) {
						fos.write(",".getBytes("UTF-8"));
					} else {
						needComma = true;
					}

					JSONObject item = new JSONObject();
					item.put(OpenHelper.COLUMN_ID, c.getLong(c.getColumnIndex(OpenHelper.COLUMN_ID)));
					item.put(OpenHelper.COLUMN_TITLE, c.getString(c.getColumnIndex(OpenHelper.COLUMN_TITLE)));
					item.put(OpenHelper.COLUMN_BODY, c.getString(c.getColumnIndex(OpenHelper.COLUMN_BODY)));
					item.put(OpenHelper.COLUMN_TYPE, c.getInt(c.getColumnIndex(OpenHelper.COLUMN_TYPE)));
					item.put(OpenHelper.COLUMN_DATE, c.getString(c.getColumnIndex(OpenHelper.COLUMN_DATE)));
					item.put(OpenHelper.COLUMN_ARCHIVED, c.getInt(c.getColumnIndex(OpenHelper.COLUMN_ARCHIVED)));
					item.put(OpenHelper.COLUMN_THEME, c.getInt(c.getColumnIndex(OpenHelper.COLUMN_THEME)));
					item.put(OpenHelper.COLUMN_COUNTER, c.getInt(c.getColumnIndex(OpenHelper.COLUMN_COUNTER)));
					item.put(OpenHelper.COLUMN_PARENT_ID, c.getLong(c.getColumnIndex(OpenHelper.COLUMN_PARENT_ID)));
					item.put(OpenHelper.COLUMN_EXTRA, c.getString(c.getColumnIndex(OpenHelper.COLUMN_EXTRA)));

					fos.write(item.toString().getBytes("UTF-8"));
				}

				c.close();
			}
		} finally {
			db.close();
		}
	}

	/**
	 * Reads all notes or categories from database
	 * @param cls the class of the model type
	 * @param columns the columns must be returned from the query
	 * @param where the where clause of the query.
	 * @param whereParams the parameters of where clause.
	 * @param sortId the sort id of categories or notes
	 * @param <T> a type which extends DatabaseModel
	 * @return a list of notes or categories
	 */
	public <T extends DatabaseModel> ArrayList<T> findNotes(Class<T> cls, String[] columns, String where, String[] whereParams, int sortId) {
		ArrayList<T> items = new ArrayList<>();

		SQLiteDatabase db = helper.getReadableDatabase();

		try {
			Cursor c = db.query(
				OpenHelper.TABLE_NOTES,
				columns,
				where,
				whereParams,
				null, null,
				sorts[sortId]
			);

			if (c != null) {
				while (c.moveToNext()) {
					try {
						items.add(cls.getDeclaredConstructor(Cursor.class).newInstance(c));
					} catch (Exception ignored) {
					}
				}

				c.close();
			}

			return items;
		} finally {
			db.close();
		}
	}

	/**
	 * Reads a note or category from the database
	 * @param cls the class of the model type
	 * @param id primary key of note or category
	 * @param <T> a type which extends DatabaseModel
	 * @return a new object of T type
	 */
	public <T extends DatabaseModel> T findNote(Class<T> cls, long id) {
		SQLiteDatabase db = helper.getReadableDatabase();

		try {
			Cursor c = db.query(
				OpenHelper.TABLE_NOTES,
				null,
				OpenHelper.COLUMN_ID + " = ?",
				new String[] {
					String.format(Locale.US, "%d", id)
				},
				null, null, null
			);

			if (c == null) return null;

			if (c.moveToFirst()) {
				try {
					return cls.getDeclaredConstructor(Cursor.class).newInstance(c);
				} catch (Exception e) {
					return null;
				}
			}

			return null;
		} finally {
			db.close();
		}
	}

	/**
	 * Change the amount of category counter
	 * @param categoryId the id of category
	 * @param amount to be added (negative or positive)
	 */
	public void addCategoryCounter(long categoryId, int amount) {
		SQLiteDatabase db = helper.getWritableDatabase();

		try {
			Cursor c = db.rawQuery(
				"UPDATE " + OpenHelper.TABLE_NOTES + " SET " + OpenHelper.COLUMN_COUNTER + " = " + OpenHelper.COLUMN_COUNTER + " + ? WHERE " + OpenHelper.COLUMN_ID + " = ?",
				new String[]{
					String.format(Locale.US, "%d", amount),
					String.format(Locale.US, "%d", categoryId)
				}
			);

			if (c != null) {
				c.moveToFirst();
				c.close();
			}
		} finally {
			db.close();
		}
	}

	/**
	 * Restores last deleted notes
	 */
	public void undoDeletion() {
		SQLiteDatabase db = helper.getWritableDatabase();

		try {
			Cursor c = db.query(
				OpenHelper.TABLE_UNDO,
				null, null, null, null, null, null
			);

			if (c != null) {
				while (c.moveToNext()) {
					String query = c.getString(c.getColumnIndex(OpenHelper.COLUMN_SQL));
					if (query != null) {
						Cursor nc = db.rawQuery(
							query,
							null
						);

						if (nc != null) {
							nc.moveToFirst();
							nc.close();
						}
					}
				}

				c.close();
			}

			clearUndoTable(db);
		} finally {
			db.close();
		}
	}

	/**
	 * Clears the undo table
	 * @param db an object of writable SQLiteDatabase
	 */
	public void clearUndoTable(SQLiteDatabase db) {
		Cursor uc = db.rawQuery("DELETE FROM " + OpenHelper.TABLE_UNDO, null);
		if (uc != null) {
			uc.moveToFirst();
			uc.close();
		}
	}

	/**
	 * Deletes a note or category (and its children) from the database
	 * @param ids a list of the notes' IDs
	 * @param categoryId the id of parent category
	 */
	public void deleteNotes(String[] ids, long categoryId) {
		SQLiteDatabase db = helper.getWritableDatabase();

		try {
			clearUndoTable(db);

			StringBuilder where = new StringBuilder();
			StringBuilder childWhere = new StringBuilder();

			boolean needOR = false;
			for (int i = 0; i < ids.length; i++) {
				if (needOR) {
					where.append(" OR ");
					childWhere.append(" OR ");
				} else {
					needOR = true;
				}
				where.append(OpenHelper.COLUMN_ID).append(" = ?");
				childWhere.append(OpenHelper.COLUMN_PARENT_ID).append(" = ?");
			}

			int count = db.delete(
				OpenHelper.TABLE_NOTES,
				where.toString(),
				ids
			);

			if (categoryId == DatabaseModel.NEW_MODEL_ID) {
				db.delete(
					OpenHelper.TABLE_NOTES,
					childWhere.toString(),
					ids
				);
			} else {
				Cursor c = db.rawQuery(
					"UPDATE " + OpenHelper.TABLE_NOTES + " SET " + OpenHelper.COLUMN_COUNTER + " = " + OpenHelper.COLUMN_COUNTER + " - ? WHERE " + OpenHelper.COLUMN_ID + " = ?",
					new String[]{
						String.format(Locale.US, "%d", count),
						String.format(Locale.US, "%d", categoryId)
					}
				);

				if (c != null) {
					c.moveToFirst();
					c.close();
				}
			}
		} finally {
			db.close();
		}
	}

	/**
	 * Inserts or updates a note or category in the database and increments the counter
	 * of category if the deleted object is an instance of Note class
	 * @param note the object of type T
	 * @param values ContentValuse of the object to be inserted or updated
	 * @param <T> a type which extends DatabaseModel
	 * @return the id of saved note
	 */
	public <T extends DatabaseModel> long saveNote(T note, ContentValues values) {
		SQLiteDatabase db = helper.getWritableDatabase();

		try {
			if (note.id > DatabaseModel.NEW_MODEL_ID) {
				// Update note
				db.update(
					OpenHelper.TABLE_NOTES,
					note.getContentValues(),
					OpenHelper.COLUMN_ID + " = ?",
					new String[]{
						String.format(Locale.US, "%d", note.id)
					}
				);
				return note.id;
			} else {
				// Create a new note
				note.id = db.insert(
					OpenHelper.TABLE_NOTES,
					null,
					values
				);

				if (note instanceof Note) {
					// Increment the counter of category
					Cursor c = db.rawQuery(
						"UPDATE " + OpenHelper.TABLE_NOTES + " SET " + OpenHelper.COLUMN_COUNTER + " = " + OpenHelper.COLUMN_COUNTER + " + 1 WHERE " + OpenHelper.COLUMN_ID + " = ?",
						new String[]{
							String.format(Locale.US, "%d", ((Note) note).categoryId)
						}
					);

					if (c != null) {
						c.moveToFirst();
						c.close();
					}
				}

				return note.id;
			}
		} catch (Exception e) {
			return DatabaseModel.NEW_MODEL_ID;
		} finally {
			db.close();
		}
	}
}