package com.trikke.writer;

import com.trikke.data.Model;
import com.trikke.data.Table;
import com.trikke.data.View;
import com.trikke.util.SqlUtil;

import javax.lang.model.element.Modifier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;

/**
 * Created by the awesome :
 * User: trikke
 * Date: 16/10/13
 * Time: 22:18
 */
public class ContentProviderWriter extends Writer
{
	private final Model mModel;

	public ContentProviderWriter( String javaOut, Model model ) throws IOException
	{
		super( javaOut, model, model.getContentProviderName() );
		this.mModel = model;
	}

	@Override
	public void compile() throws IOException
	{
		writer.emitPackage( mModel.getClassPackage() );

		emitImports();
		emitClass();
		emitFields();
		emitTableConstants();
		emitURIs();
		emitURIMatcher();

		emitMethods();

		writer.endType();

		writer.close();
	}

	private void emitImports() throws IOException
	{
		writer.emitImports( "java.util.Map", "java.util.ArrayList", "android.text.TextUtils", "android.content.*", "android.database.Cursor", "android.database.sqlite.SQLiteConstraintException", "android.database.sqlite.SQLiteDatabase", "android.database.sqlite.SQLiteException", "android.database.sqlite.SQLiteQueryBuilder", "android.database.sqlite.SQLiteStatement", "android.net.Uri", "android.text.TextUtils", "android.util.Log" );
		writer.emitEmptyLine();
	}

	private void emitClass() throws IOException
	{
		writer.beginType( mModel.getClassPackage() + "." + mModel.getContentProviderName() + " extends ContentProvider", "class", EnumSet.of( Modifier.PUBLIC, Modifier.FINAL ) );
	}

	private void emitFields() throws IOException
	{
		writer.emitEmptyLine();
		writer.emitField( "String", "TAG", EnumSet.of( Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL ), "\"" + mModel.getContentProviderName() + "\"" );
		writer.emitField( "String", "DATABASE_NAME", EnumSet.of( Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL ), "\"" + mModel.getDbClassName() + ".db\"" );
		writer.emitField( "int", "DATABASE_VERSION", EnumSet.of( Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL ), String.valueOf( mModel.getDbVersion() ) );
		writer.emitField( "String", "ROW_ID", EnumSet.of( Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL ), "\"" + Table.ANDROID_ID + "\"" );
		writer.emitEmptyLine();
		writer.emitField( "String", "AUTHORITY", EnumSet.of( Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL ), "\"" + mModel.getContentAuthority() + "\"" );
		writer.emitEmptyLine();
		writer.emitField( mModel.getDbClassName(), "mLocalDatabase", EnumSet.of( Modifier.PRIVATE ) );
		writer.emitEmptyLine();
	}

	private void emitTableConstants() throws IOException
	{
		int index = 1;
		for ( Table table : mModel.getTables() )
		{
			writer.emitSingleLineComment( table.name + " constants" );
			writer.emitField( "String", SqlUtil.IDENTIFIER( table ), EnumSet.of( Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL ), "\"" + table.name + "\"" );
			writer.emitField( "int", table.getAllName(), EnumSet.of( Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL ), "" + index );
			index++;
			writer.emitField( "int", table.getSingleName(), EnumSet.of( Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL ), "" + index );
			index++;
		}
		writer.emitEmptyLine();
		writer.emitSingleLineComment( "views constants" );
		for ( View view : mModel.getViews() )
		{
			writer.emitField( "String", view.name, EnumSet.of( Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL ), "\"" + view.name + "\"" );
			writer.emitField( "int", SqlUtil.IDENTIFIER( view ), EnumSet.of( Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL ), "" + index );
			index++;
		}
	}

	private void emitURIs() throws IOException
	{
		writer.emitEmptyLine();
		for ( Table table : mModel.getTables() )
		{
			writer.emitField( "Uri", SqlUtil.URI( table ), EnumSet.of( Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL ), "Uri.parse(\"content://" + mModel.getContentAuthority() + "/" + table.name + "\")" );
		}
		for ( View view : mModel.getViews() )
		{
			writer.emitField( "Uri", SqlUtil.URI( view ), EnumSet.of( Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL ), "Uri.parse(\"content://" + mModel.getContentAuthority() + "/" + view.name + "\")" );
		}
		writer.emitEmptyLine();
	}

