/*
 * This file is part of NixNote/NeighborNote 
 * Copyright 2009 Randy Baumgarte
 * Copyright 2013 Yuki Takahashi
 * 
 * This file may be licensed under the terms of of the
 * GNU General Public License Version 2 (the ``GPL'').
 *
 * Software distributed under the License is distributed
 * on an ``AS IS'' basis, WITHOUT WARRANTY OF ANY KIND, either
 * express or implied. See the GPL for the specific language
 * governing rights and limitations.
 *
 * You should have received a copy of the GPL along with this
 * program. If not, go to http://www.gnu.org/licenses/gpl.html
 * or write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
*/
package cx.fbn.nevernote.threads;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.LinkedBlockingQueue;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;

import com.evernote.edam.error.EDAMErrorCode;
import com.evernote.edam.error.EDAMNotFoundException;
import com.evernote.edam.error.EDAMSystemException;
import com.evernote.edam.error.EDAMUserException;
import com.evernote.edam.notestore.NoteStore;
import com.evernote.edam.notestore.NoteStore.Client;
import com.evernote.edam.notestore.SyncChunk;
import com.evernote.edam.notestore.SyncState;
import com.evernote.edam.type.Data;
import com.evernote.edam.type.LinkedNotebook;
import com.evernote.edam.type.Note;
import com.evernote.edam.type.Notebook;
import com.evernote.edam.type.Resource;
import com.evernote.edam.type.SavedSearch;
import com.evernote.edam.type.SharedNotebook;
import com.evernote.edam.type.Tag;
import com.evernote.edam.type.User;
import com.evernote.edam.userstore.AuthenticationResult;
import com.evernote.edam.userstore.UserStore;
import com.evernote.thrift.TException;
import com.evernote.thrift.protocol.TBinaryProtocol;
import com.evernote.thrift.transport.THttpClient;
import com.evernote.thrift.transport.TTransportException;
import com.trolltech.qt.core.QByteArray;
import com.trolltech.qt.core.QFile;
import com.trolltech.qt.core.QIODevice.OpenModeFlag;
import com.trolltech.qt.core.QObject;
import com.trolltech.qt.core.QTextCodec;
import com.trolltech.qt.gui.QMessageBox;

import cx.fbn.nevernote.signals.LimitSignal;
import cx.fbn.nevernote.signals.NoteIndexSignal;
import cx.fbn.nevernote.signals.NoteResourceSignal;
import cx.fbn.nevernote.signals.NoteSignal;
import cx.fbn.nevernote.signals.NotebookSignal;
import cx.fbn.nevernote.signals.SavedSearchSignal;
import cx.fbn.nevernote.signals.StatusSignal;
import cx.fbn.nevernote.signals.SyncSignal;
import cx.fbn.nevernote.signals.TagSignal;
import cx.fbn.nevernote.sql.DatabaseConnection;
import cx.fbn.nevernote.sql.DeletedItemRecord;
import cx.fbn.nevernote.utilities.ApplicationLogger;


public class SyncRunner extends QObject implements Runnable {
	
	private final ApplicationLogger logger;
	private DatabaseConnection 		conn;
	private boolean					idle;
	public boolean 					error;
	public volatile List<String>	errorSharedNotebooks;
	public volatile HashMap<String,String>	errorSharedNotebooksIgnored;
	public volatile boolean			isConnected;
	public volatile boolean	 		keepRunning;
	public volatile String			authToken;
	private long					evernoteUpdateCount;
	private final String userAgent = "NeighborNote/" + System.getProperty("os.name")
							+"/"+System.getProperty("java.vendor") + "/"
							+ System.getProperty("java.version") +";";
	
	public volatile NoteStore.Client 		localNoteStore;
	private UserStore.Client				userStore;
	
	public volatile StatusSignal			status;
	public volatile TagSignal				tagSignal;
	public volatile NotebookSignal			notebookSignal;
	public volatile NoteIndexSignal			noteIndexSignal;
	public volatile NoteSignal				noteSignal;
	public volatile SavedSearchSignal		searchSignal;
	public volatile NoteResourceSignal		resourceSignal;
	public volatile SyncSignal				syncSignal;
	public volatile LimitSignal				limitSignal;
	public volatile boolean					authRefreshNeeded;
	public volatile boolean					syncNeeded;
	public volatile boolean					disableUploads;
	public volatile boolean 				syncDeletedContent;
	private volatile List<String>			dirtyNoteGuids;
	
    public volatile String username = ""; 
    public volatile String password = ""; 
	public volatile String userStoreUrl;
    public String noteStoreUrlBase;
    private THttpClient userStoreTrans;
    private TBinaryProtocol userStoreProt;
    //private AuthenticationResult authResult;
    private AuthenticationResult linkedAuthResult;
    private User user; 
//	    private long authTimeRemaining;
    public long authRefreshTime;
    public long failedRefreshes = 0;
    public  THttpClient noteStoreTrans;
    public TBinaryProtocol noteStoreProt;
    public String noteStoreUrl;
    public long sequenceDate;
    public int updateSequenceNumber;
    private boolean refreshNeeded;
    private volatile LinkedBlockingQueue<String> workQueue;
	private static int MAX_QUEUED_WAITING = 1000;
	String dbuid;
	String dburl;
	String indexUrl;
	String resourceUrl;
	String behaviorUrl;
	
	String dbpswd;
	String dbcpswd;
	private final TreeSet<String> ignoreTags;
	private final TreeSet<String> ignoreNotebooks;
	private final TreeSet<String> ignoreLinkedNotebooks;
	private HashMap<String,String> badTagSync;

	
	public SyncRunner(String logname, String u, String i, String r, String b, String uid, String pswd, String cpswd) {
		logger = new ApplicationLogger(logname);
		
		noteSignal = new NoteSignal();
		status = new StatusSignal();
		tagSignal = new TagSignal();
		notebookSignal = new NotebookSignal();
		noteIndexSignal = new NoteIndexSignal();
		noteSignal = new NoteSignal();
		searchSignal = new SavedSearchSignal();
		syncSignal = new SyncSignal();
		resourceSignal = new NoteResourceSignal();
		limitSignal = new LimitSignal();
		resourceUrl = r;
		indexUrl = i;
		behaviorUrl = b;
		
		dbuid = uid;
		dburl = u;
		dbpswd = pswd;
		dbcpswd = cpswd;
//		this.setAutoDelete(false);
		
		isConnected = false;
		syncNeeded = false;
		authRefreshNeeded = false;
		keepRunning = true;
		idle = true;
		disableUploads = false;
		ignoreTags = new TreeSet<String>();
		ignoreNotebooks = new TreeSet<String>();
		ignoreLinkedNotebooks = new TreeSet<String>();
		
//		setAutoDelete(false);
		workQueue=new LinkedBlockingQueue<String>(MAX_QUEUED_WAITING);
	}
	@Override
	public void run() {
		errorSharedNotebooks = new ArrayList<String>();
		errorSharedNotebooksIgnored = new HashMap<String,String>();
		try {
			logger.log(logger.EXTREME, "Starting thread");
			conn = new DatabaseConnection(logger, dburl, indexUrl, resourceUrl, behaviorUrl, dbuid, dbpswd, dbcpswd, 200);
			while(keepRunning) {
				logger.log(logger.EXTREME, "Blocking until work is found");
				String work = workQueue.take();
				logger.log(logger.LOW, "Dirty Notes Before Sync: " +new Integer(conn.getNoteTable().getDirtyCount()).toString());
				logger.log(logger.EXTREME, "Work found: " +work);
				if (work.equalsIgnoreCase("stop")) {
					idle=false;
					return;
				}
				conn.getNoteTable().dumpDirtyNotes();  // Debugging statement
				idle=false;
				error=false;
				if (syncNeeded) {
					logger.log(logger.EXTREME, "SyncNeeded is true");
					refreshNeeded=false;
					sequenceDate = conn.getSyncTable().getLastSequenceDate();
					updateSequenceNumber = conn.getSyncTable().getUpdateSequenceNumber();
					try {
						logger.log(logger.EXTREME, "Beginning sync");
						evernoteSync(localNoteStore);
						logger.log(logger.EXTREME, "Sync finished");
					} catch (UnknownHostException e) {
						status.message.emit(e.getMessage());
					}
				}
				idle=true;
				logger.log(logger.EXTREME, "Signaling refresh finished.  refreshNeeded=" +refreshNeeded);
				syncSignal.finished.emit(refreshNeeded);
				if (error) {
					syncSignal.errorDisconnect.emit();
					status.message.emit(tr("Error synchronizing - see log for details."));
				}
				logger.log(logger.LOW, "Dirty Notes After Sync: " +new Integer(conn.getNoteTable().getDirtyCount()).toString());
				conn.getNoteTable().dumpDirtyNotes();
				logger.log(logger.LOW, "---");
			}
		}	
		catch (InterruptedException e1) {
			e1.printStackTrace();
		}
		conn.dbShutdown();
	}

	
	public DatabaseConnection getConnection() {
		return conn;
	}

	public boolean isIdle() {
		return idle;
	}


	public void setConnected(boolean c) {
		isConnected = c;
	}
	public void setKeepRunning(boolean r) {
		logger.log(logger.EXTREME, "Setting keepRunning=" +r);
		keepRunning = r;
	}
	public void setNoteStore(NoteStore.Client c) {
		logger.log(logger.EXTREME, "Setting NoteStore in sync thread");
		localNoteStore = c;
	}
	public void setUserStore(UserStore.Client c) {
		logger.log(logger.EXTREME, "Setting UserStore in sync thread");
		userStore = c;
	}

	public void setEvernoteUpdateCount(long s) {
		logger.log(logger.EXTREME, "Setting Update Count in sync thread");
		evernoteUpdateCount = s;
	}
	
