package org.bitseal.activities;

import info.guardianproject.cacheword.CacheWordHandler;
import info.guardianproject.cacheword.ICacheWordSubscriber;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;

import org.bitseal.R;
import org.bitseal.services.AppLockHandler;

import android.annotation.SuppressLint;
import android.app.ListActivity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

/**
 * The Activity class for the app's View Log screen. 
 * 
 * @author Jonathan Coe
 */
public class ViewErrorsActivity extends ListActivity implements ICacheWordSubscriber
{
    /** The frequency in milliseconds by which we will update the error list */
    private static final long UPDATE_FREQUENCY_MILLISECONDS = 2000;
    
    /** The maximum number of error items to be displayed */
    private static final int MAXIMUM_ERRORS_TO_DISPLAY = 50;
	
	/** The key for a boolean variable that records whether or not a user-defined database encryption passphrase has been saved */
    private static final String KEY_DATABASE_PASSPHRASE_SAVED = "databasePassphraseSaved"; 
    
    /** The maximum number of lines that we will read from logcat's output */
    private static final int LOGCAT_MAXIMUM_LINES = 2000;
    
    private static final String LOG_LEVEL_ERROR = "E";
    
    private CacheWordHandler mCacheWordHandler;
        
    private ListView mErrorListView;
    private LogAdapter mErrorAdapter;    
    private ArrayList<String> mErrors;
    
    private TimerTask refreshListTask;
    
    private String mLastLine;
    
    private int mProcessID;
    