	private void emitURIMatcher() throws IOException
	{
		writer.emitEmptyLine();
		writer.emitField( "UriMatcher", "uriMatcher", EnumSet.of( Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL ) );
		writer.beginInitializer( true );
		writer.emitStatement( "uriMatcher = new UriMatcher(UriMatcher.NO_MATCH)" );

		for ( Table table : mModel.getTables() )
		{
			writer.emitStatement( "uriMatcher.addURI(\"" + mModel.getContentAuthority() + "\", \"" + table.name + "\", " + table.getAllName() + ")" );
			writer.emitStatement( "uriMatcher.addURI(\"" + mModel.getContentAuthority() + "\", \"" + table.name + "/#\", " + table.getSingleName() + ")" );
		}

		for ( View view : mModel.getViews() )
		{
			writer.emitStatement( "uriMatcher.addURI(\"" + mModel.getContentAuthority() + "\", \"" + view.name + "\", " + SqlUtil.IDENTIFIER( view ) + ")" );
		}

		writer.endInitializer();
	}

	private void emitMethods() throws IOException
	{
		writer.emitEmptyLine();
		writer.emitAnnotation( "Override" );
		writer.beginMethod( "boolean", "onCreate", EnumSet.of( Modifier.PUBLIC ) );
		writer.emitStatement( "mLocalDatabase = new " + mModel.getDbClassName() + " (getContext())" );
		writer.emitStatement( "return true" );
		writer.endMethod();

		writer.emitEmptyLine();
		writer.beginMethod( "String", "getTableNameFromUri", EnumSet.of( Modifier.PRIVATE ), "Uri", "uri" );
		writer.beginControlFlow( "switch(uriMatcher.match(uri))" );

		for ( Table table : mModel.getTables() )
		{
			writer.emitStatement( "\tcase " + table.getAllName() + ":\ncase " + table.getSingleName() + ":\n\treturn " + SqlUtil.IDENTIFIER( table ) );
		}

		for ( View view : mModel.getViews() )
		{
			writer.emitStatement( "\tcase " + SqlUtil.IDENTIFIER( view ) + ":\n\treturn " + view.name );
		}


		writer.emitStatement( "default: break" );
		writer.endControlFlow();
		writer.emitEmptyLine();
		writer.emitStatement( "return null" );
		writer.endMethod();

		writer.emitEmptyLine();
		writer.beginMethod( "String", "getUniqueKey", EnumSet.of( Modifier.PRIVATE ), "Uri", "uri" );
		writer.emitSingleLineComment( "Only for actual tables" );
		writer.beginControlFlow( "switch(uriMatcher.match(uri))" );

		for ( Table table : mModel.getTables() )
		{
			writer.emitStatement( "\tcase " + table.getAllName() + ":\ncase " + table.getSingleName() + ":\n\treturn \"" + table.getPrimaryKey().name + "\"" );
		}

		writer.emitStatement( "default: break" );
		writer.endControlFlow();
		writer.emitEmptyLine();
		writer.emitStatement( "return null" );
		writer.endMethod();

		writer.emitEmptyLine();
		writer.beginMethod( "boolean", "containsUnique", EnumSet.of( Modifier.PRIVATE ), "Uri", "uri", "ContentValues", "contentvalues" );
		writer.emitStatement( "String unique = getUniqueKey(uri)" );
		writer.beginControlFlow( "for (String key : contentvalues.keySet())" );
		writer.beginControlFlow( "if (key.equals(unique))" );
		writer.emitStatement( "return true" );
		writer.endControlFlow();
		writer.endControlFlow();
		writer.emitStatement( "return false" );
		writer.endMethod();

		writer.emitEmptyLine();
		writer.beginMethod( "Uri", "getContentUriFromUri", EnumSet.of( Modifier.PRIVATE ), "Uri", "uri" );
		writer.emitSingleLineComment( "Only for actual tables" );
		writer.beginControlFlow( "switch(uriMatcher.match(uri))" );

		for ( Table table : mModel.getTables() )
		{
			writer.emitStatement( "\tcase " + table.getAllName() + ":\ncase " + table.getSingleName() + ":\n\treturn " + SqlUtil.URI( table ) );
		}

		writer.emitStatement( "default: break" );
		writer.endControlFlow();
		writer.emitEmptyLine();
		writer.emitStatement( "return null" );
		writer.endMethod();

		writer.emitEmptyLine();
		writer.beginMethod( "ArrayList<Uri>", "getAssociatedViewUris", EnumSet.of( Modifier.PRIVATE ), "Uri", "uri" );
		writer.emitSingleLineComment( "Only for actual views" );
		writer.emitStatement( "ArrayList<Uri> viewUris = null" );
		writer.beginControlFlow( "switch(uriMatcher.match(uri))" );
		for ( Table table : mModel.getTables() )
		{
			String statement = "\tcase " + table.getAllName() + ":\ncase " + table.getSingleName() + ":\n\t";
			ArrayList<String> associatedviews = new ArrayList<String>();
			for ( View view : mModel.getViews() )
			{
				if ( view.getFromtables().contains( table.name ) )
				{
					associatedviews.add( SqlUtil.URI( view ) );
				}
			}

			if ( !associatedviews.isEmpty() )
			{
				statement += "viewUris = new ArrayList<Uri>();\n\t";
			}

			for ( String string : associatedviews )
			{
				statement += "viewUris.add(" + string + ");\n\t";
			}

			statement += "break";

			writer.emitStatement( statement );
		}

		writer.emitStatement( "default: break" );
		writer.endControlFlow();
		writer.emitEmptyLine();
		writer.emitStatement( "return viewUris" );
		writer.endMethod();

		writer.emitEmptyLine();
		writer.emitAnnotation( "Override" );
		writer.beginMethod( "String", "getType", EnumSet.of( Modifier.PUBLIC ), "Uri", "uri" );
		writer.emitSingleLineComment( "Return a string that identifies the MIME type for a Content Provider URI" );
		writer.beginControlFlow( "switch(uriMatcher.match(uri))" );

		for ( Table table : mModel.getTables() )
		{
			writer.emitStatement( "\tcase " + table.getAllName() + ":\n\treturn \"vnd.android.cursor.dir/vnd." + mModel.getClassPackage() + "." + table.name + "\"" );
			writer.emitStatement( "\tcase " + table.getSingleName() + ":\n\treturn \"vnd.android.cursor.dir/vnd." + mModel.getClassPackage() + "." + table.name + "\"" );
		}

		for ( View view : mModel.getViews() )
		{
			writer.emitStatement( "\tcase " + SqlUtil.IDENTIFIER( view ) + ":\n\treturn \"vnd.android.cursor.dir/vnd." + mModel.getClassPackage() + "." + view.name + "\"" );
		}

		writer.emitStatement( "default:\n throw new IllegalArgumentException(\"Unsupported URI: \" + uri)" );
		writer.endControlFlow();
		writer.endMethod();

		writer.emitEmptyLine();
		writer.emitAnnotation( "Override" );
		writer.beginMethod( "Cursor", "query", EnumSet.of( Modifier.PUBLIC ), "Uri", "uri", "String[]", "projection", "String", "selection", "String[]", "selectionArgs", "String", "sortOrder" );
		writer.emitSingleLineComment( "Open database" );
		writer.emitStatement( "SQLiteDatabase db" );
		writer.beginControlFlow( "try" );
		writer.emitStatement( "db = mLocalDatabase.getWritableDatabase()" );
		writer.nextControlFlow( "catch (SQLiteException ex)" );
		writer.emitStatement( "db = mLocalDatabase.getReadableDatabase()" );
		writer.endControlFlow();

		writer.emitSingleLineComment( "Replace these with valid SQL statements if necessary." );
		writer.emitStatement( "String groupBy = null" );
		writer.emitStatement( "String having = null" );
		writer.emitStatement( "SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder()" );

		writer.emitSingleLineComment( "If this is a row query, limit the result set to the passed in row." );

		writer.emitStatement( "String rowID" );
		writer.beginControlFlow( "switch(uriMatcher.match(uri))" );

		for ( Table table : mModel.getTables() )
		{
			writer.emitStatement( "case " + table.getSingleName() + ":\n\trowID = uri.getPathSegments().get(1);\n\tqueryBuilder.appendWhere(ROW_ID + \"=\" + rowID);\n\tbreak" );
		}

		writer.emitStatement( "default: break" );
		writer.endControlFlow();

		writer.emitSingleLineComment( "Specify the table on which to perform the query. This can be a specific table or a join as required." );
		writer.emitStatement( "queryBuilder.setTables(getTableNameFromUri(uri))" );

		writer.emitEmptyLine();
		writer.emitSingleLineComment( "Execute..." );
		writer.emitStatement( "Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, groupBy, having, sortOrder)" );
		writer.emitStatement( "cursor.setNotificationUri(getContext().getContentResolver(), uri)" );
		writer.emitStatement( "return cursor" );

		writer.endMethod();


		ArrayList<String> throwTypes = new ArrayList<String>();
		throwTypes.add( "OperationApplicationException" );
		ArrayList<String> parameters = new ArrayList<String>();
		parameters.add( "ArrayList <ContentProviderOperation>" );
		parameters.add( "operations" );

		writer.emitAnnotation( "Override" );
		writer.beginMethod( "ContentProviderResult[]", "applyBatch", EnumSet.of( Modifier.PUBLIC ), parameters, throwTypes );
		writer.emitStatement( "SQLiteDatabase db = mLocalDatabase.getWritableDatabase()" );
		writer.emitStatement( "db.beginTransaction()" );
		writer.emitStatement( "final int numOperations = operations.size()" );
		writer.emitStatement( "final ContentProviderResult[] results = new ContentProviderResult[numOperations]" );
		writer.emitStatement( "Log.i(TAG, \"Applying a batch of \" + numOperations + \" operations.\")" );
		writer.beginControlFlow( "try" );

		writer.beginControlFlow( "for (int i = 0; i < numOperations; i++)" );
		writer.emitStatement( "results[i] = operations.get(i).apply(this, results, i)" );
		writer.endControlFlow();
		writer.emitStatement( "db.setTransactionSuccessful()" );
		writer.nextControlFlow( "finally" );
		writer.emitStatement( "db.endTransaction()" );
		writer.endControlFlow();
		writer.emitStatement( "return results" );
		writer.endMethod();

		writer.emitEmptyLine();
		writer.emitAnnotation( "Override" );
		writer.beginMethod( "int", "delete", EnumSet.of( Modifier.PUBLIC ), "Uri", "uri", "String", "selection", "String[]", "selectionArgs" );
		writer.emitSingleLineComment( "Open database" );
		writer.emitStatement( "SQLiteDatabase db = mLocalDatabase.getWritableDatabase()" );

		writer.emitStatement( "String rowID" );
		writer.emitStatement( "String UNIQUEID" );
		writer.beginControlFlow( "switch(uriMatcher.match(uri))" );

		for ( Table table : mModel.getTables() )
		{
			writer.emitStatement( "\tcase " + table.getSingleName() + ":" + "\n\tUNIQUEID = \"" + table.getPrimaryKey().name + "\";\n\trowID = uri.getPathSegments().get(1);\n\tselection = UNIQUEID + \"=\" + rowID + (!TextUtils.isEmpty(selection) ? \" AND (\" + selection + ')' : \"\")" );
		}

		writer.emitStatement( "default: break" );
		writer.endControlFlow();

		writer.emitEmptyLine();
		writer.beginControlFlow( "if (selection == null)" );
		writer.emitStatement( "selection = \"1\"" );
		writer.endControlFlow();
		writer.emitEmptyLine();
		writer.emitStatement( "int deleteCount = db.delete(getTableNameFromUri(uri), selection, selectionArgs)" );
		insertNotifyBlock();
		writer.emitEmptyLine();
		writer.emitStatement( "return deleteCount" );
		writer.endMethod();

		writer.emitEmptyLine();
		writer.emitAnnotation( "Override" );
		writer.beginMethod( "Uri", "insert", EnumSet.of( Modifier.PUBLIC ), "Uri", "uri", "ContentValues", "values" );
		writer.emitSingleLineComment( "Open database" );
		writer.emitStatement( "SQLiteDatabase db = mLocalDatabase.getWritableDatabase()" );
		writer.emitSingleLineComment( "Try to do an insert as per usual" );
		writer.emitStatement( "String nullColumnHack = null" );
		writer.emitStatement( "long id = db.insert(getTableNameFromUri(uri), nullColumnHack, values)" );

		// TODO : fix upsert
		/*
		if (true)
		{
			writer.emitEmptyLine();
			writer.beginControlFlow( "if (id == -1)" );
			writer.emitSingleLineComment( "There was an error inserting, try upsert!" );
			writer.beginControlFlow( "if (containsUnique(uri, values))" );
			writer.emitStatement( "ContentValues withoutUnique = new ContentValues(values)" );
			writer.emitStatement( "String unique = getUniqueKey(uri)" );
			writer.emitStatement( "withoutUnique.remove(unique)" );
			writer.emitStatement( "String selection = unique + \"=?\"" );
			writer.emitStatement( "String[] selectionArgs = new String[]{String.valueOf(values.get(unique))}" );
			writer.emitStatement( "int updated = db.update(getTableNameFromUri(uri), withoutUnique, selection, selectionArgs)" );
			writer.beginControlFlow( "if (updated > 0)");
			writer.emitSingleLineComment( "If any row was updated, we'll get the row id from that row so we can pass it along below." );
			writer.emitStatement( "SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder()");
			writer.emitStatement( "queryBuilder.setTables(getTableNameFromUri(uri))");
			writer.emitStatement( "Cursor c = queryBuilder.query(db, new String[]{\"_id\"}, selection, selectionArgs, null, null, null)");
			writer.emitStatement( "c.moveToNext()" );
			writer.emitStatement( "id = c.getLong(0)" );
			writer.emitStatement( "c.close()" );
			writer.endControlFlow();
			writer.endControlFlow();
			writer.endControlFlow();
		}
		*/

		writer.emitEmptyLine();
		writer.beginControlFlow( "if (id > -1)" );
		writer.emitSingleLineComment( "the insert was successful" );
		writer.emitStatement( "Uri insertedId = ContentUris.withAppendedId(getContentUriFromUri(uri), id)" );
		insertNotifyBlock();
		writer.emitEmptyLine();
		writer.emitStatement( "return insertedId" );
		writer.endControlFlow();
		writer.emitStatement( "return null" );
		writer.endMethod();


		writer.emitEmptyLine();
		writer.emitAnnotation( "Override" );
		writer.beginMethod( "int", "update", EnumSet.of( Modifier.PUBLIC ), "Uri", "uri", "ContentValues", "values", "String", "selection", "String[]", "selectionArgs" );
		writer.emitSingleLineComment( "Open database" );
		writer.emitStatement( "SQLiteDatabase db = mLocalDatabase.getWritableDatabase()" );
		writer.emitSingleLineComment( "If this is a row URI, limit the deletion to the specified row." );

		writer.emitStatement( "String rowID" );
		writer.emitStatement( "String UNIQUEID" );
		writer.beginControlFlow( "switch(uriMatcher.match(uri))" );

		for ( Table table : mModel.getTables() )
		{
			writer.emitStatement( "\tcase " + table.getSingleName() + ":" + "\n\tUNIQUEID = \"" + table.getPrimaryKey().name + "\";\n\trowID = uri.getPathSegments().get(1);\n\tselection = UNIQUEID + \"=\" + rowID + (!TextUtils.isEmpty(selection) ? \" AND (\" + selection + ')' : \"\")" );
		}
		writer.emitStatement( "default: break" );
		writer.endControlFlow();

		writer.emitSingleLineComment( "Perform update" );
		writer.emitStatement( "int updateCount = db.update(getTableNameFromUri(uri), values, selection, selectionArgs)" );
		insertNotifyBlock();
		writer.emitEmptyLine();
		writer.emitStatement( "return updateCount" );
		writer.endMethod();
	}


	private void insertNotifyBlock() throws IOException
	{
		writer.emitStatement( "getContext().getContentResolver().notifyChange(uri, null)" );
		writer.emitEmptyLine();
		writer.emitSingleLineComment( "For non-query statements, we also check if we need to notify any view urls. If we update/insert/remove something from a table used by a view, the view must know." );
		writer.emitStatement( "ArrayList<Uri> viewUris = getAssociatedViewUris(uri)" );
		writer.beginControlFlow( "if (viewUris != null)" );
		writer.beginControlFlow( "for (Uri viewUri : viewUris)" );
		writer.emitStatement( "getContext().getContentResolver().notifyChange(viewUri, null)" );
		writer.endControlFlow();
		writer.endControlFlow();
	}
}