	//***************************************************************
    //***************************************************************
    //** These functions deal with Evernote communications
    //***************************************************************
    //***************************************************************
	// Synchronize changes with Evernote
	@SuppressWarnings("unused")
	private void evernoteSync(Client noteStore) throws java.net.UnknownHostException {
		logger.log(logger.HIGH, "Entering SyncRunner.evernoteSync");
		
		// Rebuild list of tags & notebooks to ignore
		ignoreNotebooks.clear();
		List<String> ignore = conn.getSyncTable().getIgnoreRecords("NOTEBOOK");
		for (int i=0; i<ignore.size(); i++) 
			ignoreNotebooks.add(ignore.get(i));
		
		ignore.clear();
		ignore = conn.getSyncTable().getIgnoreRecords("LINKEDNOTEBOOK");
		for (int i=0; i<ignore.size(); i++) 
			ignoreLinkedNotebooks.add(ignore.get(i));
		
		ignoreTags.clear();
		ignore = conn.getSyncTable().getIgnoreRecords("TAG");
		for (int i=0; i<ignore.size(); i++) 
			ignoreTags.add(ignore.get(i));

		// Make sure we are connected & should keep running
		if (isConnected && keepRunning) {
			error = false;
			logger.log(logger.EXTREME, "Synchronizing with Evernote");
			status.message.emit(tr("Synchronizing with Evernote"));
			
			// Get user information
			try {
				logger.log(logger.EXTREME, "getting user from userstore");
				User user = userStore.getUser(authToken);
				logger.log(logger.EXTREME, "Saving user information");
				syncSignal.saveUserInformation.emit(user);
			} catch (EDAMUserException e1) {
				e1.printStackTrace();
				status.message.emit(tr("User exception getting user account information.  Aborting sync and disconnecting"));
				syncSignal.errorDisconnect.emit();
				error = true;
				enDisconnect();
				return;
			} catch (EDAMSystemException e1) {
				if (e1.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
					limitSignal.rateLimitReached.emit(e1.getRateLimitDuration());
				}
				
				e1.printStackTrace();
				status.message.emit(tr("System error user account information.  Aborting sync and disconnecting!"));
				syncSignal.errorDisconnect.emit();
				error = true;
				enDisconnect();
				return;
			} catch (TException e1) {
				e1.printStackTrace();
				syncSignal.errorDisconnect.emit();
				error = true;
				status.message.emit(tr("Transaction error getting user account information.  Aborting sync and disconnecting!"));
				enDisconnect();
				return;
			}
			
			// Get sync state
			SyncState syncState = null;
			try {	
				logger.log(logger.EXTREME, "Getting sync state");
				syncState = noteStore.getSyncState(authToken);  
				syncSignal.saveUploadAmount.emit(syncState.getUploaded());
				syncSignal.saveEvernoteUpdateCount.emit(syncState.getUpdateCount());
				evernoteUpdateCount = syncState.getUpdateCount();
			} catch (EDAMUserException e) {
				e.printStackTrace();
				status.message.emit(tr("Error getting sync state! Aborting sync and disconnecting!"));
				syncSignal.errorDisconnect.emit();
				enDisconnect();
				return;
			} catch (EDAMSystemException e) {
				if (e.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
					limitSignal.rateLimitReached.emit(e.getRateLimitDuration());
				}
				e.printStackTrace();
				status.message.emit(tr("Error getting sync state! Aborting sync and disconnecting!"));
				syncSignal.errorDisconnect.emit();
				enDisconnect();
				return;
			} catch (TException e) {
				e.printStackTrace();
				status.message.emit(tr("Error getting sync state! Aborting sync and disconnecting!"));
				syncSignal.errorDisconnect.emit();
				enDisconnect();
				return;
			}
			
			if (syncState == null) {
				logger.log(logger.EXTREME, "Sync State is null");
				status.message.emit(tr("Syncronization Error!"));
				return;
			}

			// Determine what to do. 
			// If we need to do a full sync.
			logger.log(logger.LOW, "Full Sequence Before: " +syncState.getFullSyncBefore());
			logger.log(logger.LOW, "Last Sequence Date: " +sequenceDate);
			logger.log(logger.LOW, "Var Last Sequence Number: " +updateSequenceNumber);
			logger.log(logger.LOW, "DB Last Sequence Number: " + conn.getSyncTable().getUpdateSequenceNumber());
			if (syncState.getFullSyncBefore() > sequenceDate) {
				logger.log(logger.EXTREME, "Full sequence date has expired");
				sequenceDate = 0;
				conn.getSyncTable().setLastSequenceDate(0);
				updateSequenceNumber = 0;
				conn.getSyncTable().setUpdateSequenceNumber(0);
			}
			// Check for "special" sync instructions
			String syncLinked = conn.getSyncTable().getRecord("FullLinkedNotebookSync");
			String syncShared = conn.getSyncTable().getRecord("FullSharedNotebookSync");
			String syncNotebooks = conn.getSyncTable().getRecord("FullNotebookSync");
			String syncInkNoteImages = conn.getSyncTable().getRecord("FullInkNoteImageSync");
			if (syncLinked != null) {
				downloadAllLinkedNotebooks(localNoteStore);
			}
			if (syncShared != null) {
				downloadAllSharedNotebooks(localNoteStore);
			}
			if (syncNotebooks != null) {
				downloadAllNotebooks(localNoteStore);
			}
			
			if (syncInkNoteImages != null) {
				List<String> guids = conn.getNoteTable().noteResourceTable.findInkNotes();
				for (int i=0; i<guids.size(); i++) {
					downloadInkNoteImage(guids.get(i), authToken);
				}
				conn.getSyncTable().deleteRecord("FullInkNoteImageSync");
			}
			
			// If there are remote changes
			logger.log(logger.LOW, "Update Count: " +syncState.getUpdateCount());
			logger.log(logger.LOW, "Last Update Count: " +updateSequenceNumber);
			
			if (syncState.getUpdateCount() > updateSequenceNumber) {
				logger.log(logger.EXTREME, "Refresh needed is true");
				refreshNeeded = true;
				logger.log(logger.EXTREME, "Downloading changes");
				syncRemoteToLocal(localNoteStore);
			}
			
			//*****************************************
			//* Sync linked/shared notebooks 
			//*****************************************
			syncLinkedNotebooks();
			//conn.getNoteTable().getDirty();
			//disableUploads = true;   /// DELETE THIS LINE!!!!
			if (!disableUploads) {
				logger.log(logger.EXTREME, "Uploading changes");
				// Synchronize remote changes
				if (!error)
					syncExpunged(localNoteStore);
				if (!error)
					syncLocalTags(localNoteStore);
				if (!error)
					syncLocalNotebooks(localNoteStore);
				if (!error)
					syncLocalLinkedNotebooks(localNoteStore);
				if (!error) 
					syncDeletedNotes(localNoteStore);
				if (!error)
					syncLocalNotes();
				if (!error)
					syncLocalSavedSearches(localNoteStore);
			}
			
			status.message.emit(tr("Cleaning up"));
			List<String> notes = conn.getNoteTable().expungeIgnoreSynchronizedNotes(conn.getSyncTable().getIgnoreRecords("NOTEBOOK"), 
					conn.getSyncTable().getIgnoreRecords("TAG"), conn.getSyncTable().getIgnoreRecords("LINKEDNOTEBOOK"));
			if (notes.size() > 0)
				syncSignal.refreshLists.emit();
			
			//*****************************************
			//* End of synchronization
			//*****************************************
			if (refreshNeeded)
				syncSignal.refreshLists.emit();
			
			if (!error) {
				logger.log(logger.LOW, "Sync completed.  Errors=" +error);
				if (!disableUploads) 
					status.message.emit(tr("Synchronizing complete"));
				else
					status.message.emit(tr("Download syncronization complete.  Uploads have been disabled."));
				
				logger.log(logger.EXTREME, "Saving sync time");
				if (syncState.getCurrentTime() > sequenceDate)
					sequenceDate = syncState.getCurrentTime();
				if (syncState.getUpdateCount() > updateSequenceNumber)
					updateSequenceNumber = syncState.getUpdateCount();
				conn.getSyncTable().setLastSequenceDate(sequenceDate);
				conn.getSyncTable().setUpdateSequenceNumber(updateSequenceNumber);
			}
		}
		logger.log(logger.HIGH, "Leaving SyncRunner.evernoteSync");
	}
	
	// Sync deleted items with Evernote
	private void syncExpunged(Client noteStore) {
		logger.log(logger.HIGH, "Entering SyncRunner.syncExpunged");
		
		List<DeletedItemRecord> expunged = conn.getDeletedTable().getAllDeleted();
 		boolean error = false;
		for (int i=0; i<expunged.size() && keepRunning; i++) {

//			if (authRefreshNeeded)
//				if (!refreshConnection())
//					return;

			try {
				if (expunged.get(i).type.equalsIgnoreCase("TAG")) {
					logger.log(logger.EXTREME, "Tag expunged");
					conn.getDeletedTable().expungeDeletedItem(expunged.get(i).guid, "TAG");	
					updateSequenceNumber = noteStore.expungeTag(authToken, expunged.get(i).guid);
					conn.getSyncTable().setUpdateSequenceNumber(updateSequenceNumber);
					conn.getSyncTable().setLastSequenceDate(sequenceDate);
					conn.getSyncTable().setUpdateSequenceNumber(updateSequenceNumber);				
				}
				if 	(expunged.get(i).type.equalsIgnoreCase("NOTEBOOK")) {
					logger.log(logger.EXTREME, "Notebook expunged");
					conn.getDeletedTable().expungeDeletedItem(expunged.get(i).guid, "NOTEBOOK");
					updateSequenceNumber = noteStore.expungeNotebook(authToken, expunged.get(i).guid);
					conn.getSyncTable().setLastSequenceDate(sequenceDate);
					conn.getSyncTable().setUpdateSequenceNumber(updateSequenceNumber);
				}
				if (expunged.get(i).type.equalsIgnoreCase("NOTE")) {
					logger.log(logger.EXTREME, "Note expunged");
					conn.getDeletedTable().expungeDeletedItem(expunged.get(i).guid, "NOTE");
					updateSequenceNumber = noteStore.deleteNote(authToken, expunged.get(i).guid);
					refreshNeeded = true;
					conn.getSyncTable().setLastSequenceDate(sequenceDate);
					conn.getSyncTable().setUpdateSequenceNumber(updateSequenceNumber);
				}
				if (expunged.get(i).type.equalsIgnoreCase("SAVEDSEARCH")) {
					logger.log(logger.EXTREME, "saved search expunged");
					conn.getDeletedTable().expungeDeletedItem(expunged.get(i).guid, "SAVEDSEARCH");
					updateSequenceNumber = noteStore.expungeSearch(authToken, expunged.get(i).guid);
					conn.getSyncTable().setLastSequenceDate(sequenceDate);
					conn.getSyncTable().setUpdateSequenceNumber(updateSequenceNumber);
				}
			} catch (EDAMUserException e) {
				logger.log(logger.LOW, "EDAM User Excepton in syncExpunged: " +expunged.get(i).guid);   // This can happen if we try to delete a deleted note
			} catch (EDAMSystemException e) {
				if (e.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
					limitSignal.rateLimitReached.emit(e.getRateLimitDuration());
				}
				logger.log(logger.LOW, "EDAM System Excepton in syncExpunged: "+expunged.get(i).guid);
				logger.log(logger.LOW, e.getStackTrace());
				error=true;
			} catch (EDAMNotFoundException e) {
				logger.log(logger.LOW, "EDAM Not Found Excepton in syncExpunged: "+expunged.get(i).guid);
			} catch (TException e) {
				logger.log(logger.LOW, "EDAM TExcepton in syncExpunged: "+expunged.get(i).guid);
				logger.log(logger.LOW, e.getStackTrace());
				error=true;
			}
		}
		if (!error)
			conn.getDeletedTable().expungeAllDeletedRecords();
		
		logger.log(logger.HIGH, "Leaving SyncRunner.syncExpunged");

	}
	private void syncDeletedNotes(Client noteStore) {
		if (syncDeletedContent)
			return;
		logger.log(logger.HIGH, "Entering SyncRunner.syncDeletedNotes");
		status.message.emit(tr("Synchronizing deleted notes."));

		List<Note> notes = conn.getNoteTable().getDirty();
		// Sync the local notebooks with Evernote's
		for (int i=0; i<notes.size() && keepRunning; i++) {
			
//			if (authRefreshNeeded)
//				if (!refreshConnection())
//					return;
			
			Note enNote = notes.get(i);
			try {
				if (enNote.getUpdateSequenceNum() > 0 && (enNote.isActive() == false || enNote.getDeleted() > 0)) {
					// Check that the note is valid.  
					if (enNote.isActive() == true || enNote.getDeleted() == 0) {
						conn.getNoteTable().deleteNote(enNote.getGuid());
						enNote = conn.getNoteTable().getNote(enNote.getGuid(), false, false, false, false, false);
					}
					if (syncDeletedContent) {
						logger.log(logger.EXTREME, "Deleted note found & synch content selected");
						Note delNote = conn.getNoteTable().getNote(enNote.getGuid(), true, true, true, true, true);
						delNote = getNoteContent(delNote);
						delNote = noteStore.updateNote(authToken, delNote);
						enNote.setUpdateSequenceNum(delNote.getUpdateSequenceNum());
						conn.getNoteTable().updateNoteSequence(enNote.getGuid(), enNote.getUpdateSequenceNum());
					} else {
						logger.log(logger.EXTREME, "Deleted note found & sync content not selected");
						int usn = noteStore.deleteNote(authToken, enNote.getGuid());
						enNote.setUpdateSequenceNum(usn);
						conn.getNoteTable().updateNoteSequence(enNote.getGuid(), enNote.getUpdateSequenceNum());						
					}
					logger.log(logger.EXTREME, "Resetting deleted dirty flag");
					conn.getNoteTable().resetDirtyFlag(enNote.getGuid());
					updateSequenceNumber = enNote.getUpdateSequenceNum();
					logger.log(logger.EXTREME, "Saving sequence number");
					conn.getSyncTable().setUpdateSequenceNumber(updateSequenceNumber);
				}				
			} catch (EDAMUserException e) {
				logger.log(logger.LOW, "*** EDAM User Excepton syncLocalNotes "+e);
				//status.message.emit("Error sending local note: " +e.getParameter());
				//logger.log(logger.LOW, e.toString());	
				//error = true;
			} catch (EDAMSystemException e) {
				if (e.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
					limitSignal.rateLimitReached.emit(e.getRateLimitDuration());
				}
				logger.log(logger.LOW, "*** EDAM System Excepton syncLocalNotes "+e);
				status.message.emit(tr("Error: ") +e);
				logger.log(logger.LOW, e.toString());		
				error = true;
			} catch (EDAMNotFoundException e) {
				logger.log(logger.LOW, "*** EDAM Not Found Excepton syncLocalNotes " +e);
				//status.message.emit("Error deleting local note: " +e +" - Continuing");
				//logger.log(logger.LOW, e.toString());		
				//error = true;
			} catch (TException e) {
				logger.log(logger.LOW, "*** EDAM TExcepton syncLocalNotes "+e);
				status.message.emit(tr("Error sending local note: ") +e);
				logger.log(logger.LOW, e.toString());	
				error = true;
			}		
		}
	}
	// Sync notes with Evernote
	private void syncLocalNotes() {
		logger.log(logger.HIGH, "Entering SyncRunner.syncNotes");
		logger.log(logger.LOW, "Dirty local notes found: " +new Integer(conn.getNoteTable().getDirtyCount()).toString());
		status.message.emit(tr("Sending local notes."));

		List<Note> notes = conn.getNoteTable().getDirty();
		// Sync the local notebooks with Evernote's
		for (int i=0; i<notes.size() && keepRunning; i++) {
			syncLocalNote(localNoteStore, notes.get(i), authToken);
		}
		logger.log(logger.HIGH, "Leaving SyncRunner.syncNotes");

	}
	// Sync notes with Evernote
	private void syncLocalNote(Client noteStore, Note enNote, String token) {
		logger.log(logger.HIGH, "Entering SyncRunner.syncNotes");
		status.message.emit(tr("Sending local notes."));
			
		if (enNote.isActive()) {
			try {
				if (enNote.getUpdateSequenceNum() > 0) {
					logger.log(logger.EXTREME, "Active dirty note found - non new - " +enNote.getGuid());
					logger.log(logger.EXTREME, "Fetching note content");
					enNote = getNoteContent(enNote);
					logger.log(logger.MEDIUM, "Updating note : "+ enNote.getGuid() +" <title>" +enNote.getTitle()+"</title>");
					enNote = noteStore.updateNote(token, enNote);
				} else { 
					logger.log(logger.EXTREME, "Active dirty found - new note " +enNote.getGuid());
					String oldGuid = enNote.getGuid();
					logger.log(logger.MEDIUM, "Fetching note content");
					enNote = getNoteContent(enNote);
					logger.log(logger.MEDIUM, "Creating note : "+ enNote.getGuid() +" <title>" +enNote.getTitle()+"</title>");
					enNote = noteStore.createNote(token, enNote);
					logger.log(logger.MEDIUM, "New note Guid : "+ enNote.getGuid() +" <title>" +enNote.getTitle()+"</title>");
					noteSignal.guidChanged.emit(oldGuid, enNote.getGuid());
					conn.getNoteTable().updateNoteGuid(oldGuid, enNote.getGuid());
				}
				updateSequenceNumber = enNote.getUpdateSequenceNum();
				logger.log(logger.EXTREME, "Saving note");
				conn.getNoteTable().updateNoteSequence(enNote.getGuid(), enNote.getUpdateSequenceNum());
				List<Resource> rl = enNote.getResources();
				logger.log(logger.EXTREME, "Getting note resources");
				for (int j=0; j<enNote.getResourcesSize() && keepRunning; j++) {
					Resource newRes = rl.get(j);
					Data d = newRes.getData();
					if (d!=null) {	
						logger.log(logger.EXTREME, "Calculating resource hash");
						String hash = byteArrayToHexString(d.getBodyHash());
						logger.log(logger.EXTREME, "updating resources by hash");
						String oldGuid = conn.getNoteTable().noteResourceTable.getNoteResourceGuidByHashHex(enNote.getGuid(), hash);
						conn.getNoteTable().updateNoteResourceGuidbyHash(enNote.getGuid(), newRes.getGuid(), hash);
						resourceSignal.resourceGuidChanged.emit(enNote.getGuid(), oldGuid, newRes.getGuid());
					}
				}
				logger.log(logger.EXTREME, "Resetting note dirty flag");
				conn.getNoteTable().resetDirtyFlag(enNote.getGuid());
				updateSequenceNumber = enNote.getUpdateSequenceNum();
				logger.log(logger.EXTREME, "Emitting note sequence number change");
				conn.getSyncTable().setUpdateSequenceNumber(updateSequenceNumber);

			} catch (EDAMUserException e) {
				logger.log(logger.LOW, "*** EDAM User Excepton syncLocalNotes "+e);
				status.message.emit(tr("Error sending local note: ")	 +e.getParameter());
				logger.log(logger.LOW, e.toString());	
				error = true;
			} catch (EDAMSystemException e) {
				if (e.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
					limitSignal.rateLimitReached.emit(e.getRateLimitDuration());
				}
				logger.log(logger.LOW, "*** EDAM System Excepton syncLocalNotes "+e);
				status.message.emit(tr("Error: ") +e);
				logger.log(logger.LOW, e.toString());		
				error = true;
			} catch (EDAMNotFoundException e) {
				logger.log(logger.LOW, "*** EDAM Not Found Excepton syncLocalNotes " +e);
				status.message.emit(tr("Error sending local note: ") +e);
				logger.log(logger.LOW, e.toString());	
				error = true;
			} catch (TException e) {
				logger.log(logger.LOW, "*** EDAM TExcepton syncLocalNotes "+e);
				status.message.emit(tr("Error sending local note: ") +e);
				logger.log(logger.LOW, e.toString());	
				error = true;
			}
		}
		logger.log(logger.HIGH, "Leaving SyncRunner.syncLocalNote");

	}