    private static final String TAG = "VIEW_ERRORS_ACTIVITY";
	
	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_view_errors);
		
		try
		{
			// Check whether the user has set a database encryption passphrase
			SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
			if (prefs.getBoolean(KEY_DATABASE_PASSPHRASE_SAVED, false))
			{
				// Connect to the CacheWordService
				mCacheWordHandler = new CacheWordHandler(this);
				mCacheWordHandler.connectToService();
			}
			
			// Get Bitseal's current process ID
			mProcessID = android.os.Process.myPid();
			
			// Populate a ListView with Bitseal's recent errors
	        mErrorListView = (ListView) findViewById(android.R.id.list);
	        updateListView();
		}
		catch (Exception e)
		{
			Log.e(TAG, "Exception ocurred in ViewErrorsActivity.onCreate(). The exception message was:\n"
					+ e.getMessage());
	    }
	}
	
	@Override
	protected void onPause() 
	{
		super.onPause();
		
		refreshListTask.cancel();
	}
	
	protected void onResume() 
	{
		super.onResume();
		
		// Check for new log lines regularly and update the ListView if any are found
        refreshListTask = new TimerTask()
	    {
	        @Override
	        public void run() 
	        {
	            runOnUiThread(new Runnable()
	            {
	                public void run() 
	                {
	                	if (checkForNewLines())
	                	{
	                		updateListView();
	                	}
	                }
	            });
	        }
	    };
	    new Timer().schedule(refreshListTask, 0, UPDATE_FREQUENCY_MILLISECONDS);
	}
	
	/**
	 * Checks whether there are new errors to be displayed
	 */
	private boolean checkForNewLines()
	{
		try 
		{
            Process process = Runtime.getRuntime().exec("logcat -d");
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line = "";
            
            // Check whether the last line of the error output is new
            String newLastLine = "";
            while ((line = bufferedReader.readLine()) != null)
            {
            	// Get the most recent line of the output we want to display
            	if (filterErrors(line))
            	{
            		newLastLine = line;
            	}
            }
            
            // If the last line is new (i.e. there is new error output), refresh the displayed text
        	if (newLastLine.equals(mLastLine) == false)
        	{
        		return true;
        	}
        	else
        	{
        		return false;
        	}
		}
		catch (Exception e)
		{
			Log.e(TAG, "Exception ocurred in ViewErrorsActivity.checkForNewLines. The exception message was:\n"
					+ e.getMessage());
			return false;
        }
	}
	
	/**
	 * Returns the an ArrayList<String> containing the errors
	 * which should be displayed
	 */
	private ArrayList<String> getErrors()
	{
		try
		{
			// Get the logcat output and prepare to read it
			Process process = Runtime.getRuntime().exec("logcat -d");
			BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
			ArrayList<String> errorLines = new ArrayList<String>();
			String line = "";

			// Count the number of lines in the logcat output
            int lines = 0;
            while ((bufferedReader.readLine()) != null)
            {
            	lines ++;
            }
        	
        	// Create a new BufferedReader so we can read from the start again
            process = Runtime.getRuntime().exec("logcat -d");
    		bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            
            // If there are more lines than we are prepared to process, only process the most recent ones
            int startPoint = lines - LOGCAT_MAXIMUM_LINES;
            if (lines > LOGCAT_MAXIMUM_LINES)
            {
            	// Skip through lines until we are at the correct start point
            	for (int i = 0; i < startPoint && bufferedReader.ready(); bufferedReader.readLine()) { }
            }
        	
            // Read the selected lines
        	while ((line = bufferedReader.readLine()) != null)
	        {
	            // Filter log output by Bitseal's current process number and by removing unwanted lines
	        	if (filterErrors(line))
	        	{
	        		errorLines.add(line);
	            }
	        }
            
            // If there are no lines to display, return a placeholder message
            if (errorLines.size() == 0)
            {
            	errorLines.add(getResources().getString(R.string.activity_view_errors_placeholder_message));
            }
            else
            {
		        // Record the last read line
		        mLastLine = errorLines.get(errorLines.size() - 1);
		        
		        // If the log text is over the maximum number of items, shorten it
		        if (errorLines.size() > MAXIMUM_ERRORS_TO_DISPLAY)
		        {
		        	errorLines = new ArrayList<String>(errorLines.subList(errorLines.size() - MAXIMUM_ERRORS_TO_DISPLAY, errorLines.size()));
		        }
            }
	        
	        return errorLines;
		}
		catch (Exception e)
		{
			Log.e(TAG, "Exception ocurred in ViewErrorsActivity.getErrors(). The exception message was:\n"
					+ e.getMessage());
			ArrayList<String> placeholderList = new ArrayList<String>();
			placeholderList.add(getResources().getString(R.string.activity_view_errors_placeholder_message));
			return placeholderList;
	    }
	}
	
	/**
	 * Filters a log line, returning whether or not is should be included
	 * in the displayed output
	 */
	private boolean filterErrors(String line)
	{
		// Filter log output by Bitseal's current process number
    	if (line.contains(String.valueOf(mProcessID)))
        {
			// Filter log output by Bitseal's current process number
	    	if (line.startsWith(LOG_LEVEL_ERROR))
	        {
	    		return true;
	        }
	    	else
	    	{
	    		return false;
	    	}
        }
    	else
    	{
    		return false;
    	}
	}
	
	/** 
	 * Updates the error log ListView
	 **/
    private void updateListView()
    {
    	// Get the error log lines to display
    	mErrors = getErrors();
    	
		// Save ListView state so that we can resume at the same scroll position
		Parcelable state = mErrorListView.onSaveInstanceState();
		
		// Re-instantiate the ListView and re-populate it
        mErrorListView = new ListView(this);
        mErrorListView = (ListView)findViewById(android.R.id.list);
        mErrorAdapter = new LogAdapter(mErrors);
		mErrorListView.setAdapter(mErrorAdapter);
		
		// Restore previous state (including selected item index and scroll position)
		mErrorListView.onRestoreInstanceState(state);
				
		// If the user has scrolled to the bottom of the ListView, keep scrolling to the
		// bottom as new items are added
		try
		{
			if (mErrorListView.getLastVisiblePosition() == mErrorListView.getAdapter().getCount() -1 &&
					mErrorListView.getChildAt(mErrorListView.getChildCount() - 1).getBottom() <= mErrorListView.getHeight())
			{
				scrollMyListViewToBottom();;
			}
		}
		catch (Exception e)
		{
			Log.e(TAG, "Exception ocurred in ViewErrorsActivity.updateListView(). The exception message was:\n"
					+ e.getMessage());
		}
    }
    
    private void scrollMyListViewToBottom()
    {
    	mErrorListView.post(new Runnable()
        {
            @Override
            public void run()
            {
                // Select the last row so it will scroll into view
            	mErrorListView.setSelection(mErrorAdapter.getCount() - 1);
            }
        });
    }
	
	/**
	 * A ViewHolder used to speed up this activity's ListView.
	 */
    static class ViewHolder 
    {
	    public TextView logLineTextView;
	}
	    
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data)
    {
        ((LogAdapter)mErrorListView.getAdapter()).notifyDataSetChanged();
    }
    
    private class LogAdapter extends ArrayAdapter<String> 
    {
        public LogAdapter(ArrayList<String> logLines) 
        {
            super(getBaseContext(), android.R.layout.simple_list_item_1, logLines);
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) 
        {
        	// If we weren't given a view that can be recycled, inflate a new one
        	if (convertView == null)
            {
        		convertView = getLayoutInflater().inflate(R.layout.list_item_log, parent, false);
        		
        	    // Configure the view holder
        	    ViewHolder viewHolder = new ViewHolder();
        	    viewHolder.logLineTextView = (TextView) convertView.findViewById(R.id.view_log_line_textview);
        	    convertView.setTag(viewHolder);
            }
        	
        	ViewHolder holder = (ViewHolder) convertView.getTag();
        	
            // Get the log line
            final String error = getItem(position);
            holder.logLineTextView.setText(error);
            
            if (error.equals(getResources().getString(R.string.activity_view_errors_placeholder_message)))
            {
            	holder.logLineTextView.setTextColor(Color.BLACK);
            	holder.logLineTextView.setTextSize(18);
            }
            else
            {
            	holder.logLineTextView.setTextColor(Color.RED);
            	
    			convertView.setOnLongClickListener(new View.OnLongClickListener()
    			{
                    @SuppressWarnings("deprecation")
					@SuppressLint("NewApi")
					@Override
                    public boolean onLongClick(View v) 
                    {
                        Log.i(TAG, "Error list item long clicked");
                 	    
                        // Copy the error to the clipboard
        				int sdk = android.os.Build.VERSION.SDK_INT;
        				
        				if(sdk < android.os.Build.VERSION_CODES.HONEYCOMB) 
        				{
        				    android.text.ClipboardManager clipboard = (android.text.ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
        				    clipboard.setText(error);
        				} 
        				
        				else 
        				{
        				    android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); 
        				    android.content.ClipData clip = android.content.ClipData.newPlainText("COPIED_MESSAGE_TEXT", error);
        				    clipboard.setPrimaryClip(clip);
        				}
        				
        				Toast.makeText(getApplicationContext(), R.string.activity_view_errors_error_copied_toast, Toast.LENGTH_LONG).show();
        				
        				// Indicate that we don't want any further processing
        				return true;
                    }
                });
            }
        	
			return convertView;
        }
    }
	
 	@Override
 	public boolean onCreateOptionsMenu(Menu menu) 
 	{
 		// Inflate the menu; this adds items to the action bar if it is present.
 		getMenuInflater().inflate(R.menu.options_menu, menu);
 		return true;
 	}
     
     @Override
     public boolean onPrepareOptionsMenu(Menu menu)
     {
     	SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
     	if (prefs.getBoolean(KEY_DATABASE_PASSPHRASE_SAVED, false) == false)
 		{
 			menu.removeItem(R.id.menu_item_lock);
 		}
         return super.onPrepareOptionsMenu(menu);
     }
 	
 	@SuppressLint("InlinedApi")
 	@Override
 	public boolean onOptionsItemSelected(MenuItem item) 
 	{
 	    switch(item.getItemId()) 
 	    {
 		    case R.id.menu_item_inbox:
 		        Intent intent1 = new Intent(this, InboxActivity.class);
 		        intent1.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
 		        startActivity(intent1);
 		        break;
 		        
 		    case R.id.menu_item_sent:
 		        Intent intent2 = new Intent(this, SentActivity.class);
 		        intent2.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
 		        startActivity(intent2);
 		        break;  
 		        
 		    case R.id.menu_item_compose:
 		        Intent intent3 = new Intent(this, ComposeActivity.class);
 		        intent3.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
 		        startActivity(intent3);
 		        break;
 		        
 		    case R.id.menu_item_identities:
 		        Intent intent4 = new Intent(this, IdentitiesActivity.class);
 		        intent4.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
 		        startActivity(intent4);
 		        break;
 		        
 		    case R.id.menu_item_addressBook:
 		        Intent intent5 = new Intent(this, AddressBookActivity.class);
 		        intent5.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
 		        startActivity(intent5);
 		        break;
 		        
 		    case R.id.menu_item_settings:
 		        Intent intent6 = new Intent(this, SettingsActivity.class);
 		        intent6.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
 		        startActivity(intent6);
 		        break;
 		        
 		    case R.id.menu_item_lock:
		    	AppLockHandler.runLockRoutine(mCacheWordHandler);
		        break;
 		        
 		    default:
 		        return super.onOptionsItemSelected(item);
 	    }

 	    return true;
 	}
     
     @Override
     protected void onStop()
     {
     	super.onStop();
     	if (mCacheWordHandler != null)
     	{
         	mCacheWordHandler.disconnectFromService();
     	}
      }
 	
 	@SuppressLint("InlinedApi")
 	@Override
 	public void onCacheWordLocked()
 	{
 		// Redirect to the lock screen activity
         Intent intent = new Intent(getBaseContext(), LockScreenActivity.class);
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) // FLAG_ACTIVITY_CLEAR_TASK only exists in API 11 and later 
         {
         	intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);// Clear the stack of activities
         }
         else
         {
         	intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         }
         startActivity(intent);
 	}

 	@Override
 	public void onCacheWordOpened()
 	{
 		// Nothing to do here currently
 	}
 	
 	@Override
 	public void onCacheWordUninitialized()
 	{
 		// Database encryption is currently not enabled by default, so there is nothing to do here
 	}
}