	// Sync Notebooks with Evernote
	private void syncLocalNotebooks(Client noteStore) {
		logger.log(logger.HIGH, "Entering SyncRunner.syncLocalNotebooks");
		
		status.message.emit(tr("Sending local notebooks."));
		List<Notebook> remoteList = new ArrayList<Notebook>();
		try {
			logger.log(logger.EXTREME, "Getting remote notebooks to compare with local");
			remoteList = noteStore.listNotebooks(authToken);
		} catch (EDAMUserException e1) {
			logger.log(logger.LOW, "*** EDAM User Excepton syncLocalNotebooks getting remote Notebook List");
			status.message.emit(tr("Error: ") +e1);
			logger.log(logger.LOW, e1.toString());		
			error = true;
		} catch (EDAMSystemException e1) {
			if (e1.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
				limitSignal.rateLimitReached.emit(e1.getRateLimitDuration());
			}
			logger.log(logger.LOW, "*** EDAM System Excepton syncLocalNotebooks getting remote Notebook List");
			status.message.emit(tr("Error: ") +e1);
			logger.log(logger.LOW, e1.toString());	
			error = true;
		} catch (TException e1) {
			logger.log(logger.LOW, "*** EDAM Transaction Excepton syncLocalNotebooks getting remote Notebook List");
			status.message.emit(tr("Error: ") +e1);
			logger.log(logger.LOW, e1.toString());	
			error = true;
		}
		logger.log(logger.EXTREME, "Getting local dirty notebooks");
		List<Notebook> notebooks = conn.getNotebookTable().getDirty();
		int sequence;
		// Sync the local notebooks with Evernote's
		for (int i=0; i<notebooks.size() && keepRunning; i++) {
			
//			if (authRefreshNeeded)
//				if (!refreshConnection())
//					return;
			
			Notebook enNotebook = notebooks.get(i);
			try {
				if (enNotebook.getUpdateSequenceNum() > 0) {
					logger.log(logger.EXTREME, "Existing notebook is dirty");
					sequence = noteStore.updateNotebook(authToken, enNotebook);
				} else {
					logger.log(logger.EXTREME, "New dirty notebook found");
					String oldGuid = enNotebook.getGuid();
					boolean found = false;
					
					// Look for a notebook with the same name.  If one is found, we don't need 
					// to create another one
					logger.log(logger.EXTREME, "Looking for matching notebook name");
					for (int k=0; k<remoteList.size() && !found && keepRunning; k++) {
						if (remoteList.get(k).getName().equalsIgnoreCase(enNotebook.getName())) {
							enNotebook = remoteList.get(k);
							logger.log(logger.EXTREME, "Matching notebook found");
							found = true;
						}
					}
					if (!found)
						enNotebook = noteStore.createNotebook(authToken, enNotebook);
					
					logger.log(logger.EXTREME, "Updating notebook in database");
					conn.getNotebookTable().updateNotebookGuid(oldGuid, enNotebook.getGuid());
					sequence = enNotebook.getUpdateSequenceNum();
				}
				logger.log(logger.EXTREME, "Updating notebook sequence in database");
				conn.getNotebookTable().updateNotebookSequence(enNotebook.getGuid(), sequence);
				logger.log(logger.EXTREME, "Resetting dirty flag in notebook");
				conn.getNotebookTable().resetDirtyFlag(enNotebook.getGuid());
				updateSequenceNumber = sequence;
				logger.log(logger.EXTREME, "Emitting sequence number to main thread");
				conn.getSyncTable().setUpdateSequenceNumber(updateSequenceNumber);
			} catch (EDAMUserException e) {
				logger.log(logger.LOW, "*** EDAM User Excepton syncLocalNotebooks");
				logger.log(logger.LOW, e.toString() + ": Stack : " +enNotebook.getStack());	
				error = true;
			} catch (EDAMSystemException e) {
				if (e.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
					limitSignal.rateLimitReached.emit(e.getRateLimitDuration());
				}
				logger.log(logger.LOW, "*** EDAM System Excepton syncLocalNotebooks");
				logger.log(logger.LOW, e.toString());		
				error = true;
			} catch (EDAMNotFoundException e) {
				logger.log(logger.LOW, "*** EDAM Not Found Excepton syncLocalNotebooks");
				logger.log(logger.LOW, e.toString());		
				error = true;
			} catch (TException e) {
				logger.log(logger.LOW, "*** EDAM TExcepton syncLocalNotebooks");
				logger.log(logger.LOW, e.toString());	
				error = true;
			}		
		}
		logger.log(logger.HIGH, "Leaving SyncRunner.syncLocalNotebooks");

	}
	// Sync Tags with Evernote
	private void syncLocalTags(Client noteStore) {
		logger.log(logger.HIGH, "Entering SyncRunner.syncLocalTags");
		List<Tag> remoteList = new ArrayList<Tag>();
		status.message.emit(tr("Sending local tags."));
		
		try {
			logger.log(logger.EXTREME, "Getting remote tags to compare names with the local tags");
			remoteList = noteStore.listTags(authToken);
		} catch (EDAMUserException e1) {
			logger.log(logger.LOW, "*** EDAM User Excepton syncLocalTags getting remote Tag List");
			status.message.emit(tr("Error: ") +e1);
			logger.log(logger.LOW, e1.toString());	
			error = true;
		} catch (EDAMSystemException e1) {
			if (e1.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
				limitSignal.rateLimitReached.emit(e1.getRateLimitDuration());
			}
			logger.log(logger.LOW, "*** EDAM System Excepton syncLocalTags getting remote Tag List");
			status.message.emit(tr("Error: ") +e1);
			logger.log(logger.LOW, e1.toString());		
			error = true;
		} catch (TException e1) {
			logger.log(logger.LOW, "*** EDAM Transaction Excepton syncLocalTags getting remote Tag List");
			status.message.emit(tr("Error: ") +e1);
			logger.log(logger.LOW, e1.toString());	
			error = true;
		}		
		
		int sequence;
		
		if (badTagSync == null)
			badTagSync = new HashMap<String,String>();
		else
			badTagSync.clear();
		
		Tag enTag = findNextTag();
		
		// This is a hack.  Sometimes this function goes flookey and goes into a 
		// perpetual loop.  This causes  NeverNote to flood Evernote's servers.
		// This is a safety valve to prevent unlimited loops.
		int maxCount = conn.getTagTable().getDirty().size()+10;
		int loopCount = 0;
		
		while(enTag!=null && loopCount < maxCount) {
			loopCount++;
//			if (authRefreshNeeded)
//				if (!refreshConnection())
//					return;

			try {
				if (enTag.getUpdateSequenceNum() > 0) {
					logger.log(logger.EXTREME, "Updating tag");
					sequence = noteStore.updateTag(authToken, enTag);
				} else {
					
					// Look for a tag with the same name.  If one is found, we don't need 
					// to create another one
					logger.log(logger.EXTREME, "New tag.  Comparing with remote names");
					boolean found = false;
					String oldGuid = enTag.getGuid();
					for (int k=0; k<remoteList.size() && !found && keepRunning; k++) {
						if (remoteList.get(k).getName().equalsIgnoreCase(enTag.getName())) {
							conn.getTagTable().updateTagGuid(enTag.getGuid(), remoteList.get(k).getGuid());
							enTag = remoteList.get(k);
							logger.log(logger.EXTREME, "Matching tag name found");
							found = true;
						}
					}
					if (!found)
						enTag = noteStore.createTag(authToken, enTag);
					else
						enTag.setUpdateSequenceNum(noteStore.updateTag(authToken,enTag));
					sequence = enTag.getUpdateSequenceNum();
					if (!oldGuid.equals(enTag.getGuid())) {
						logger.log(logger.EXTREME, "Updating tag guid");
						conn.getTagTable().updateTagGuid(oldGuid, enTag.getGuid());
					}
				}
				logger.log(logger.EXTREME, "Updating tag sequence number");
				conn.getTagTable().updateTagSequence(enTag.getGuid(), sequence);
				logger.log(logger.EXTREME, "Resetting tag dirty flag");
				conn.getTagTable().resetDirtyFlag(enTag.getGuid());
				logger.log(logger.EXTREME, "Emitting sequence number to the main thread.");
				updateSequenceNumber = sequence;
				conn.getSyncTable().setUpdateSequenceNumber(updateSequenceNumber);
			} catch (EDAMUserException e) {
				logger.log(logger.LOW, "*** EDAM User Excepton syncLocalTags: " +enTag.getName());
				logger.log(logger.LOW, e.toString());
				badTagSync.put(enTag.getGuid(),null);
				error = true;
			} catch (EDAMSystemException e) {
				if (e.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
					limitSignal.rateLimitReached.emit(e.getRateLimitDuration());
				}
				logger.log(logger.LOW, "** EDAM System Excepton syncLocalTags: " +enTag.getName());
				logger.log(logger.LOW, e.toString());	
				badTagSync.put(enTag.getGuid(),null);
				error = true;
			} catch (EDAMNotFoundException e) {
				logger.log(logger.LOW, "*** EDAM Not Found Excepton syncLocalTags: " +enTag.getName());
				logger.log(logger.LOW, e.toString());
				badTagSync.put(enTag.getGuid(),null);
				error = true;
			} catch (TException e) {
				logger.log(logger.LOW, "*** EDAM TExcepton syncLocalTags: " +enTag.getName());
				logger.log(logger.LOW, e.toString());
				badTagSync.put(enTag.getGuid(),null);
				error = true;
			}	
			
			// Find the next tag
			logger.log(logger.EXTREME, "Finding next tag");
			enTag = findNextTag();
		}
		logger.log(logger.HIGH, "Leaving SyncRunner.syncLocalTags");
	}
	private void syncLocalLinkedNotebooks(Client noteStore) {
		logger.log(logger.HIGH, "Entering SyncRunner.syncLocalLinkedNotebooks");
		
		List<String> list = conn.getLinkedNotebookTable().getDirtyGuids();
		for (int i=0; i<list.size(); i++) {
			LinkedNotebook book = conn.getLinkedNotebookTable().getNotebook(list.get(i));
			try {
				noteStore.updateLinkedNotebook(authToken, book);
			} catch (EDAMUserException e) {
				logger.log(logger.LOW, "*** EDAM User Excepton syncLocalLinkedNotebooks");
				status.message.emit(tr("Error: ") +e);
				logger.log(logger.LOW, e.toString());		
				error = true;
				e.printStackTrace();
			} catch (EDAMNotFoundException e) {
				logger.log(logger.LOW, "*** EDAM Not Found Excepton syncLocalLinkedNotebooks");
				status.message.emit(tr("Error: ") +e);
				logger.log(logger.LOW, e.toString());		
				error = true;
				e.printStackTrace();
			} catch (EDAMSystemException e) {
				if (e.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
					limitSignal.rateLimitReached.emit(e.getRateLimitDuration());
				}
				logger.log(logger.LOW, "*** EDAM System Excepton syncLocalLinkedNotebooks");
				status.message.emit(tr("Error: ") +e);
				logger.log(logger.LOW, e.toString());		
				error = true;
				e.printStackTrace();
			} catch (TException e) {
				logger.log(logger.LOW, "*** EDAM TExcepton syncLocalLinkedNotebooks");
				status.message.emit(tr("Error: ") +e);
				logger.log(logger.LOW, e.toString());		
				error = true;
				e.printStackTrace();
			}
		}
		logger.log(logger.HIGH, "Leaving SyncRunner.syncLocalLinkedNotebooks");
	}
	// Sync Saved Searches with Evernote
	private void syncLocalSavedSearches(Client noteStore) {
		logger.log(logger.HIGH, "Entering SyncRunner.syncLocalSavedSearches");
		List<SavedSearch> remoteList = new ArrayList<SavedSearch>();
		status.message.emit(tr("Sending saved searches."));
	
		logger.log(logger.EXTREME, "Getting saved searches to compare with local");
		try {
			remoteList = noteStore.listSearches(authToken);
		} catch (EDAMUserException e1) {
			logger.log(logger.LOW, "*** EDAM User Excepton syncLocalTags getting remote saved search List");
			status.message.emit(tr("Error: ") +e1);
			logger.log(logger.LOW, e1.toString());	
			error = true;
		} catch (EDAMSystemException e1) {
			if (e1.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
				limitSignal.rateLimitReached.emit(e1.getRateLimitDuration());
			}
			logger.log(logger.LOW, "*** EDAM System Excepton syncLocalTags getting remote saved search List");
			status.message.emit(tr("Error: ") +e1);
			logger.log(logger.LOW, e1.toString());		
			error = true;
		} catch (TException e1) {
			logger.log(logger.LOW, "*** EDAM Transaction Excepton syncLocalTags getting remote saved search List");
			status.message.emit(tr("Error: ") +e1);
			logger.log(logger.LOW, e1.toString());	
			error = true;
		}		
		
		List<SavedSearch> searches = conn.getSavedSearchTable().getDirty();
		int sequence;
		// Sync the local notebooks with Evernote's
		logger.log(logger.EXTREME, "Beginning to send saved searches");
		for (int i=0; i<searches.size() &&  keepRunning; i++) {
			
//			if (authRefreshNeeded)
//				if (!refreshConnection())
//					return;
			
			SavedSearch enSearch = searches.get(i);
			try {
				if (enSearch.getUpdateSequenceNum() > 0) 
					sequence = noteStore.updateSearch(authToken, enSearch);
				else {
					logger.log(logger.EXTREME, "New saved search found.");
					// Look for a tag with the same name.  If one is found, we don't need 
					// to create another one
					boolean found = false;
					logger.log(logger.EXTREME, "Matching remote saved search names with local");
					for (int k=0; k<remoteList.size() && !found && keepRunning; k++) {
						if (remoteList.get(k).getName().equalsIgnoreCase(enSearch.getName())) {
							enSearch = remoteList.get(k);
							found = true;
							logger.log(logger.EXTREME, "Matching saved search found");
							sequence = enSearch.getUpdateSequenceNum();
						}
					}

					String oldGuid = enSearch.getGuid();
					if (!found)
						enSearch = noteStore.createSearch(authToken, enSearch);
					sequence = enSearch.getUpdateSequenceNum();
					logger.log(logger.EXTREME, "Updating tag guid in local database");
					conn.getSavedSearchTable().updateSavedSearchGuid(oldGuid, enSearch.getGuid());
				}
				logger.log(logger.EXTREME, "Updating tag sequence in local database");
				conn.getSavedSearchTable().updateSavedSearchSequence(enSearch.getGuid(), sequence);
				logger.log(logger.EXTREME, "Resetting tag dirty flag");
				conn.getSavedSearchTable().resetDirtyFlag(enSearch.getGuid());
				logger.log(logger.EXTREME, "Emitting sequence number to the main thread.");
				updateSequenceNumber = sequence;
				conn.getSyncTable().setUpdateSequenceNumber(updateSequenceNumber);
			} catch (EDAMUserException e) {
				logger.log(logger.LOW, "*** EDAM User Excepton syncLocalTags");
				logger.log(logger.LOW, e.toString());	
				error = true;
			} catch (EDAMSystemException e) {
				if (e.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
					limitSignal.rateLimitReached.emit(e.getRateLimitDuration());
				}
				logger.log(logger.LOW, "** EDAM System Excepton syncLocalTags");
				logger.log(logger.LOW, e.toString());	
				error = true;
			} catch (EDAMNotFoundException e) {
				logger.log(logger.LOW, "*** EDAM Not Found Excepton syncLocalTags");
				logger.log(logger.LOW, e.toString());	
				error = true;
			} catch (TException e) {
				logger.log(logger.LOW, "*** EDAM TExcepton syncLocalTags");
				logger.log(logger.LOW, e.toString());	
				error = true;
			}		
		}

		logger.log(logger.HIGH, "Entering SyncRunner.syncLocalSavedSearches");
	}	

	// Sync evernote changes with local database
	private void syncRemoteToLocal(Client noteStore) {
		logger.log(logger.HIGH, "Entering SyncRunner.syncRemoteToLocal");

		List<Note> dirtyNotes = conn.getNoteTable().getDirty();
		dirtyNoteGuids = new ArrayList<String>();
		for (int i=0; i<dirtyNotes.size() && keepRunning; i++) {
			dirtyNoteGuids.add(dirtyNotes.get(i).getGuid());
		}
		
		int chunkSize = 10;
		SyncChunk chunk = null;
		boolean fullSync = false;
		boolean more = true;
		
		if (updateSequenceNumber == 0)
			fullSync = true;
		
		status.message.emit(tr("Downloading 0% complete."));
		
		while(more &&  keepRunning) {
			
//			if (authRefreshNeeded)
//				if (!refreshConnection())
//					return;
			
			int sequence = updateSequenceNumber;
			try {
//				conn.beginTransaction();
				logger.log(logger.EXTREME, "Getting chunk from Evernote");
				chunk = noteStore.getSyncChunk(authToken, sequence, chunkSize, fullSync);
				logger.log(logger.LOW, "Chunk High Sequence: " +chunk.getChunkHighUSN());
			} catch (EDAMUserException e) {
				error = true;
				e.printStackTrace();
				status.message.emit(e.getMessage());
			} catch (EDAMSystemException e) {
				if (e.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
					limitSignal.rateLimitReached.emit(e.getRateLimitDuration());
				}
				error = true;
				e.printStackTrace();
				status.message.emit(e.getMessage());
			} catch (TException e) {
				error = true;
				e.printStackTrace();
				status.message.emit(e.getMessage());
			} 
			if (error || chunk == null) 
				return;
				
		
			
			syncRemoteTags(chunk.getTags());
			syncRemoteSavedSearches(chunk.getSearches());
			syncRemoteNotebooks(chunk.getNotebooks());
			syncRemoteNotes(noteStore, chunk.getNotes(), fullSync, authToken);
			syncRemoteResources(noteStore, chunk.getResources());
			syncRemoteLinkedNotebooks(chunk.getLinkedNotebooks());
			
			// Signal about any updated notes to invalidate the cache
			for (int i=0; i<chunk.getNotesSize(); i++) 
				noteSignal.noteChanged.emit(chunk.getNotes().get(i).getGuid(), null); 
			syncExpungedNotes(chunk);
			
			
			// Check for more notes
			if (chunk.getChunkHighUSN() <= updateSequenceNumber) 
				more = false;
			if (error)
				more = false;
			logger.log(logger.EXTREME, "More notes? " +more);

			
			// Save the chunk sequence number
			if (!error && chunk.getChunkHighUSN() > 0 && keepRunning) {
				logger.log(logger.EXTREME, "emitting sequence number to main thread");
				updateSequenceNumber = chunk.getChunkHighUSN();
				conn.getSyncTable().setLastSequenceDate(chunk.getCurrentTime());
				conn.getSyncTable().setUpdateSequenceNumber(updateSequenceNumber);
//				conn.commitTransaction();
			}
			
			
			if (more) {
				long pct = chunk.getChunkHighUSN() * 100;
				conn.getSyncTable().setLastSequenceDate(chunk.getCurrentTime());
				pct = pct/evernoteUpdateCount;
				status.message.emit(tr("Downloading ") +new Long(pct).toString()+tr("% complete."));
			}
//			conn.commitTransaction();
		}
		logger.log(logger.HIGH, "Leaving SyncRunner.syncRemoteToLocal");
	}
	// Sync expunged notes
	private void syncExpungedNotes(SyncChunk chunk) {
		// Do the local deletes
		logger.log(logger.EXTREME, "Doing local deletes");
		List<String> guid = chunk.getExpungedNotes();
		if (guid != null) {
			for (int i=0; i<guid.size() && keepRunning; i++) {
				String notebookGuid = "";
				Note localNote = conn.getNoteTable().getNote(guid.get(i), false, false, false, false, false);
				if (localNote != null) {
					conn.getNoteTable().updateNoteSequence(guid.get(i), 0);
					notebookGuid = localNote.getNotebookGuid();
				}
				// If the note is in a local notebook (which means we moved it) or if the 
				// note returned is null (which means it is already deleted or flagged expunged) 
				// we delete it.
				if (!conn.getNotebookTable().isNotebookLocal(notebookGuid) || localNote == null) {
					logger.log(logger.EXTREME, "Expunging local note from database");
					conn.getNoteTable().expungeNote(guid.get(i), true, false);
				}
			}
		}
		guid = chunk.getExpungedNotebooks();
		if (guid != null)
			for (int i=0; i<guid.size() && keepRunning; i++) {
				logger.log(logger.EXTREME, "Expunging local notebook from database");
				conn.getNotebookTable().expungeNotebook(guid.get(i), false);
			}
		guid = chunk.getExpungedTags();
		if (guid != null)
			for (int i=0; i<guid.size() && keepRunning; i++) {
				logger.log(logger.EXTREME, "Expunging tags from local database");
				conn.getTagTable().expungeTag(guid.get(i), false);
			}
		guid = chunk.getExpungedSearches();
		if (guid != null) 
			for (int i=0; i<guid.size() && keepRunning; i++) {
				logger.log(logger.EXTREME, "Expunging saved search from local database");
				conn.getSavedSearchTable().expungeSavedSearch(guid.get(i), false);
			}
		guid = chunk.getExpungedLinkedNotebooks();
		if (guid != null) 
			for (int i=0; i<guid.size() && keepRunning; i++) {
				logger.log(logger.EXTREME, "Expunging linked notebook from local database");
				conn.getLinkedNotebookTable().expungeNotebook(guid.get(i), false);
			}

	}
	// Sync remote tags
	private void syncRemoteTags(List<Tag> tags) {
		logger.log(logger.EXTREME, "Entering SyncRunner.syncRemoteTags");
		if (tags != null) {
			for (int i=0; i<tags.size() && keepRunning; i++) {
				String oldGuid;
				oldGuid = conn.getTagTable().findTagByName(tags.get(i).getName());
				if (oldGuid != null && !tags.get(i).getGuid().equalsIgnoreCase(oldGuid))
					conn.getTagTable().updateTagGuid(oldGuid, tags.get(i).getGuid());
				conn.getTagTable().syncTag(tags.get(i), false);
			}
		}
		logger.log(logger.EXTREME, "Leaving SyncRunner.syncRemoteTags");
	}
	// Sync remote saved searches
	private void syncRemoteSavedSearches(List<SavedSearch> searches) {
		logger.log(logger.EXTREME, "Entering SyncRunner.syncSavedSearches");
		if (searches != null) {
			for (int i=0; i<searches.size() && keepRunning; i++) {
				String oldGuid;
				oldGuid = conn.getSavedSearchTable().findSavedSearchByName(searches.get(i).getName());
				if (oldGuid != null && !searches.get(i).getGuid().equalsIgnoreCase(oldGuid))
					conn.getSavedSearchTable().updateSavedSearchGuid(oldGuid, searches.get(i).getGuid());
				conn.getSavedSearchTable().syncSavedSearch(searches.get(i), false);
			}
		}
		logger.log(logger.EXTREME, "Leaving SyncRunner.syncSavedSearches");
	}
	// Sync remote linked notebooks
	private void syncRemoteLinkedNotebooks(List<LinkedNotebook> books) {
		logger.log(logger.EXTREME, "Entering SyncRunner.syncLinkedNotebooks");
		if (books != null) {
			for (int i=0; i<books.size() && keepRunning; i++) {
				conn.getLinkedNotebookTable().updateNotebook(books.get(i), false); 
			}
		}
		logger.log(logger.EXTREME, "Leaving SyncRunner.syncLinkedNotebooks");
	}
	// Sync remote Notebooks 2
	private void syncRemoteNotebooks(List<Notebook> notebooks) {
		logger.log(logger.EXTREME, "Entering SyncRunner.syncRemoteNotebooks");
		if (notebooks != null) {
			for (int i=0; i<notebooks.size() && keepRunning; i++) {
				String oldGuid;
				oldGuid = conn.getNotebookTable().findNotebookByName(notebooks.get(i).getName());
				if (oldGuid != null && !conn.getNotebookTable().isNotebookLocal(oldGuid) && !notebooks.get(i).getGuid().equalsIgnoreCase(oldGuid))
					conn.getNotebookTable().updateNotebookGuid(oldGuid, notebooks.get(i).getGuid());
				conn.getNotebookTable().syncNotebook(notebooks.get(i), false); 
				
				// Synchronize shared notebook information
//				if (notebooks.get(i).getSharedNotebookIdsSize() > 0) {
//					conn.getSharedNotebookTable().expungeNotebookByGuid(notebooks.get(i).getGuid(), false);
//					for (int j=0; j<notebooks.get(i).getSharedNotebookIdsSize(); j++) {
//						syncRemoteSharedNotebook(notebooks.get(i).getGuid(), notebooks.get(i).getSharedNotebookIds().get(j), authToken);
//					}
//				}
			}
		}			
		logger.log(logger.EXTREME, "Leaving SyncRunner.syncRemoteNotebooks");
	}
	// Sync remote shared notebook
//	private void syncRemoteSharedNotebook(String guid, Long id, String token) {
//		List<SharedNotebook> books = noteStore.getSharedNotebookByAuth(authToken);
//	}
	// Sync remote Resources
	private void syncRemoteResources(Client noteStore, List<Resource> resource) {
		logger.log(logger.EXTREME, "Entering SyncRunner.syncRemoteResources");
		if (resource != null) {
			for (int i=0; i<resource.size() && keepRunning; i++) {
				syncRemoteResource(noteStore, resource.get(i), authToken);
			}
		}
		logger.log(logger.EXTREME, "Leaving SyncRunner.syncRemoteResources");
	}
	// Sync remote resource
	private void syncRemoteResource(Client noteStore, Resource resource, String authToken) {
		// This is how the logic for this works.
		// 1.) If the resource is not in the local database, we add it.
		// 2.) If a copy of the resource is in the local database and the note isn't dirty, we update the local copy
		// 3.) If a copy of the resource is in the local databbase and it is dirty and the hash doesn't match, we ignore it because there
		//     is a conflict.  The note conflict should get a copy of the resource at that time.
		
		Note n = conn.getNoteTable().getNote(resource.getNoteGuid(), false, false, false, false, false);
		if (n!=null) {
			logger.log(logger.HIGH, "Resource for note " +n.getGuid() +" : " +n.getTitle());
		}
		boolean saveNeeded = false;
		/* #1 */		Resource r = getEvernoteResource(noteStore, resource.getGuid(), true,true,true, authToken);
						Resource l = conn.getNoteTable().noteResourceTable.getNoteResource(r.getGuid(), false);
						if (l == null) {
							logger.log(logger.HIGH, "Local resource not found");
							saveNeeded = true;
						} else {
		/* #2 */			boolean isNoteDirty = conn.getNoteTable().isNoteDirty(r.getNoteGuid());
							if (!isNoteDirty) {
								logger.log(logger.HIGH, "Local resource found, but is not dirty");
								saveNeeded = true;
							} else {
		/* #3 */				String remoteHash = "";
								if (r != null && r.getData() != null && r.getData().getBodyHash() != null)
									remoteHash = byteArrayToHexString(r.getData().getBodyHash());
								String localHash = "";
								if (l != null && l.getData() != null && l.getData().getBodyHash() != null)
									remoteHash = byteArrayToHexString(l.getData().getBodyHash());
						
								if (localHash.equalsIgnoreCase(remoteHash))
									saveNeeded = true;
							}
						}
						
						logger.log(logger.HIGH, "Resource save needed: " +saveNeeded);
						if (saveNeeded) 
							conn.getNoteTable().noteResourceTable.updateNoteResource(r, false);
						if (r.getMime().equalsIgnoreCase("application/vnd.evernote.ink"))
							downloadInkNoteImage(r.getGuid(), authToken);
		

	}
	// Sync remote notes
	private void syncRemoteNotes(Client noteStore, List<Note> note, boolean fullSync, String token) {

		if (note != null) {
			
			logger.log(logger.EXTREME, "Entering SyncRunner.syncRemoteNotes");
			logger.log(logger.LOW, "Local Dirty Notes: ");
			logger.log(logger.LOW, "Remote Dirty Notes:");
			for (int i=0; i<note.size();i++) {
				logger.log(logger.LOW, i +" : " +note.get(i).getGuid() + " : " +note.get(i).getTitle() );
			}
			logger.log(logger.LOW, "---");
			
			for (int i=0; i<note.size() && keepRunning; i++) {
				Note n = getEvernoteNote(noteStore, note.get(i).getGuid(), true, fullSync, true,true, token);
				syncRemoteNote(n, fullSync, token);
			}
		}
		logger.log(logger.EXTREME, "Leaving SyncRunner.syncRemoteNotes");
	}
	private void syncRemoteNote(Note n, boolean fullSync, String token) {
		if (n!=null) {
			
			// Basically, this is how the sync logic for a note works.
			// If the remote note has changed and the local has not, we
			// accept the change.
			// If both the local & remote have changed but the sequence
			// numbers are the same, we don't accept the change.  This
			// seems to happen when attachments are indexed by the server.
			// If both the local & remote have changed and the sequence numbers
			// are different we move the local copy to a local notebook (making sure
			// to copy all resources) and we accept the new one.			
			boolean conflictingNote = true;
			logger.log(logger.EXTREME, "Checking for duplicate note " +n.getGuid() +" : " +n.getTitle());
			if (dirtyNoteGuids != null && dirtyNoteGuids.contains(n.getGuid())) { 
				logger.log(logger.EXTREME, "Conflict check beginning");
				conflictingNote = checkForConflict(n);
				logger.log(logger.EXTREME, "Conflict check results " +conflictingNote);
				if (conflictingNote)
					moveConflictingNote(n.getGuid());
			}
			boolean ignoreNote = false;
			if (ignoreNotebooks.contains(n.getNotebookGuid()))
				ignoreNote = true;
			for (int i=0; i<n.getTagGuidsSize(); i++) {
				if (ignoreTags.contains(n.getTagGuids().get(i))) {
					ignoreNote = true;
					i=n.getTagGuidsSize();
				}
			}
				
			if ((conflictingNote || fullSync) && !ignoreNote) {
				logger.log(logger.EXTREME, "Saving Note");
				conn.getNoteTable().syncNote(n);
				// The following was commented out because it caused a race condition on the database where resources 
				// may be lost.  We do the same thing elsewhere;.
//				noteSignal.noteChanged.emit(n.getGuid(), null);   // Signal to ivalidate note cache 
				noteSignal.noteDownloaded.emit(n, true);		// Signal to add note to index
					logger.log(logger.EXTREME, "Note Saved");
				if (fullSync && n.getResources() != null) {
					for (int q=0; q<n.getResources().size() && keepRunning; q++) {
						logger.log(logger.EXTREME, "Getting note resources.");
						conn.getNoteTable().noteResourceTable.updateNoteResource(n.getResources().get(q), false);
						if (n.getResources().get(q).getMime().equalsIgnoreCase("application/vnd.evernote.ink"))
							downloadInkNoteImage(n.getResources().get(q).getGuid(), token);
					}
				}
			}
		}
	}
	private Note getEvernoteNote(Client noteStore, String guid, boolean withContent, boolean withResourceData, boolean withResourceRecognition, boolean withResourceAlternateData, String token) { 
		Note n = null;
		try {
			logger.log(logger.EXTREME, "Retrieving note " +guid);
			n = noteStore.getNote(token, guid, withContent, withResourceData, withResourceRecognition, withResourceAlternateData);
			logger.log(logger.EXTREME, "Note " +guid +" has been retrieved.");
		} catch (EDAMUserException e) {
			logger.log(logger.LOW, "*** EDAM User Excepton getEvernoteNote");
			logger.log(logger.LOW, e.toString());	
			error = true;
			e.printStackTrace();
		} catch (EDAMSystemException e) {
			if (e.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
				limitSignal.rateLimitReached.emit(e.getRateLimitDuration());
			}
			logger.log(logger.LOW, "*** EDAM System Excepton getEvernoteNote");
			logger.log(logger.LOW, e.toString());	
			error = true;
			e.printStackTrace();
		} catch (EDAMNotFoundException e) {
			logger.log(logger.LOW, "*** EDAM Not Found Excepton getEvernoteNote");
			logger.log(logger.LOW, e.toString());	
			error = true;
			e.printStackTrace();
		} catch (TException e) {
			logger.log(logger.LOW, "*** EDAM TExcepton getEvernoteNote");
			logger.log(logger.LOW, e.toString());	
			error = true;
			e.printStackTrace();
		}
		return n;
	}
	private Resource getEvernoteResource(Client noteStore, String guid, boolean withData, boolean withRecognition, boolean withAttributes, String token) { 
		Resource n = null;
		try {
			logger.log(logger.EXTREME, "Retrieving resource " +guid);
			n = noteStore.getResource(token, guid, withData, withRecognition, withAttributes, withAttributes);
			logger.log(logger.EXTREME, "Resource " +guid +" has been retrieved.");
		} catch (EDAMUserException e) {
			logger.log(logger.LOW, "*** EDAM User Excepton getEvernoteNote");
			logger.log(logger.LOW, e.toString());	
			error = true;
			e.printStackTrace();
		} catch (EDAMSystemException e) {
			if (e.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
				limitSignal.rateLimitReached.emit(e.getRateLimitDuration());
			}
			logger.log(logger.LOW, "*** EDAM System Excepton getEvernoteNote");
			logger.log(logger.LOW, e.toString());	
			error = true;
			e.printStackTrace();
		} catch (EDAMNotFoundException e) {
			logger.log(logger.LOW, "*** EDAM Not Found Excepton getEvernoteNote");
			logger.log(logger.LOW, e.toString());	
			error = true;
			e.printStackTrace();
		} catch (TException e) {
			logger.log(logger.LOW, "*** EDAM TExcepton getEvernoteNote");
			logger.log(logger.LOW, e.toString());	
			error = true;
			e.printStackTrace();
		}
		return n;
	}

	
	private boolean checkForConflict(Note n) {
		logger.log(logger.EXTREME, "Checking note sequence number  " +n.getGuid());
		Note oldNote = conn.getNoteTable().getNote(n.getGuid(), false, false, false, false, false);
		logger.log(logger.EXTREME, "Local/Remote sequence numbers: " +oldNote.getUpdateSequenceNum()+"/"+n.getUpdateSequenceNum());
		logger.log(logger.LOW, "Remote Note Title:" +n.getTitle());
		logger.log(logger.LOW, "Local Note Title:" +oldNote.getTitle());
		if (oldNote.getUpdateSequenceNum() == n.getUpdateSequenceNum()) {
			return false;
		} 
		boolean oldIsDirty = conn.getNoteTable().isNoteDirty(n.getGuid());
		if (!oldIsDirty) 
			return false;
		return true;
	}
	
	private void moveConflictingNote(String guid) {
		logger.log(logger.EXTREME, "Conflicting change found for note " +guid);
		List<Notebook> books = conn.getNotebookTable().getAllLocal();
		String notebookGuid = null;
		for (int i=0; i<books.size() && keepRunning; i++) {
			if (books.get(i).getName().equalsIgnoreCase("Conflicting Changes (local)") ||
					books.get(i).getName().equalsIgnoreCase("Conflicting Changes")) {
				notebookGuid = books.get(i).getGuid();
				i=books.size();
			}
		}
		
		if (notebookGuid == null) {
			logger.log(logger.EXTREME, "Building conflicting change notebook " +guid);
			Calendar currentTime = new GregorianCalendar();
			Long l = new Long(currentTime.getTimeInMillis());
			long prevTime = l;
			while (prevTime==l) {
				currentTime = new GregorianCalendar();
				l=currentTime.getTimeInMillis();
			}
			String randint = new String(Long.toString(l));
		
			Notebook newBook = new Notebook();
			newBook.setUpdateSequenceNum(0);
			newBook.setGuid(randint);
			newBook.setName("Conflicting Changes");
			newBook.setServiceCreated(new Date().getTime());
			newBook.setServiceUpdated(new Date().getTime());
			newBook.setDefaultNotebook(false);
			newBook.setPublished(false);
			
			conn.getNotebookTable().addNotebook(newBook, false, true);
			notebookSignal.listChanged.emit();
			notebookGuid = newBook.getGuid();
			refreshNeeded = true;
		}
		
		// Now that we have a good notebook guid, we need to move the conflicting note
		// to the local notebook
		logger.log(logger.EXTREME, "Moving conflicting note " +guid);
		Calendar currentTime = new GregorianCalendar();
		Long l = new Long(currentTime.getTimeInMillis());
		long prevTime = l;
		while (prevTime==l) {
			currentTime = new GregorianCalendar();
			l = currentTime.getTimeInMillis();
		}
		String newGuid = new String(Long.toString(l));
					
		Note oldNote = conn.getNoteTable().getNote(guid, true, true, false, false, false);
		for (int i=0; i<oldNote.getResources().size() && keepRunning; i++) {
			l = new Long(currentTime.getTimeInMillis());
			String newResG = new String(Long.toString(l));
			String oldResG = oldNote.getResources().get(i).getGuid();
			conn.getNoteTable().noteResourceTable.resetUpdateSequenceNumber(oldResG, true);
			conn.getNoteTable().noteResourceTable.updateNoteResourceGuid(oldResG, newResG, true);
		}
		
		conn.getNoteTable().resetNoteSequence(guid);
		conn.getNoteTable().updateNoteGuid(guid, newGuid);
		conn.getNoteTable().updateNoteNotebook(newGuid, notebookGuid, true);
		
		noteSignal.notebookChanged.emit(newGuid, notebookGuid);
		refreshNeeded = true;
		noteSignal.guidChanged.emit(guid,newGuid);
	}
	


	
	//******************************************************
	//******************************************************
	//** Utility Functions
	//******************************************************
	//******************************************************
	// Convert a byte array to a hex string
	private static String byteArrayToHexString(byte data[]) {
		StringBuffer buf = new StringBuffer();
	    for (byte element : data) {
	    	int halfbyte = (element >>> 4) & 0x0F;
	        int two_halfs = 0;
	        do {
		       	if ((0 <= halfbyte) && (halfbyte <= 9))
		               buf.append((char) ('0' + halfbyte));
		           else
		           	buf.append((char) ('a' + (halfbyte - 10)));
		       	halfbyte = element & 0x0F;
	        } while(two_halfs++ < 1);
	    }
	    return buf.toString();		
	}

	
	
	//*******************************************************
	//* Find dirty tags, which do not have newly created parents
	//*******************************************************
	private Tag findNextTag() {
		logger.log(logger.HIGH, "Entering SyncRunner.findNextTag");
		Tag nextTag = null;
		List<Tag> tags = conn.getTagTable().getDirty();
		
		// Find the parent.  If the parent has a sequence > 0 then it is a good
		// parent.
		for (int i=0; i<tags.size() && keepRunning; i++) {
			if (!badTagSync.containsKey(tags.get(i).getGuid())) {
				if (tags.get(i).getParentGuid() == null) {
					logger.log(logger.HIGH, "Leaving SyncRunner.findNextTag - tag found without parent");
					return tags.get(i);
				}
				Tag parentTag = conn.getTagTable().getTag(tags.get(i).getParentGuid());
				if (parentTag.getUpdateSequenceNum() > 0) {
					logger.log(logger.HIGH, "Leaving SyncRunner.findNextTag - tag found");
					return tags.get(i);
				}
			}
		}
		
		logger.log(logger.HIGH, "Leaving SyncRunner.findNextTag - no tags returned");
		return nextTag;
	}
	
	
	   // Connect to Evernote
    public boolean enConnect()  {
    	try {
			userStoreTrans = new THttpClient(userStoreUrl);
			userStoreTrans.setCustomHeader("User-Agent", userAgent);
		} catch (TTransportException e) {
			QMessageBox mb = new QMessageBox(QMessageBox.Icon.Critical, "Transport Excepton", e.getLocalizedMessage());
			mb.exec();
			e.printStackTrace();
		}
		userStoreProt = new TBinaryProtocol(userStoreTrans);
	    userStore = new UserStore.Client(userStoreProt, userStoreProt);
	    
	    syncSignal.saveUserStore.emit(userStore);
	    try {
			//authResult = userStore.authenticate(username, password, consumerKey, consumerSecret);
	    	user = userStore.getUser(authToken);
		} catch (EDAMUserException e) {
			QMessageBox mb = new QMessageBox(QMessageBox.Icon.Critical, "Error", "Invalid Authorization");
			mb.exec();
			isConnected = false;
			return false;
		} catch (EDAMSystemException e) {
			if (e.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
				limitSignal.rateLimitReached.emit(e.getRateLimitDuration());
			}
			QMessageBox mb = new QMessageBox(QMessageBox.Icon.Critical, "EDAM System Excepton", e.getLocalizedMessage());
			mb.exec();
			e.printStackTrace();
			isConnected = false;
		} catch (TException e) {
			QMessageBox mb = new QMessageBox(QMessageBox.Icon.Critical, "Transport Excepton", e.getLocalizedMessage());
			mb.exec();
			e.printStackTrace();
			isConnected = false;
		}
		
	    boolean versionOk = false;
		try {
			versionOk = userStore.checkVersion("NeighborNote", 
	            com.evernote.edam.userstore.Constants.EDAM_VERSION_MAJOR, 
	              com.evernote.edam.userstore.Constants.EDAM_VERSION_MINOR);
		} catch (TException e) {
			e.printStackTrace();
			isConnected = false;
		} 
	    if (!versionOk) { 
	        System.err.println("Incomatible EDAM client protocol version"); 
	        isConnected = false;
	    }
	    //if (authResult != null) {
	    	//user = authResult.getUser(); 
	    	//authToken = authResult.getAuthenticationToken(); 
	    if (user == null || noteStoreUrlBase == null) {
	    	logger.log(logger.LOW, "Error retrieving user information.  Aborting.");
	    	System.err.println("Error retrieving user information.");
	    	isConnected = false;	
			QMessageBox mb = new QMessageBox(QMessageBox.Icon.Critical, tr("Connection Error"), tr("Error retrieving user information.  Synchronization not complete"));
			mb.exec();
			return false;
	    	
	    }
	    	noteStoreUrl = noteStoreUrlBase + user.getShardId();
	    	syncSignal.saveAuthToken.emit(authToken);
	    	syncSignal.saveNoteStore.emit(localNoteStore);
	    	
	 
	    	try {
	    		noteStoreTrans = new THttpClient(noteStoreUrl);
	    		noteStoreTrans.setCustomHeader("User-Agent", userAgent);
	    	} catch (TTransportException e) {
	    		QMessageBox mb = new QMessageBox(QMessageBox.Icon.Critical, "Transport Excepton", e.getLocalizedMessage());
	    		mb.exec();
	    		e.printStackTrace();
	    		isConnected = false;
	    	} 
	    	noteStoreProt = new TBinaryProtocol(noteStoreTrans);
	    	localNoteStore = 
	    		new NoteStore.Client(noteStoreProt, noteStoreProt); 
	    	isConnected = true;
	    	//authTimeRemaining = authResult.getExpiration() - authResult.getCurrentTime();
	    	//authRefreshTime = authTimeRemaining / 2;
	    //}
	    
		// Get user information
		try {
			User user = userStore.getUser(authToken);
			syncSignal.saveUserInformation.emit(user);
		} catch (EDAMUserException e1) {
			e1.printStackTrace();
		} catch (EDAMSystemException e1) {
			if (e1.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
				limitSignal.rateLimitReached.emit(e1.getRateLimitDuration());
			}
			e1.printStackTrace();
		} catch (TException e1) {
			e1.printStackTrace();
		}
	    
	    return isConnected;
    }
	// Disconnect from the database				
    public void enDisconnect() {
    	isConnected = false;
    }
    
    /*
    // Refresh the connection
    private synchronized boolean refreshConnection() {
    	
		logger.log(logger.EXTREME, "Entering SyncRunner.refreshConnection()");
//        Calendar cal = Calendar.getInstance();
		
        // If we are not connected let's get out of here
        if (!isConnected)
        	return false;
        
   		// If we fail too many times, then let's give up.
   		if (failedRefreshes >=5) {
   			logger.log(logger.EXTREME, "Refresh attempts have failed.  Disconnecting.");
   			isConnected = false;
   			status.message.emit(tr("Unable to synchronize - Authentication failed"));
   			return false;
   		}
        
   		// If this is the first time through, then we need to set this
//   		if (authRefreshTime == 0 || cal.getTimeInMillis() > authRefreshTime) 
//   			authRefreshTime = cal.getTimeInMillis();
   		
 //  		// Default to checking again in 5 min.  This in case we fail.
 //  		authRefreshTime = authRefreshTime +(5*60*1000);     

   		// Try to get a new token
		AuthenticationResult newAuth = null; 
		logger.log(logger.EXTREME, "Beginning to try authentication refresh");
    	try {
    		if (userStore != null && authToken != null) 
    			newAuth = userStore.refreshAuthentication(authToken); 
    		else
    			return false;
    		logger.log(logger.EXTREME, "UserStore.refreshAuthentication has succeeded.");
		} catch (EDAMUserException e) {
			e.printStackTrace();
			syncSignal.authRefreshComplete.emit(false);
			failedRefreshes++;
			return false;
		} catch (EDAMSystemException e) {
			e.printStackTrace();
			syncSignal.authRefreshComplete.emit(false);
			failedRefreshes++;
			return false;		
		} catch (TException e) { 
			e.printStackTrace();
			syncSignal.authRefreshComplete.emit(false);
			failedRefreshes++;
			return false;
		}
		
		// If we didn't get a good auth, then we've failed
		if (newAuth == null) {
			failedRefreshes++;
			status.message.emit(tr("Unable to synchronize - Authentication failed"));
			logger.log(logger.EXTREME, "Authentication failure #" +failedRefreshes);
			status.message.emit(tr("Unable to synchronize - Authentication failed"));
			syncSignal.authRefreshComplete.emit(false);
			return false;
		}
		
		// We got a good token.  Now we need to setup the time to renew it.
		logger.log(logger.EXTREME, "Saving authentication tokens");
		authResult = newAuth;
		authToken = new String(newAuth.getAuthenticationToken());
//		authTimeRemaining = authResult.getExpiration() - authResult.getCurrentTime();
//		authRefreshTime = cal.getTimeInMillis() + (authTimeRemaining/4);	
		failedRefreshes=0;
		syncSignal.authRefreshComplete.emit(true);
		authRefreshNeeded = false;
			
		// This should never happen, but if it does we consider this a faild attempt.
//		if (authTimeRemaining <= 0) {
//			failedRefreshes++;
//			syncSignal.authRefreshComplete.emit(false);
//		}
		
		return true;
    }
    
    */
    
	public synchronized boolean addWork(String request) {
		if (workQueue.offer(request))
			return true;
		return false;
	}
    
    private Note getNoteContent(Note n) {
		QTextCodec codec = QTextCodec.codecForLocale();
		codec = QTextCodec.codecForName("UTF-8");
    	n.setContent(codec.toUnicode(new QByteArray(n.getContent())));
    	return n;
    }



    //*********************************************************
    //* Special download instructions.  Used for DB upgrades
    //*********************************************************
    private void downloadAllSharedNotebooks(Client noteStore) {
    	try {
			List<SharedNotebook> books = noteStore.listSharedNotebooks(authToken);
			logger.log(logger.LOW, "Shared notebooks found = " +books.size());
			for (int i=0; i<books.size(); i++) {
				conn.getSharedNotebookTable().updateNotebook(books.get(i), false);
			}
			conn.getSyncTable().deleteRecord("FullSharedNotebookSync");
		} catch (EDAMUserException e1) {
			e1.printStackTrace();
			status.message.emit(tr("User exception Listing shared notebooks."));
			logger.log(logger.LOW, e1.getMessage());
			return;
		} catch (EDAMSystemException e1) {
			if (e1.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
				limitSignal.rateLimitReached.emit(e1.getRateLimitDuration());
			}
			e1.printStackTrace();
			status.message.emit(tr("System exception Listing shared notebooks."));
			logger.log(logger.LOW, e1.getMessage());
			return;
		} catch (TException e1) {
			e1.printStackTrace();
			status.message.emit(tr("Transaction exception Listing shared notebooks."));
			logger.log(logger.LOW, e1.getMessage());
			return;
		} catch (EDAMNotFoundException e1) {
			e1.printStackTrace();
			status.message.emit(tr("EDAM Not Found exception Listing shared notebooks."));
			logger.log(logger.LOW, e1.getMessage());
		}
    }
    private void downloadAllNotebooks(Client noteStore) {
    	try {
			List<Notebook> books = noteStore.listNotebooks(authToken);
			logger.log(logger.LOW, "Shared notebooks found = " +books.size());
			for (int i=0; i<books.size(); i++) {
				conn.getNotebookTable().updateNotebook(books.get(i), false);
			}
			conn.getSyncTable().deleteRecord("FullNotebookSync");
		} catch (EDAMUserException e1) {
			e1.printStackTrace();
			status.message.emit(tr("User exception Listing notebooks."));
			logger.log(logger.LOW, e1.getMessage());
			return;
		} catch (EDAMSystemException e1) {
			if (e1.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
				limitSignal.rateLimitReached.emit(e1.getRateLimitDuration());
			}
			e1.printStackTrace();
			status.message.emit(tr("System exception Listing notebooks."));
			logger.log(logger.LOW, e1.getMessage());
			return;
		} catch (TException e1) {
			e1.printStackTrace();
			status.message.emit(tr("Transaction exception Listing notebooks."));
			logger.log(logger.LOW, e1.getMessage());
			return;
		}
    }
    private void downloadAllLinkedNotebooks(Client noteStore) {
    	try {
			List<LinkedNotebook> books = noteStore.listLinkedNotebooks(authToken);
			logger.log(logger.LOW, "Linked notebooks found = " +books.size());
			for (int i=0; i<books.size(); i++) {
				conn.getLinkedNotebookTable().updateNotebook(books.get(i), false);
			}
			conn.getSyncTable().deleteRecord("FullLinkedNotebookSync");
		} catch (EDAMUserException e1) {
			e1.printStackTrace();
			status.message.emit(tr("User exception Listing linked notebooks."));
			logger.log(logger.LOW, e1.getMessage());
			return;
		} catch (EDAMSystemException e1) {
			if (e1.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
				limitSignal.rateLimitReached.emit(e1.getRateLimitDuration());
			}
			e1.printStackTrace();
			status.message.emit(tr("System exception Listing linked notebooks."));
			logger.log(logger.LOW, e1.getMessage());
			return;
		} catch (TException e1) {
			e1.printStackTrace();
			status.message.emit(tr("Transaction exception Listing lineked notebooks."));
			logger.log(logger.LOW, e1.getMessage());
			return;
		} catch (EDAMNotFoundException e1) {
			e1.printStackTrace();
			status.message.emit(tr("EDAM Not Found exception Listing linked notebooks."));
			logger.log(logger.LOW, e1.getMessage());
		}
    }

    
    private void downloadInkNoteImage(String guid, String authToken) {
		String urlBase = noteStoreUrl.replace("/edam/note/", "/shard/") + "/res/"+guid+".ink?slice=";
//		urlBase = "https://www.evernote.com/shard/s1/res/52b567a9-54ae-4a08-afc5-d5bae275b2a8.ink?slice=";
		Integer slice = 1;
		Resource r = conn.getNoteTable().noteResourceTable.getNoteResource(guid, false);
		conn.getInkImagesTable().expungeImage(r.getGuid());
		int sliceCount = 1+((r.getHeight()-1)/480);
		HttpClient http = new DefaultHttpClient();
    	for (int i=0; i<sliceCount; i++) {
    		String url = urlBase + slice.toString();
    		HttpPost post = new HttpPost(url);
    		post.getParams().setParameter("auth", authToken);
    		List <NameValuePair> nvps = new ArrayList <NameValuePair>();
            nvps.add(new BasicNameValuePair("auth", authToken));

            try {
				post.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
			} catch (UnsupportedEncodingException e1) {
				e1.printStackTrace();
			}
    		try {
    			HttpResponse response = http.execute(post);
    			HttpEntity resEntity = response.getEntity();
    			InputStream is = resEntity.getContent();
    			QByteArray data = writeToFile(is);
    			conn.getInkImagesTable().saveImage(guid, slice, data);
			} catch (ClientProtocolException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}

			slice++;
    	}
    	http.getConnectionManager().shutdown(); 
		noteSignal.noteChanged.emit(r.getNoteGuid(), null);   // Signal to ivalidate note cache
    }
    
    
    public QByteArray writeToFile(InputStream iStream) throws IOException {

    	    File temp = File.createTempFile("nn-inknote-temp", ".png");

    	    // Save InputStream to the file.
    	    BufferedOutputStream fOut = null;
    	    try {
    	      fOut = new BufferedOutputStream(new FileOutputStream(temp));
    	      byte[] buffer = new byte[32 * 1024];
    	      int bytesRead = 0;
    	      while ((bytesRead = iStream.read(buffer)) != -1) {
    	        fOut.write(buffer, 0, bytesRead);
    	      }
    	    }
    	    finally {
    	        iStream.close();
    	        fOut.close();
    	    }
    	    QFile tempFile = new QFile(temp.getAbsoluteFile().toString());
    	    tempFile.open(OpenModeFlag.ReadOnly);
    	    QByteArray data = tempFile.readAll();
    	    tempFile.close();
    	    tempFile.remove();
    	    return data;
    }
    
    
	//******************************************
	//* Begin syncing shared notebooks 
	//******************************************
    private void syncLinkedNotebooks() {
    	logger.log(logger.MEDIUM, "Authenticating linked Notebooks");
    	status.message.emit(tr("Synchronizing shared notebooks."));
    	List<LinkedNotebook> books = conn.getLinkedNotebookTable().getAll();
    	
    	errorSharedNotebooks.clear();
		
    	for (int i=0; i<books.size(); i++) {
    		if (errorSharedNotebooksIgnored.containsKey(books.get(i).getGuid()))
    			break;
    		try {
    			logger.log(logger.EXTREME, "Checking notebook: " +books.get(i).getShareName());
   				long lastSyncDate = conn.getLinkedNotebookTable().getLastSequenceDate(books.get(i).getGuid());
   				int lastSequenceNumber = conn.getLinkedNotebookTable().getLastSequenceNumber(books.get(i).getGuid());

   				logger.log(logger.EXTREME, "Last Sequence Number on file: " +lastSequenceNumber);
   				
   				// Authenticate to the owner's shard
   				String linkedNoteStoreUrl 	= noteStoreUrlBase + books.get(i).getShardId();
   				logger.log(logger.EXTREME, "linkedNoteStoreURL: " +linkedNoteStoreUrl);
   				THttpClient linkedNoteStoreTrans 	= new THttpClient(linkedNoteStoreUrl);
   				TBinaryProtocol linkedNoteStoreProt 	= new TBinaryProtocol(linkedNoteStoreTrans);
   				Client linkedNoteStore = new NoteStore.Client(linkedNoteStoreProt, linkedNoteStoreProt);   	

   				linkedAuthResult = null;
   				if (books.get(i).getShareKey() != null) {
   					logger.log(logger.EXTREME, "Share Key Not Null: " +books.get(i).getShareKey());
   					linkedAuthResult = linkedNoteStore.authenticateToSharedNotebook(books.get(i).getShareKey(), authToken);
   					logger.log(logger.EXTREME, "Authentication Token" +linkedAuthResult.getAuthenticationToken());
   				} else {
   					logger.log(logger.EXTREME, "Share key is null");
   					linkedAuthResult = new AuthenticationResult();
   					linkedAuthResult.setAuthenticationToken("");
   				}
   				SyncState linkedSyncState = 
   					linkedNoteStore.getLinkedNotebookSyncState(linkedAuthResult.getAuthenticationToken(), books.get(i));
   				if (linkedSyncState.getUpdateCount() > lastSequenceNumber) {
   					logger.log(logger.EXTREME, "Remote changes found");
   					if (lastSyncDate < linkedSyncState.getFullSyncBefore()) {
   						lastSequenceNumber = 0;
   					} 
   					logger.log(logger.EXTREME, "Calling syncLinkedNotebook for " +books.get(i).getShareName());
   					syncLinkedNotebook(linkedNoteStore, books.get(i), 
   							lastSequenceNumber, linkedSyncState.getUpdateCount(), authToken);
   				}
    			
    			// Synchronize local changes
    			syncLocalLinkedNoteChanges(linkedNoteStore, books.get(i));
				
    		} catch (EDAMUserException e) {
    			e.printStackTrace();
    		} catch (EDAMNotFoundException e) {
    			status.message.emit(tr("Error synchronizing \" " +
					books.get(i).getShareName()+"\". Please verify you still have access to that shared notebook."));
    			errorSharedNotebooks.add(books.get(i).getGuid());
    			errorSharedNotebooksIgnored.put(books.get(i).getGuid(), books.get(i).getGuid());
    			logger.log(logger.LOW, "Error synchronizing shared notebook.  EDAMNotFound: "+e.getMessage());
    			logger.log(logger.LOW, e.getStackTrace());
    			error = true;
    			e.printStackTrace();
    		} catch (EDAMSystemException e) {
    			if (e.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
    				limitSignal.rateLimitReached.emit(e.getRateLimitDuration());
    			}
    			error = true;
    			logger.log(logger.LOW, "System error authenticating against shared notebook. "+
    					"Key: "+books.get(i).getShareKey() +" Error:" +e.getMessage());
    			e.printStackTrace();
    		} catch (TException e) {
    			error = true;
    			e.printStackTrace();
    		}
    	}
    	
    	// Cleanup tags
    	conn.getTagTable().removeUnusedLinkedTags();
    	conn.getTagTable().cleanupTags();
    	tagSignal.listChanged.emit();
    	return;
	}

    
    //**************************************************************
    //* Linked notebook contents (from someone else's account)
    //*************************************************************
	private void syncLinkedNotebook(Client linkedNoteStore, LinkedNotebook book, int usn, int highSequence, String token) {
		logger.log(logger.EXTREME, "Entering syncLinkedNotebook");
		if (ignoreLinkedNotebooks.contains(book.getGuid()))
			return;
		List<Note> dirtyNotes = conn.getNoteTable().getDirtyLinkedNotes();
		if (dirtyNoteGuids == null) 
			dirtyNoteGuids = new ArrayList<String>();

		for (int i=0; i<dirtyNotes.size() && keepRunning; i++) {
			dirtyNoteGuids.add(dirtyNotes.get(i).getGuid());
		}
		boolean fullSync = false;
		if (usn == 0)
			fullSync = true;
		boolean syncError = false;
		while (usn < highSequence && !syncError) {
			refreshNeeded = true;
			try {
				SyncChunk chunk = 
					linkedNoteStore.getLinkedNotebookSyncChunk(token, book, usn, 10, fullSync);
				
				// Expunge notes
				syncExpungedNotes(chunk);

				logger.log(logger.EXTREME, "Syncing remote notes: " +chunk.getNotesSize());
				syncRemoteNotes(linkedNoteStore, chunk.getNotes(), fullSync, linkedAuthResult.getAuthenticationToken());
				logger.log(logger.EXTREME, "Finding new linked tags");
				findNewLinkedTags(linkedNoteStore, chunk.getNotes(), linkedAuthResult.getAuthenticationToken());
				// Sync resources
				logger.log(logger.EXTREME, "Synchronizing tags: " +chunk.getTagsSize());
				for (int i=0; i<chunk.getResourcesSize(); i++) {
					syncRemoteResource(linkedNoteStore, chunk.getResources().get(i), linkedAuthResult.getAuthenticationToken());
				}
				logger.log(logger.EXTREME, "Synchronizing linked notebooks: " +chunk.getNotebooksSize());
				syncRemoteLinkedNotebooks(linkedNoteStore, chunk.getNotebooks(), false, book);
				syncLinkedTags(chunk.getTags(), book.getGuid());
				
				// Go through & signal any notes that have changed so we can refresh the user's view
				for (int i=0; i<chunk.getNotesSize(); i++) 
					noteSignal.noteChanged.emit(chunk.getNotes().get(i).getGuid(), null);

				// Expunge Notebook records
				logger.log(logger.EXTREME, "Expunging linked notebooks: " +chunk.getExpungedLinkedNotebooksSize());
				for (int i=0; i<chunk.getExpungedLinkedNotebooksSize(); i++) {
					conn.getLinkedNotebookTable().expungeNotebook(chunk.getExpungedLinkedNotebooks().get(i), false);
				}
				usn = chunk.getChunkHighUSN();
				conn.getLinkedNotebookTable().setLastSequenceDate(book.getGuid(),chunk.getCurrentTime());
				conn.getLinkedNotebookTable().setLastSequenceNumber(book.getGuid(),chunk.getChunkHighUSN());
			} catch (EDAMUserException e) {
				syncError = true;
				status.message.emit(tr("EDAM UserException synchronizing linked notbook.  See the log for datails."));
				e.printStackTrace();
				logger.log(logger.LOW, tr("EDAM UserException synchronizing linked notbook ")+ e.getMessage());
			} catch (EDAMSystemException e) {
				if (e.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
					limitSignal.rateLimitReached.emit(e.getRateLimitDuration());
				}
				syncError = true;
				status.message.emit(tr("EDAM SystemException synchronizing linked notbook.  See the log for datails."));
				e.printStackTrace();
				logger.log(logger.LOW, tr("EDAM SystemException synchronizing linked notbook.  See the log for datails") +e.getMessage());
			} catch (EDAMNotFoundException e) {
				syncError = true;
				status.message.emit(tr("Notebook URL not found. Removing notobook ") +book.getShareName());
				conn.getNotebookTable().deleteLinkedTags(book.getGuid());
				conn.getLinkedNotebookTable().expungeNotebook(book.getGuid(), false);
				logger.log(logger.LOW, tr("Notebook URL not found. Removing notobook ") +e.getMessage());
			} catch (TException e) {
				syncError = true;
				status.message.emit(tr("EDAM TException synchronizing linked notbook.  See the log for datails."));
				e.printStackTrace();
				logger.log(logger.LOW, tr("EDAM TException synchronizing linked notbook.  See the log for datails." )+e.getMessage());
			}
		}
		logger.log(logger.EXTREME, "leaving syncLinkedNotebook");
	}
	// Sync remote tags
	private void syncLinkedTags(List<Tag> tags, String notebookGuid) {
		logger.log(logger.EXTREME, "Entering SyncRunner.syncRemoteTags");
		if (tags != null) {
			for (int i=0; i<tags.size() && keepRunning; i++) {
				conn.getTagTable().syncLinkedTag(tags.get(i), notebookGuid, false);
			}
		}
		logger.log(logger.EXTREME, "Leaving SyncRunner.syncRemoteTags");
	}
	
	// Sync notebooks from a linked notebook
	private void syncRemoteLinkedNotebooks(Client noteStore, List<Notebook> notebooks, boolean readOnly, LinkedNotebook linked) {
		logger.log(logger.EXTREME, "Entering SyncRunner.syncRemoteNotebooks");
		if (notebooks != null) {
			for (int i=0; i<notebooks.size() && keepRunning; i++) {
				try {
					logger.log(logger.EXTREME, "auth token:" +linkedAuthResult.getAuthenticationToken());
					if (!linkedAuthResult.getAuthenticationToken().equals("")) {
						SharedNotebook s = noteStore.getSharedNotebookByAuth(linkedAuthResult.getAuthenticationToken());
						logger.log(logger.EXTREME, "share key:"+s.getShareKey() +" notebookGuid" +s.getNotebookGuid());
						conn.getLinkedNotebookTable().setNotebookGuid(s.getShareKey(), s.getNotebookGuid());
						readOnly = !s.isNotebookModifiable();
					} else {
						readOnly = true;
					}
					notebooks.get(i).setName(linked.getShareName());
					notebooks.get(i).setDefaultNotebook(false);
					conn.getNotebookTable().syncLinkedNotebook(notebooks.get(i), false, readOnly); 
				} catch (EDAMUserException e) {
					readOnly = true;
					e.printStackTrace();
				} catch (EDAMNotFoundException e) {
					readOnly = true;
					e.printStackTrace();
				} catch (EDAMSystemException e) {
					if (e.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
						limitSignal.rateLimitReached.emit(e.getRateLimitDuration());
					}
					readOnly = true;
					e.printStackTrace();
				} catch (TException e) {
					readOnly = true;
					e.printStackTrace();
				}

			}
		}			
		logger.log(logger.EXTREME, "Leaving SyncRunner.syncRemoteNotebooks");
	}

	private void findNewLinkedTags(Client noteStore, List<Note> newNotes, String token) {
		if (newNotes == null)
			return;
		for (int i=0; i<newNotes.size(); i++) {
			Note n = newNotes.get(i);
			for (int j=0; j<n.getTagGuidsSize(); j++) {
				String tag = n.getTagGuids().get(j);
				if (!conn.getTagTable().exists(tag)) {
					Tag newTag;
					try {
						newTag = noteStore.getTag(token, tag);
						conn.getTagTable().addTag(newTag, false);
					} catch (EDAMUserException e) {
						e.printStackTrace();
					} catch (EDAMSystemException e) {
						if (e.getErrorCode() == EDAMErrorCode.RATE_LIMIT_REACHED) {
							limitSignal.rateLimitReached.emit(e.getRateLimitDuration());
						}
						e.printStackTrace();
					} catch (EDAMNotFoundException e) {
						e.printStackTrace();
					} catch (TException e) {
						e.printStackTrace();
					}
					
				}
			}
		}
	}

	// Synchronize changes locally done to linked notes
	private void syncLocalLinkedNoteChanges(Client noteStore, LinkedNotebook book) {
		logger.log(logger.EXTREME, "Entering SyncRunner.synclocalLinkedNoteChanges");
		String notebookGuid = conn.getLinkedNotebookTable().getNotebookGuid(book.getGuid());
		logger.log(logger.EXTREME, "Finding changes for " +book.getShareName() +":" +book.getGuid() + ":" +notebookGuid);
		List<Note> notes = conn.getNoteTable().getDirtyLinked(notebookGuid);
		logger.log(logger.EXTREME, "Number of changes found: " +notes.size());
		for (int i=0; i<notes.size(); i++) {
			logger.log(logger.EXTREME, "Calling syncLocalNote with key " +linkedAuthResult.getAuthenticationToken());
			syncLocalNote(noteStore, notes.get(i), linkedAuthResult.getAuthenticationToken());
		}
		logger.log(logger.EXTREME, "Leaving SyncRunner.synclocalLinkedNoteChanges");
	}

}
