# gosync is an open source Google Drive(TM) sync application for Linux
# modify it under the terms of the GNU General Public License
#
# Copyright (C) 2015 Himanshu Chauhan
# This program is free software; you can redistribute it and/or
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#

import sys, os, wx, ntpath, threading, hashlib, time, copy, io
import shutil
if sys.version_info > (3,):
    long = int
    import urllib.request
else:
    import urllib2

from os.path import expanduser
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler
from threading import Thread
from apiclient.errors import HttpError
from apiclient import errors
from apiclient.http import MediaFileUpload
from apiclient.http import MediaIoBaseDownload
import logging
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
import json, pickle
try :
	from .GoSyncDriveTree import GoogleDriveTree
	from .defines import *
	from .GoSyncEvents import *
	from .GoSyncUtils import *
except (ImportError, ValueError):
	from GoSyncDriveTree import GoogleDriveTree
	from defines import *
	from GoSyncEvents import *
	from GoSyncUtils import *

class ClientSecretsNotFound(RuntimeError):
    """Client secrets file was not found"""
class FileNotFound(RuntimeError):
    """File was not found on google drive"""
class FolderEmpty(Exception):
    """Folder is empty"""
class FolderNotFound(RuntimeError):
    """Folder on Google Drive was not found"""
    def __init__(self, err_folder):
        self.e_folder = err_folder
    def __str__(self):
        return self.e_folder

class UnknownError(RuntimeError):
    """Unknown/Unexpected error happened"""
class MD5ChecksumCalculationFailed(RuntimeError):
    """Calculation of MD5 checksum on a given file failed"""
class RegularFileUploadFailed(RuntimeError):
    """Upload of a regular file failed"""
class RegularFileTrashFailed(RuntimeError):
    """Could not move file to trash"""
class FileListQueryFailed(RuntimeError):
    """The query of file list failed"""
class ConfigLoadFailed(RuntimeError):
    """Failed to load the GoSync configuration file"""
class AuthenticationFailed(RuntimeError):
    """Failed to authenticate with google"""
class InternetNotReachable(RuntimeError):
    """Failed to connect to the internet"""

audio_file_mimelist = ['audio/mpeg', 'audio/x-mpeg-3', 'audio/mpeg3', 'audio/aiff', 'audio/x-aiff', 'audio/m4a', 'audio/mp4', 'audio/flac', 'audio/mp3']
movie_file_mimelist = ['video/mp4', 'video/x-msvideo', 'video/mpeg', 'video/flv', 'video/quicktime']
image_file_mimelist = ['image/png', 'image/jpeg', 'image/jpg', 'image/tiff']
document_file_mimelist = ['application/powerpoint', 'applciation/mspowerpoint', \
                              'application/x-mspowerpoint', 'application/pdf', \
                              'application/x-dvi']
google_docs_mimelist = ['application/vnd.google-apps.spreadsheet', \
                            'application/vnd.google-apps.sites', \
                            'application/vnd.google-apps.script', \
                            'application/vnd.google-apps.presentation', \
                            'application/vnd.google-apps.fusiontable', \
                            'application/vnd.google-apps.form', \
                            'application/vnd.google-apps.drawing', \
                            'application/vnd.google-apps.document', \
                            'application/vnd.google-apps.map']

Default_Log_Level = 3

class GoSyncModel(object):
    def __init__(self):
        self.calculatingDriveUsage = False
        self.driveAudioUsage = 0
        self.driveMoviesUsage = 0
        self.drivePhotoUsage = 0
        self.driveDocumentUsage = 0
        self.driveOthersUsage = 0
        #self.totalFilesToCheck = 0
        self.Log_Level = 3 # 1=error, 2=info, 3=debug
        self.savedTotalSize = 0
        self.fcount = 0
        self.updates_done = 0
        self.syncing_now = False
        self.force_usage_calculation = False
        self.initial_run = True
        self.can_autostart = True
        self.auto_start_sync = False
        self.sync_interval = 1800
        self.shutting_down = False
        self.use_system_notif = True

        self.config_path = os.path.join(os.environ['HOME'], ".gosync")
        self.credential_file = os.path.join(self.config_path, "credentials.json")
        self.client_pickle = os.path.join(self.config_path, "token.pickle")
        self.settings_file = os.path.join(self.config_path, "settings.yaml")
        self.base_mirror_directory = os.path.join(os.environ['HOME'], "Google Drive")
        self.client_secret_file = os.path.join(os.environ['HOME'], '.gosync', 'client_secrets.json')
        self.sync_selection = []
        self.config_file = os.path.join(os.environ['HOME'], '.gosync', 'gosyncrc')
        self.config_dict = {}
        self.account_dict = {}
        self.drive_usage_dict = {}
        self.config=None
        self.LargeFileSize = 250000000

        self.logger = logging.getLogger(APP_NAME)
        self.logger.setLevel(logging.DEBUG)
        fh = logging.FileHandler(os.path.join(os.environ['HOME'], 'GoSync.log'))
        fh.setLevel(logging.DEBUG)
        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
        fh.setFormatter(formatter)
        self.logger.addHandler(fh)

        self.SendlToLog(3,"Initialize - Started Initialize")

        if not os.path.exists(self.config_path):
            os.mkdir(self.config_path, 0o0755)

        if not os.path.exists(self.credential_file):
        #check if Credentials.json file exists
            self.SendlToLog(3,"Initialize - Missing Credentials File")
            if (self.AskChooseCredentialsFile()):
                self.SendlToLog(3,"Initialize - Ask Location of Credentials File")
                if (self.getCredentialFile() == False) :
                    self.SendlToLog(1,"Initialize - Failled to load Credentials File")
                    raise ClientSecretsNotFound()
            else:
                self.SendlToLog(3,"Initialize - Declined to Locate Credentials File")
                raise ClientSecretsNotFound()
        self.SendlToLog(2,"Initialize - Completed Credentials Verification")

        self.SendlToLog(2, "Initialize - Saving credentials")
        if not os.path.exists(self.settings_file) or not os.path.isfile(self.settings_file):
            sfile = open(self.settings_file, 'w')
            sfile.write("save_credentials: False")
            sfile.write("\n")
            sfile.write("save_credentials_file: ")
            sfile.write(self.credential_file)
            sfile.write("\n")
            sfile.write("save_credentials_backend: file\n")
            sfile.close()

        self.SendlToLog(2, "Initialize - starting oberserver")
        self.observer = Observer()
        self.SendlToLog(2, "Initialize - Going for authentication")
        self.DoAuthenticate()
        self.about_drive = self.drive.about().get(fields='user, storageQuota').execute()
        self.SendlToLog(2,"Initialize - Completed Drive Quota Execution")

        self.user_email = self.about_drive['user']['emailAddress']
        self.SendlToLog(3,"Initialize - Completed Account Information Load")

        self.tree_pickle_file = os.path.join(self.config_path, 'gtree-' + self.user_email + '.pick')

        self.SendlToLog(3, "Initialize - Loaded tree pickle")

        if not os.path.exists(self.config_file):
            self.SendlToLog(3, "Initialize - Creating default config file")
            self.CreateDefaultConfigFile()
            self.SendlToLog(3,"Initialize - Completed Default Config File Creation")
#todo : add default config logic in method
        try:
            self.SendlToLog(3, "Initialize - Trying to load config file %s" % self.config_file)
            self.LoadConfig()
            self.SendlToLog(3,"Initialize - Read %s as base mirror" % self.base_mirror_directory)
            if not os.path.exists(self.base_mirror_directory):
                os.mkdir(self.base_mirror_directory, 0o0755)

            #create subdir linked to active account
            self.mirror_directory = os.path.join(self.base_mirror_directory, self.user_email)
            if not os.path.exists(self.mirror_directory):
                os.mkdir(self.mirror_directory, 0o0755)

            self.SendlToLog(3,"Initialize - Completed mirror_directory validation")
            self.SendlToLog(3,"Initialize - Mirror Directory: %s" % self.mirror_directory)
            if not self.sync_selection:
                self.can_autostart = False
        except:
            self.SendlToLog(2, "Initialize - Exception during configuration load")
            raise

        self.SendlToLog(3,"Initialize - Completed Config File Load")


#todo : confirm this is to monitor file changes
        self.iobserv_handle = self.observer.schedule(FileModificationNotifyHandler(self), self.mirror_directory, recursive=True)
        self.sync_lock = threading.Lock()
        self.sync_thread = threading.Thread(target=self.run)
        self.usage_calc_thread = threading.Thread(target=self.calculateUsage)
        self.sync_thread.daemon = True
        self.usage_calc_thread.daemon = True
        self.syncRunning = threading.Event()
        self.syncRunning.clear()
        self.usageCalculateEvent = threading.Event()
        self.usageCalculateEvent.set()

        if not os.path.exists(self.tree_pickle_file):
            self.driveTree = GoogleDriveTree()
            #Until driveTree is present, GoSync cannot autostart.
            self.can_autostart = False
        else:
            try:
                self.driveTree = pickle.load(open(self.tree_pickle_file, "rb"))
            except:
                self.driveTree = GoogleDriveTree()
                self.can_autostart = False
        self.SendlToLog(3,"Initialize - Completed GoogleDriveTree File")
        self.SendlToLog(3,"Initialize - Completed Initialize")

# Sends Log Level Message to Log File
# Depends on Log_Level constant
    def SendlToLog(self, LogType, LogMsg):
        if (self.Log_Level >= LogType) :
            if (LogType == 3) :
                self.logger.debug(LogMsg)
            if (LogType == 2) :
                self.logger.info(LogMsg)
            if (LogType == 1) :
                self.logger.error(LogMsg)

    def SetTheBallRolling(self):
        #if we can autostart and user has selected autostart
        #then auto start the sync
        if self.can_autostart and self.auto_start_sync:
            self.SendlToLog(2, "SetTheBallRolling: Starting sync")
            self.StartSync()
        else:
            self.StopSync()

        self.sync_thread.start()
        self.usage_calc_thread.start()
        self.observer.start()

    def StopTheShow(self):
        self.shutting_down = True
        self.observer.unschedule_all()
        # Wakeup the threads if they are sleeping
        # so that they can exit
        self.usageCalculateEvent.set()
        self.sync_thread.join()
        self.usage_calc_thread.join()

    def IsUserLoggedIn(self):
        return self.is_logged_in

    def HashOfFile(self, abs_filepath):
        data = open(abs_filepath, "rb").read()
        return hashlib.md5(data).hexdigest()

    def CreateDefaultConfigFile(self):
        f = open(self.config_file, 'w')
        self.config_dict['Sync Selection'] = [['root', '']]
        self.config_dict['SyncInterval'] = 1800
        self.config_dict['AutoStartSync'] = False
        self.config_dict['UseSystemNotif'] = True
        self.config_dict['BaseMirrorDirectory'] = self.base_mirror_directory
        self.config_dict['LogLevel'] = Default_Log_Level
        self.account_dict[self.user_email] = self.config_dict
        json.dump(self.account_dict, f)
        f.close()

    def LoadConfig(self):
        try:
            f = open(self.config_file, 'r')
            try:
                self.config = json.load(f)
                try:
                    self.config_dict = self.config[self.user_email]
                    if self.config_dict['SyncInterval']:
                        self.sync_interval = self.config_dict['SyncInterval']
                        if self.sync_interval < 30 or self.sync_interval > (24*60*60):
                            self.SendlToLog(3, "LoadConfig: Setting sync to default value")
                            self.sync_interval = 1800

                    self.SendlToLog(3, "Sync Interval: %d seconds" % self.sync_interval)

                    if self.config_dict['LogLevel']:
                        lvl = self.config_dict['LogLevel']
                        if lvl > 3:
                            self.SendlToLog(2, "LoadConfig: Setting log level to default (%s)" % Default_Log_Level)
                            self.Log_Level = Default_Log_Level
                        else:
                            self.SendlToLog(2, "LoadConfig: Log level is %d" % lvl)
                            self.Log_Level = lvl
                    else:
                        self.SendlToLog(2, "LoadConfig: Not log level set. Setting to default")
                        self.Log_Level = Default_Log_Level

                    #Load the base mirror selected by user.
                    if self.config_dict['BaseMirrorDirectory']:
                        self.base_mirror_directory = self.config_dict['BaseMirrorDirectory']
                        self.SendlToLog(3, "Initialize - Base Mirror: %s" % self.base_mirror_directory)
                    else:
                        self.SendlToLog(3, "LoadConfig: BaseMirrorDirectory not set")

                    self.sync_selection = self.config_dict['Sync Selection']
                    if not self.config_dict['AutoStartSync']:
                        self.SendlToLog(2, "LoadConfig: Autostart of sync is disabled")
                        self.auto_start_sync = False
                    else:
                        self.auto_start_sync = self.config_dict['AutoStartSync']
                        if self.auto_start_sync:
                            self.SendlToLog(2, "LoadConfig: Autostart is enabled")
                        else:
                            self.SendlToLog(2, "LoadConfig: Autostart is disabled")
                    try:
                        self.drive_usage_dict = self.config_dict['Drive Usage']
                        #self.totalFilesToCheck = self.drive_usage_dict['Total Files']
                        self.savedTotalSize = self.drive_usage_dict['Total Size']
                        self.driveAudioUsage = self.drive_usage_dict['Audio Size']
                        self.driveMoviesUsage = self.drive_usage_dict['Movies Size']
                        self.driveDocumentUsage = self.drive_usage_dict['Document Size']
                        self.drivePhotoUsage = self.drive_usage_dict['Photo Size']
                        self.driveOthersUsage = self.drive_usage_dict['Others Size']

                        # TODO: This isn't right place for UI components
                        self.use_system_notif = self.config_dict['UseSystemNotif']
                        if self.use_system_notif is None:
                            self.use_system_notif = True
                    except:
                        pass
                except:
                    pass

                f.close()
            except:
                raise ConfigLoadFailed()
        except:
            raise ConfigLoadFailed()

    def SaveConfig(self):
        f = open(self.config_file, 'w')
        f.truncate()
        self.config_dict['AutoStartSync'] = self.auto_start_sync
        self.config_dict['BaseMirrorDirectory'] = self.base_mirror_directory
        self.config_dict['SyncInterval'] = self.sync_interval
        self.config_dict['UseSystemNotif'] = self.use_system_notif
        self.config_dict['LogLevel'] = self.Log_Level
        if not self.sync_selection:
            self.config_dict['Sync Selection'] = [['root', '']]

        self.account_dict[self.user_email] = self.config_dict

        json.dump(self.account_dict, f)
        f.close()

    def AskChooseCredentialsFile(self):
        dial = wx.MessageDialog(None, 'No Credentials file was found!\n\nDo you want to load one?\n',
                                'Error', wx.YES_NO | wx.ICON_EXCLAMATION)
        res = dial.ShowModal()
        if res == wx.ID_YES:
            return True
        else:
            return false


    def getCredentialFile(self):
        # ask for the Credential file and save it in Config directory then return True
        defDir, defFile = '', ''
        dlg = wx.FileDialog(None,
               'Load Credential File',
                 '~', 'Credentials.json',
                 'json files (*.json)|*.json',
                 wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
        if dlg.ShowModal() == wx.ID_CANCEL:
            return False
        try:
            shutil.copy(dlg.GetPath(), self.credential_file)
            return True
        except:
            return False

    def DoAuthenticate(self):
        try:
            # If modifying these scopes, delete the file token.pickle.
            SCOPES = ['https://www.googleapis.com/auth/drive']
            creds = None
            # The file token.pickle stores the user's access and refresh tokens, and is
            # created automatically when the authorization flow completes for the first
            # time.
            if os.path.exists(self.client_pickle):
                with open(self.client_pickle, 'rb') as token:
                    self.SendlToLog(2, "Authenticate - Loading pickle file")
                    try:
                        creds = pickle.load(token)
                        self.SendlToLog(2, "Authenticate - Loading pickle file: SUCCESS")
                    except:
                        self.SendlToLog(2, "Authenticate - Failed to load pickle file")
                        creds = None

            # If there are no (valid) credentials available, let the user log in.
            if not creds or not creds.valid:
                if creds and creds.expired and creds.refresh_token:
                    self.SendlToLog(2, "Authenticate - expired. Refreshing")
                    creds.refresh(Request())
                else:
                    self.SendlToLog(2, "Authenticate - New authentication")
                    self.SendlToLog(2, "Authenticate - File %s" % (self.credential_file))
                    flow = InstalledAppFlow.from_client_secrets_file(self.credential_file, SCOPES)
                    self.SendlToLog(2, "Authenticate - running local server")
                    creds = flow.run_local_server(port=0)
                self.SendlToLog(2, "Authenticate - Saving pickle file")
                # Save the credentials for the next run
                with open(self.client_pickle, 'wb') as token:
                    pickle.dump(creds, token)

            self.SendlToLog(2, "Authenticate - Building service")
            try:
                service = build('drive', 'v3', credentials=creds)
                self.SendlToLog(2, "Authenticate - service built successfully!")
            except:
                self.SendlToLog(2, "Authenticate - service built failed. Going for re-authentication")
                try:
                    flow = InstalledAppFlow.from_client_secrets_file(self.credential_file, SCOPES)
                    creds = flow.run_local_server(port=0)
                    service = build('drive', 'v3', credentials=creds)
                except:
                    raise AuthenticationFailed()

            self.drive = service
            self.is_logged_in = True
            return service
        except:
            dial = wx.MessageDialog(None, "Authentication Rejected!\n",
                                    'Information', wx.ID_OK | wx.ICON_EXCLAMATION)
            dial.ShowModal()
            self.is_logged_in = False
            pass

    def DoUnAuthenticate(self):
            self.do_sync = False
            self.observer.unschedule(self.iobserv_handle)
            self.iobserv_handle = None
            os.remove(self.credential_file)
            self.is_logged_in = False

    def DriveInfo(self):
        return self.about_drive

    def PathLeaf(self, path):
        head, tail = ntpath.split(path)
        return tail or ntpath.basename(head)


    def CreateDirectoryInParent(self, dirname, parent_id='root'):
        file_metadata = {'name': dirname,
                        'mimeType':'application/vnd.google-apps.folder'}
        file_metadata['parents'] = [parent_id]
        upfile = self.drive.files().create(body=file_metadata, fields='id').execute()

    def CreateDirectoryByPath(self, dirpath):
        self.SendlToLog(3,"create directory: %s\n" % dirpath)
        drivepath = dirpath.split(self.mirror_directory+'/')[1]
        basepath = os.path.dirname(drivepath)
        dirname = self.PathLeaf(dirpath)

        try:
            f = self.LocateFolderOnDrive(drivepath)
            return
        except FolderNotFound:
            if basepath == '':
                self.CreateDirectoryInParent(dirname)
            else:
                try:
                    parent_folder = self.LocateFolderOnDrive(basepath)
                    self.CreateDirectoryInParent(dirname, parent_folder['id'])
                except:
                    errorMsg = "Failed to locate directory path %s on drive.\n" % basepath
                    self.SendlToLog(1,errorMsg)
                    dial = wx.MessageDialog(None, errorMsg, 'Directory Not Found',
                                            wx.ID_OK | wx.ICON_EXCLAMATION)
                    dial.ShowModal()
                    return
        except FileListQueryFailed:
            errorMsg = "Server Query Failed!\n"
            self.SendlToLog(1,errorMsg)
            dial = wx.MessageDialog(None, errorMsg, 'Directory Not Found',
                                    wx.ID_OK | wx.ICON_EXCLAMATION)
            dial.ShowModal()
            return

    def CreateRegularFile(self, file_path, parent='root', uploaded=False):
        self.SendlToLog(3,"Create file %s\n" % file_path)
        filename = self.PathLeaf(file_path)
        file_metadata = {'name': filename}
        file_metadata['parents'] = [parent]
        media = MediaFileUpload(file_path, resumable=True)
        upfile = self.drive.files().create(body=file_metadata,
                                    media_body=media,
                                    fields='id').execute()

    def GetRelativeFolder(self, file_path, IsFolder=False):
        if IsFolder:
            return file_path.split(self.mirror_directory+'/')[1]
        else:
            drivepath = file_path.split(self.mirror_directory+'/')[1]
            return os.path.dirname(drivepath)

    def UploadFolder(self, a_dirpath, addToTree=True):
        self.SendlToLog(3, "UploadFolder: %s" % a_dirpath)
        dirpath=self.GetRelativeFolder(a_dirpath, True)
        self.SendlToLog(3, "UploadFolder: %s (Relative: %s)" % (a_dirpath, dirpath))

        self.UploadFile(a_dirpath)
        while True:
            try:
                self.SendlToLog(3, "UploadFolder: Locating new directory %s in remote"
                                % dirpath)
                nf = self.LocateFolderOnDrive(dirpath)
                self.sync_selection.append([dirpath, nf['id']])
                self.SendlToLog(3, "UploadFolder: New Child: %s ID: %s" % (nf['name'], nf['id']))
                self.SendlToLog(2, "UploadFolder: Added newly uploaded directory %s in sync list" % dirpath)
                parent = os.path.dirname(dirpath)
                if parent == '':
                    parent = 'root'
                    self.SendlToLog(3, "UploadFolder: Find new child's parent: %s, child: %s (%s)"
                                    % (parent, nf['name'], nf['id']))
                    self.driveTree.AddFolder(parent, nf['id'], nf['name'], nf)
                    self.SendlToLog(2, "UploadFolder: Added to tree")
                    GoSyncEventController().PostEvent(GOSYNC_EVENT_CALCULATE_USAGE_DONE, 0)
                else:
                    parent_f = self.LocateFolderOnDrive(parent)
                    self.SendlToLog(3, "UploadFolder: Find new child's parent: %s (%s)"
                                    % (parent_f['name'], parent))
                    self.driveTree.AddFolder(parent_f['id'], nf['id'], nf['name'], nf)
                    self.SendlToLog(3, "UploadFolder: Added new child %s to parent %s"
                                    % (nf['name'], parent_f['name']))
                    GoSyncEventController().PostEvent(GOSYNC_EVENT_CALCULATE_USAGE_DONE, 0)
                break
            except InternetNotReachable:
                self.SendlToLog(1, "UploadFolder: Internet down")
                while True:
                    time.sleep(5)
                    if not self.IsInternetReachable():
                        continue
                    break
                continue
            except FileListQueryFailed:
                time.sleep(5)
                continue
            except:
                self.SendlToLog(1, "UploadFolder: Unknown exception: Trying to get ID of new directory")
                raise

    def UploadFile(self, file_path):
        self.SendlToLog(3, "UploadFile: %s" % file_path)
        if os.path.isfile(file_path):
            drivepath = file_path.split(self.mirror_directory+'/')[1]
            self.SendlToLog(3,"file: %s drivepath is %s\n" % (file_path, drivepath))
            try:
                f = self.LocateFileOnDrive(drivepath)
                self.SendlToLog(3,'Found file %s on remote (dpath: %s)\n' % (f['name'], drivepath))
                newfile = False
                self.SendlToLog(3,'Checking if they are same... ')
                if f['md5Checksum'] == self.HashOfFile(file_path):
                    self.SendlToLog(3,'yes\n')
                    return
                else:
                    self.SendlToLog(3,'no\n')
            except (FileNotFound, FolderNotFound):
                self.SendlToLog(3,"A new file!\n")
                newfile = True
            except:
                raise

            dirpath = os.path.dirname(drivepath)
            if dirpath == '':
                self.SendlToLog(3,'Creating %s file in root\n' % file_path)
                try:
                    GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_UPDATE, {'UpLoading %s in root' % file_path})
                    self.CreateRegularFile(file_path, 'root', newfile)
                except:
                    self.SendlToLog(1, "CreateRegularFile: Failed to upload %s" % file_path)
                    raise RegularFileUploadFailed()
            else:
                try:
                    f = self.LocateFolderOnDrive(dirpath)
                    self.CreateRegularFile(file_path, f['id'], newfile)
                except FolderNotFound:
                    # We are coming from premise that upload comes as part
                    # of observer. So before notification of this file's
                    # creation happens, a notification of its parent directory
                    # must have come first.
                    # So,
                    # Folder not found? That cannot happen. Can it?
                    self.SendlToLog(1, "CreateRegularFile: Failed to upload %s in %s" % file_path, dir_path)
                    raise RegularFileUploadFailed()
        else:
            self.CreateDirectoryByPath(file_path)

    def UploadObservedFile(self, file_path):
        if self.IsSyncRunning():
            self.SendlToLog(3, "UploadObservedFile: File %s is created but sync is running. Possibly created by sync. Skipping."
                            % file_path)
            return

        self.sync_lock.acquire()
        try:
            if os.path.isdir(file_path):
                GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_UPDATE,
                                                  {"Creating Folder: %s" % self.GetRelativeFolder(file_path)})
                self.UploadFolder(file_path)
            else:
                GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_UPDATE,
                                                  {"Uploading File: %s" % self.GetRelativeFolder(file_path, False)})
                self.UploadFile(file_path)
        except InternetNotReachable as ie:
            self.SendlToLog(1, "UploadObservedFile - Internet is down")
        except:
            self.SendlToLog(1, "UploadObservedFile - Failed to upload file %s\n" % file_path)

        self.SendlToLog(3, "UploadObservedFile - Releasing lock")
        self.sync_lock.release()

    def RenameFile(self, file_object, new_title):
        try:
            file = {'name': new_title}

            updated_file = self.drive.files().update( body= file,
                                                         fileId=file_object['id'],
                                                         fields='id, appProperties').execute()
            return updated_file
        except errors.HttpError as error:
            self.SendlToLog(1,'An error occurred while renaming file: %s' % error)
            return None
        except:
            self.logger.exception('An unknown error occurred file renaming file\n')
            return None

    def RenameObservedFile(self, file_path, new_name):
        self.sync_lock.acquire()
        drive_path = file_path.split(self.mirror_directory+'/')[1]
        self.SendlToLog(3,"RenameObservedFile: Rename %s to new name %s\n"
                          % (file_path, new_name))
        try:
            ftd = self.LocateFileOnDrive(drive_path)
            nftd = self.RenameFile(ftd, new_name)
            if not nftd:
                self.SendlToLog(1,"File rename failed\n")
        except:
            self.logger.exception("Could not locate file on drive.\n")

        self.sync_lock.release()

    def TrashFile(self, file_object):
        try:
            file_metadata = {'trashed':True}
            self.drive.files().update(body=file_metadata,fileId=file_object['id']).execute()
            self.SendlToLog(3,{"TRASH_FILE: File %s deleted successfully.\n" % file_object['name']})
        except errors.HttpError as error:
            self.SendlToLog(1,"TRASH_FILE: HTTP Error\n")
            raise RegularFileTrashFailed()

    def TrashFileCallback(self, Folder):
        self.SendlToLog(3, "TrashFileCallback: Folder: %s being deleted" % Folder.GetPath())
        if Folder and Folder.GetPath() in self.sync_selection:
            self.sync_selection.remove(Folder.GetPath())
            self.SendlToLog(3, "TrashFileCallback: Folder %s deleted from sync list" % Folder.GetPath())

    def TrashObservedFile(self, file_path):
        if self.IsSyncRunning():
            self.SendlToLog(3, "TrashObservedFile: File %s Deleted but sync is running" % file_path)
            return

        self.sync_lock.acquire()
        drive_path = file_path.split(self.mirror_directory+'/')[1]
        self.SendlToLog(3,{"TRASH_FILE: dirpath to delete: %s\n" % drive_path})
        try:
            ftd = self.LocateFileOnDrive(drive_path)
            if not ftd:
                self.SendlToLog(1,{"TRASH_FILE: invalid file handle for %s\n" % drive_path})

            try:
                if ftd['mimeType'] == 'application/vnd.google-apps.folder':
                    self.SendlToLog(3, "Deleting folder %s (%s) from local drive tree"
                                    % (self.GetRelativeFolder(file_path), ftd['id']))
                    self.driveTree.DeleteFolder(ftd['id'], self.TrashFileCallback)
                    GoSyncEventController().PostEvent(GOSYNC_EVENT_CALCULATE_USAGE_DONE, 0)
                self.TrashFile(ftd)
            except RegularFileTrashFailed:
                self.SendlToLog(1,{"TRASH_FILE: Failed to move file %s to trash\n" % drive_path})
                raise
            except:
                raise
        except (FileNotFound, FileListQueryFailed, FolderNotFound):
            self.SendlToLog(1,{"TRASH_FILE: Failed to locate %s file on drive\n" % drive_path})
            pass
        except:
            self.SendlToLog(1,{"TRASH_FILE: Unknown exception for file %s (%s)\n" % (drive_path, file_path)})

        self.sync_lock.release()

    def MoveFile(self, src_file, dst_folder='root', src_folder='root'):
        try:
            if dst_folder != 'root':
                did = dst_folder['id']
            else:
                did = 'root'

            if src_folder != 'root':
                sid = src_folder['id']
            else:
                sid = 'root'

            updated_file = self.drive.files().update(fileId=src_file['id'],
                                    addParents=did,
                                    removeParents=sid,
                                    fields='id, parents').execute()

        except:
            self.logger.exception("move failed\n")

    def MoveObservedFile(self, src_path, dest_path):
        from_drive_path = src_path.split(self.mirror_directory+'/')[1]
        to_drive_path = os.path.dirname(dest_path.split(self.mirror_directory+'/')[1])

        self.SendlToLog(3,"Moving file %s to %s\n" % (from_drive_path, to_drive_path))

        try:
            ftm = self.LocateFileOnDrive(from_drive_path)
            self.SendlToLog(3,"MoveObservedFile: Found source file on drive\n")
            if os.path.dirname(from_drive_path) == '':
                sf = 'root'
            else:
                sf = self.LocateFolderOnDrive(os.path.dirname(from_drive_path))
            self.SendlToLog(3,"MoveObservedFile: Found source folder on drive\n")
            try:
                if to_drive_path == '':
                    df = 'root'
                else:
                    df = self.LocateFolderOnDrive(to_drive_path)
                self.SendlToLog(3,"MoveObservedFile: Found destination folder on drive\n")
                try:
                    self.SendlToLog(3,"MovingFile() ")
                    self.MoveFile(ftm, df, sf)
                    self.SendlToLog(3,"done\n")
                except (Unkownerror, FileMoveFailed):
                    self.SendlToLog(1,"MovedObservedFile: Failed\n")
                    return
                except:
                    self.SendlToLog(1,"?????\n")
                    return
            except FolderNotFound:
                self.SendlToLog(1,"MoveObservedFile: Couldn't locate destination folder on drive.\n")
                return
            except:
                self.SendlToLog(1,"MoveObservedFile: Unknown error while locating destination folder on drive.\n")
                return
        except FileNotFound:
                self.SendlToLog(1,"MoveObservedFile: Couldn't locate file on drive.\n")
                return
        except FileListQueryFailed:
            self.SendlToLog(1,"MoveObservedFile: File Query failed. aborting.\n")
            return
        except FolderNotFound:
            self.SendlToLog(1,"MoveObservedFile: Folder not found\n")
            return
        except:
            self.SendlToLog(1,"MoveObservedFile: Unknown error while moving file.\n")
            return

    def HandleMovedFile(self, src_path, dest_path):
        drive_path1 = os.path.dirname(src_path.split(self.mirror_directory+'/')[1])
        drive_path2 = os.path.dirname(dest_path.split(self.mirror_directory+'/')[1])

        if drive_path1 == drive_path2:
            self.SendlToLog(3,"Rename file\n")
            self.RenameObservedFile(src_path, self.PathLeaf(dest_path))
        else:
            self.SendlToLog(3,"Move file\n")
            self.MoveObservedFile(src_path, dest_path)

    #################################################
    ####### DOWNLOAD SECTION (Syncing local) #######
    #################################################


#### LocateFileInFolder
    def LocateFileInFolder(self, filename, parent='root'):
        try:
            self.SendlToLog(3, "LocateFileInFolder - Querying remote\n")
            file_list = self.MakeFileListQuery("'%s' in parents and trashed=false" % parent)
            for f in file_list:
                if f['name'] == filename:
                    self.SendlToLog(3, "LocateFileInFolder - Found\n")
                    return f
            self.SendlToLog(1, "LocateFileInFolder - %s not found\n" % filename)
            raise FileNotFound()
        except InternetNotReachable:
            self.SendlToLog(1, "Internet is down\n")
            raise
        except:
            self.SendlToLog(3, "Raising FileNotFound\n")
            raise FileNotFound()

#### LocateFileOnDrive
    def LocateFileOnDrive(self, abs_filepath):
        dirpath = os.path.dirname(abs_filepath)
        filename = self.PathLeaf(abs_filepath)

        if dirpath != '':
            try:
                self.SendlToLog(3, "LocateFileOnDrive - locating %s directory on remote\n" % dirpath)
                f = self.LocateFolderOnDrive(dirpath)
                try:
                    self.SendlToLog(3, "LocateFileOnDrive - locating %s file\n" % f['id'])
                    fil = self.LocateFileInFolder(filename, f['id'])
                    self.SendlToLog(3, "LocateFileOnDrive - File found\n")
                    return fil
                except InternetNotReachable:
                    self.SendlToLog(3,"LocateFileOnDrive - Internet seems to be down!\n")
                    raise
                except FileNotFound:
                    self.SendlToLog(3,"LocateFileOnDrive - Local File (%s) not in remote." % filename)
                    raise
                except FileListQueryFailed:
                    self.SendlToLog(3,"LocateFileOnDrive - Locate File (%s) list query failed" % filename)
                    raise
            except InternetNotReachable:
                self.SendlToLog(3, "LocateFileOnDrive (Folder) - Internet seems to be down!\n")
                raise
            except FolderNotFound:
                self.SendlToLog(3,"LocateFileOnDrive - Local Folder (%s) not in remote" % dirpath)
                raise
            except FileListQueryFailed:
                self.SendlToLog(3,"LocateFileOnDrive - Locate Folder (%s) list query failed" % dirpath)
                raise
        else:
            try:
                #self.SendlToLog(3, "LocateFileOnDrive - locating %s file\n" % f['id'])
                fil = self.LocateFileInFolder(filename)
                self.SendlToLog(3, "LocateFileOnDrive - File found\n")
                return fil
            except InternetNotReachable:
                self.SendlToLog(3, "LocateFileOnDrive (File) - Internet seems to be down!\n")
                raise
            except FileNotFound:
                self.SendlToLog(3,"LocateFileOnDrive: Local File (%s) not in remote." % filename)
                raise
            except FileListQueryFailed:
                self.SendlToLog(3,"LocateFileOnDrive: File (%s) list query failed." % filename)
                raise
            except:
                self.SendlToLog(1,"LocateFileOnDrive: Unknown error in locating file (%s) in local folder (root)" % filename)
                raise

#### LocateFolderOnDrive
    def LocateFolderOnDrive(self, folder_path):
        """
        Locate and return the directory in the path. The complete path
        is walked and the last directory is returned. An exception is raised
        if the path walking fails at any stage.
        """
        dir_list = folder_path.split(os.sep)
        croot = 'root'
        for dir1 in dir_list:
            try:
                folder = self.GetFolderOnDrive(dir1, croot)
                if not folder:
                    raise FolderNotFound(folder_path)
            except:
                raise

            croot = folder['id']

        return folder

#### GetFolderOnDrive
    def GetFolderOnDrive(self, folder_name, parent='root'):
        """
        Return the folder with name in "folder_name" in the parent folder
        mentioned in parent.
        """
        self.SendlToLog(3,"GetFolderOnDrive: Checking Folder (%s) on (%s)" % (folder_name, parent))
        try:
            file_list = self.MakeFileListQuery("'%s' in parents and trashed=false"  % parent)
            for f in file_list:
                if f['name'] == folder_name and f['mimeType']=='application/vnd.google-apps.folder':
                    self.SendlToLog(3,"GetFolderOnDrive: Found Folder (%s) on (%s)" % (folder_name, parent))
                    return f
        except InternetNotReachable:
            raise
        except:
            return None

#### SyncLocalDirectory
    def SyncLocalDirectory(self):
        if not self.syncRunning.is_set() or self.shutting_down:
            self.SendlToLog(3,"SyncLocalDirectory: Sync has been paused. Aborting.\n")
            return

        self.SendlToLog(3,"### SyncLocalDirectory: - Sync Started")
        for root, dirs, files in os.walk(self.mirror_directory):
            if not self.IsDirectoryMonitored(root):
                self.SendlToLog(2, "SyncLocalDirectory - Directory %s is not monitored. Deleting Locally" % root)
                shutil.rmtree(root, ignore_errors=False, onerror=None)
                continue

            for names in files:
                while True:
                    if not self.syncRunning.is_set() or self.shutting_down:
                        self.SendlToLog(3,"SyncLocalDirectory: Sync has been paused. Aborting.\n")
                        return

                    GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_UPDATE, {"Checking Local: %s" % names})
                    try:
                        dirpath = os.path.join(root, names)
                        drivepath = dirpath.split(self.mirror_directory+'/')[1]
                        self.SendlToLog(3,"SyncLocalDirectory: Checking Local File (%s)" % drivepath)
                        f = self.LocateFileOnDrive(drivepath)
                        self.SendlToLog(3,"SyncLocalDirectory: Skipping Local File (%s) same as Remote\n" % dirpath)
                        break
                    except FileListQueryFailed:
                        # if the file list query failed, we can't delete the local file even if
                        # its gone in remote drive. Let the next sync come and take care of this
                        # Log the event though
                        self.SendlToLog(2,"SyncLocalDirectory: Remote File (%s) Check Failed. Aborting.\n" % dirpath)
                        return
                    except InternetNotReachable:
                        self.SendlToLog(2, "SyncLocalDirectory: Network is down!\n")
                        GoSyncEventController().PostEvent(GOSYNC_EVENT_INTERNET_UNREACHABLE, 1)
                        while True:
                            if self.IsInternetReachable():
                                GoSyncEventController().PostEvent(GOSYNC_EVENT_INTERNET_UNREACHABLE, 0)
                                self.SendlToLog(2, "SyncLocalDirectory: Network is up!\n")
                                break
                            else:
                                time.sleep(5)
                                continue
                    except:
                        if os.path.exists(dirpath) and os.path.isfile(dirpath):
                            self.SendlToLog(2,"SyncLocalDirectory: Uploading Local File (%s) - Not in Remote\n" % dirpath)
                            GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_UPDATE,
                                                              {"Uploading: %s" % self.GetRelativeFolder(dirpath, False)})
                            self.UploadFile(dirpath)

            for names in dirs:
                nf = None
                if not self.syncRunning.is_set() or self.shutting_down:
                    self.SendlToLog(3,"SyncLocalDirectory: Sync has been paused. Aborting.\n")
                    return

                if not self.IsDirectoryMonitored(names):
                    self.SendlToLog(2, "SyncLocalDirectory - Directory %s is not monitored. Deleting Locally" % root)
                    shutil.rmtree(root, ignore_errors=False, onerror=None)
                    continue

                self.SendlToLog(3, "Checking Local Folder: %s" % names)

                GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_UPDATE, {"Checking Local: %s" % names})
                try:
                    dirpath = os.path.join(root, names)
                    drivepath = dirpath.split(self.mirror_directory+'/')[1]
                    f = self.LocateFileOnDrive(drivepath)
                except FileListQueryFailed:
                    # if the file list query failed, we can't delete the local file even if
                    # its gone in remote drive. Let the next sync come and take care of this
                    # Log the event though
                    self.SendlToLog(2,"SyncLocalDirectory: Remote Folder (%s) Check Failed. Aborting.\n" % dirpath)
                    return
                except InternetNotReachable:
                    self.SendlToLog(1, "SyncLocalDirectory: Internet seems to be down!\n")
                    raise
                except:
                    if os.path.exists(dirpath) and os.path.isdir(dirpath):
                        self.SendlToLog(3,"SyncLocalDirectory: Uploading Local Folder (%s) - Not in Remote\n" % dirpath)
                        try:
                            GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_UPDATE, {"Creating Folder: %s" % f['name']})
                            self.UploadFolder(dirpath)
                        except:
                            self.SendlToLog(1, "SyncLocalDirectory: Failed to upload local folder (%s)\n" % dirpath)
                            raise

        self.SendlToLog(3,"### SyncLocalDirectory: - Sync Completed")


    #################################################
    ####### DOWNLOAD SECTION (Syncing remote) #######
    #################################################
    def IsInternetReachable(self, host='http://www.google.com'):
        try:
            if sys.version_info > (3,):
                urllib.request.urlopen(host)
            else:
                urllib2.urlopen(host)
            return True
        except:
            return False

    def MakeFileListQuery(self, query):
        retry = 0
        while True:
            try:
                page_token = None
                filelist = []
                #self.SendlToLog(3, "Query: %s\n" % query)
                while True:
                    response = self.drive.files().list(q=query,
                                                       spaces='drive',
                                                       fields='nextPageToken, files(id, name, mimeType, size, md5Checksum)',
                                                       pageToken=page_token).execute()
                    filelist.extend(response.get('files',[]))
                    page_token = response.get('nextPageToken', None)
                    if page_token is None:
                        break

                if not filelist:
                    if not self.IsInternetReachable():
                        self.SendlToLog(1, "Internet is down\n")
                        raise InternetNotReachable()
                    else:
                        self.SendlToLog(3, "Empty Folder\n")
                        return None
                else:
                    return filelist
            except HttpError as error:
                self.SendlToLog(1, "MakeFileListQuery - %s\n", error.resp.reason)
                if error.resp.status in [403, 500, 503, 429]:
                    self.SendlToLog(1, "MakeFileListQuery - Status: %d. (Retrying)\n", error.resp.status)
                    time.sleep(5)
                    continue

                if not self.IsInternetReachable():
                    self.SendlToLog(1, "MakeFileListQuery - Internet is down\n")
                    raise InternetNotReachable()
            except:
                if not self.IsInternetReachable():
                    self.SendlToLog(1, "MakeFileListQuery (unknown except) - Internet is down\n")
                    raise InternetNotReachable()
                else:
                    if retry == 0:
                        self.SendlToLog(1, "MakeFileListQuery - Query failed. Trying one more time.")
                        retry = 1;
                        continue
                    else:
                        self.SendlToLog(1, "MakeFileListQuery (unknown except %d) - Raising FileListQueryFailed", e.errno)
                        raise FileListQueryFailed()
            break

    def TotalFilesInFolder(self, parent='root'):
        file_count = 0
        try:
            file_list = self.MakeFileListQuery("'%s' in parents and trashed=false"  % parent)
            for f in file_list:
                if f['mimeType'] == 'application/vnd.google-apps.folder':
                    GoSyncEventController().PostEvent(GOSYNC_EVENT_SCAN_UPDATE, {'Scanning: %s' % f['name']})
                    file_count += self.TotalFilesInFolder(f['id'])
                    file_count += 1
                else:
                    file_count += 1

            return file_count
        except:
            raise

    def IsGoogleDocument(self, f):
        if any(f['mimeType'] in s for s in google_docs_mimelist):
            return True
        else:
            return False

    def TotalFilesInDrive(self):
        return self.TotalFilesInFolder()

    def IsMonitoringAll(self):
        if self.sync_selection[0][1] == '':
            return True
        else:
            return False

    def IsDirectoryMonitored(self, dir):
        if self.IsMonitoringAll():
            self.SendlToLog(3, "IsDirectoryMonitored: Complete monitoring is on! (%s)" % dir)
            return True

        try:
            dirpath = dir.split(self.mirror_directory+'/')[1]
            for d in self.sync_selection:
                if d[0] == dirpath:
                    self.SendlToLog(3, "IsDirectoryMonitored: Match: SL: %s dirpath: %s" % (d[0], dirpath))
                    return True
                else:
                    continue

            self.SendlToLog(3, "IsDirectoryMonitored: No match: dirpath: %s" % (dirpath))
            return False
        except:
            self.SendlToLog(3, "IsDirectoryMonitored: %s is in root. Always monitored!" % dir)
            # This is root directory. Which is always monitored
            return True

    def IsFilePathMonitored(self, path):
        if self.IsMonitoringAll():
            self.SendlToLog(3, "IsFilePathMonitored: Path: %s. Complete mirror directory is monitored" % path)
            return True

        try:
            bfile = path.split(self.mirror_directory+'/')[1]
            dirpath = os.path.dirname(bfile)
        except:
            dirpath = os.path.dirname(path)

        if dirpath == '':
            self.SendlToLog(3, "IsFilePathMonitored: Path: %s is in ROOT. Always monitored" % path)
            return True
        else:
            for e in self.sync_selection:
                if e[0] == dirpath:
                    self.SendlToLog(3, "IsFilePathMonitored: Path: %s is monitored" % dirpath)
                    return True
                else:
                    continue

            self.SendlToLog(3, "IsFilePathMonitored: Path: %s is not monitored" % dirpath)
            return False


#### DownloadFileByObject
    def partial(self, total_byte_len, part_size_limit):
        s = []
        for p in range(0, total_byte_len, part_size_limit):
            last = min(total_byte_len - 1, p + part_size_limit - 1)
            s.append([p, last])
        return s

    def DownloadFileByObject(self, file_obj, download_path):
        abs_filepath = os.path.join(download_path, file_obj['name'])
        if os.path.exists(abs_filepath):
            if self.HashOfFile(abs_filepath) == file_obj['md5Checksum']:
                self.SendlToLog(3,'DownloadFileByObject: Skipping File (%s) - same as remote.\n' % abs_filepath)
                return
            else:
                self.SendlToLog(2,"DownloadFileByObject: Skipping File (%s) - Local and Remote - Same Name but Different Content.\n" % abs_filepath)
        else:
            self.SendlToLog(3,'DownloadFileByObject: Download Started - File (%s)' % abs_filepath)
            self.SendlToLog(3,'DownloadFileByObject: Download Started - File size (%s)' % file_obj['size'])
            total_size = int(file_obj['size'])
            if ( total_size == 0) :
                open(abs_filepath, 'a').close()
                self.SendlToLog(2,'DownloadFileByObject: Download Completed - File (%s)\n' % abs_filepath)
                GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_UPDATE, {''})
                return
            fd = abs_filepath.split(self.mirror_directory+'/')[1]
            if ( total_size < self.LargeFileSize) :
                GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_UPDATE,
                                              {'Downloading %s' % fd})
                while True:
                    try:
                        request = self.drive.files().get_media(fileId=file_obj['id'])
                        fh = io.FileIO(abs_filepath, 'wb')
                        downloader = MediaIoBaseDownload(fh, request)
                        done = False
                        while done is False:
                            status, done = downloader.next_chunk()
                        fh.close()
                        self.updates_done = 1
                        self.SendlToLog(3,'DownloadFileByObject: Download Completed - File (%s)\n' % abs_filepath)
                        GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_UPDATE, {''})
                        break
                    except HttpError as err:
                        self.SendlToLog(3, "DownloadFileByObject: Error: %s\n", err.resp.reason)
                        #These mean backend error, needs retry after atleast 1 second
                        if err.resp.status in [403, 500, 503, 429]:
                            self.SendlToLog(3, "DownloadFileByObject: Backend error. Retrying after 5 seconds.\n")
                            time.sleep(5)
                            continue
                        else:
                            raise
                    except:
                        print(er)
                        raise
            else :
                try:
                    # Downloading large files : 100M chunk size
                    GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_UPDATE,
                                              {'Downloading %s' % fd})
                    s = self.partial(total_size, 1000000000) 
                    with open(abs_filepath, 'wb') as file:
                        for bytes in s:
                            request = self.drive.files().get_media(fileId=file_obj['id'])
                            request.headers["Range"] = "bytes={}-{}".format(bytes[0], bytes[1]) 
                            fh = io.BytesIO(request.execute())
                            file.write(fh.getvalue())
                            file.flush()
                    self.updates_done = 1
                    self.SendlToLog(2,'DownloadFileByObject: Download Completed - File (%s)\n' % abs_filepath)
                    GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_UPDATE, {''})
                except:
                    print(er)
                    raise



#### SyncRemoteDirectory
    def SyncRemoteDirectory(self, parent, pwd, recursive=True):
        self.SendlToLog(3,"### SyncRemoteDirectory: - Sync Started - Remote Directory (%s) ... Recursive = %s\n" % (pwd, recursive))
        if not self.syncRunning.is_set() or self.shutting_down:
            self.SendlToLog(2,"SyncRemoteDirectory: Sync has been paused. Aborting.\n")
            return

        if not os.path.exists(os.path.join(self.mirror_directory, pwd)):
            os.makedirs(os.path.join(self.mirror_directory, pwd))

        try:
            if not self.syncRunning.is_set() or self.shutting_down:
                self.SendlToLog(2, "SyncRemoteDirectory: Sync has been paused. Aborting.")
                return

            file_list = self.MakeFileListQuery("'%s' in parents and trashed=false" % parent)

            #This direcotry is empty nothing to sync.
            if not file_list:
                return

            for f in file_list:
                if not self.syncRunning.is_set() or self.shutting_down:
                    self.SendlToLog(3,"SyncRemoteDirectory: Sync has been paused. Aborting.\n")
                    return

                self.SendlToLog(3, "Checking: %s\n" % f['name'])
                GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_UPDATE, {"Checking: %s" % f['name']})
                if not self.syncRunning.is_set():
                    self.SendlToLog(2,"SyncRemoteDirectory: Sync has been paused. Aborting.\n")
                    return

                if f['mimeType'] == 'application/vnd.google-apps.folder':
                    if not recursive:
                        continue

                    abs_dirpath = os.path.join(self.mirror_directory, pwd, f['name'])
                    self.SendlToLog(3,"SyncRemoteDirectory: Checking directory (%s)" % f['name'])
                    if not os.path.exists(abs_dirpath):
                        self.SendlToLog(3,"SyncRemoteDirectory: Creating directory (%s)" % abs_dirpath)
                        os.makedirs(abs_dirpath)
                        self.SendlToLog(3,"SyncRemoteDirectory: Created directory (%s)" % abs_dirpath)
                    self.SendlToLog(3,"SyncRemoteDirectory: Syncing directory (%s)\n" % f['name'])
                    while True:
                        try:
                            self.SyncRemoteDirectory(f['id'], os.path.join(pwd, f['name']))
                            break
                        except InternetNotReachable:
                            GoSyncEventController().PostEvent(GOSYNC_EVENT_INTERNET_UNREACHABLE, 1)
                            self.SendlToLog(1, "SyncRemoteDirectory - Network has gone down")
                            while True:
                                if self.IsInternetReachable():
                                    GoSyncEventController().PostEvent(GOSYNC_EVENT_INTERNET_UNREACHABLE, 0)
                                    self.SendlToLog(2, "SyncRemoteDirectory - Network is up!")
                                    break
                                else:
                                    time.sleep(5)
                                    continue
                    if not self.syncRunning.is_set() or self.shutting_down:
                        self.SendlToLog(3,"SyncRemoteDirectory: Sync has been paused. Aborting.\n")
                        return
                else:
                    if not self.syncRunning.is_set() or self.shutting_down:
                        self.SendlToLog(3, "SyncRemoteDirectory: Sync has been paused. Aborting download")
                        return

                    self.SendlToLog(3,"SyncRemoteDirectory: Checking file (%s)" % f['name'])
                    if not self.IsGoogleDocument(f):
                        self.DownloadFileByObject(f, os.path.join(self.mirror_directory, pwd))
                    else:
                        self.SendlToLog(3,"SyncRemoteDirectory: Skipping file (%s) is a google document.\n" % f['name'])
        except InternetNotReachable:
            self.SendlToLog(1, "SyncRemoteDirectory: Internet not reachable\n")
            raise
        except:
            self.SendlToLog(1,"SyncRemoteDirectory: Failed to sync directory (%s)" % f['name'])
            raise
        self.SendlToLog(3,"### SyncRemoteDirectory: - Sync Completed - Remote Directory (%s) ... Recursive = %s\n" % (pwd, recursive))

#### validate_sync_settings
    def validate_sync_settings(self):
        for d in self.sync_selection:
            if d[0] != 'root':
                try:
                    f = self.LocateFolderOnDrive(d[0])
                    if f['id'] != d[1]:
                        raise FolderNotFound(d[0])
                    break
                except FolderNotFound:
                    raise FolderNotFound(d[0])
                except:
                    raise
            else:
                if d[1] != '':
                    raise FolderNotFound("Root SHA not right")

#### run (Sync Local and Remote Directory)
    def run(self):
        while not self.shutting_down:
            self.SendlToLog(3, "SyncThread - run - Waiting for Sync to be enabled")
            self.syncRunning.wait()

            if self.shutting_down:
                self.SendToLog(2, "SyncThread - run - GoSync is shutting down!")
                break

            if not self.IsInternetReachable():
                self.SendlToLog(2, "SyncThread - run - Internet is down. Clearing running.")
                GoSyncEventController().PostEvent(GOSYNC_EVENT_INTERNET_UNREACHABLE, 1)
                while True:
                    if not self.IsInternetReachable():
                        time.sleep(5)
                    else:
                        self.SendlToLog(2, "SyncThread - run - Internet is up!")
                        GoSyncEventController().PostEvent(GOSYNC_EVENT_INTERNET_UNREACHABLE, 0)
                        break

            self.SendlToLog(3, "SyncThread - run - Trying to acquire lock.")
            self.sync_lock.acquire()
            self.SendlToLog(3, "SyncThread - run - Lock acquired.")

            try:
                self.SendlToLog(3, "SyncThread - run - Validating sync settings")
                self.validate_sync_settings()
                self.SendlToLog(2, "SyncThread - run - validated")
            except InternetNotReachable:
                self.SendlToLog(2, "SyncThread - run - Validate sync settings => Internet is down")
                self.sync_lock.release()
                continue
            except FolderNotFound as f:
                self.SendlToLog(2, "SyncThread - run - Validate sync settings => Folder %s not found" % f.e_folder)
                GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_INV_FOLDER, f.e_folder)
                self.syncRunning.clear()
                self.sync_lock.release()
                continue

            self.SendlToLog(2, "SyncThread - run - Staring the sync now")
            self.syncing_now = True
            try:
                GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_STARTED, None)
                self.SendlToLog(3,"###############################################")
                self.SendlToLog(3,"Start - Syncing remote directory")
                self.SendlToLog(3,"###############################################")
                for d in self.sync_selection:
                    if d[0] != 'root':
                        #Root folder files are always synced (not recursive)
                        self.SyncRemoteDirectory('root', '', False)
                        #Then sync current folder (recursively)
                        self.SyncRemoteDirectory(d[1], d[0])
                    else:
                        #Sync Root folder (recursively)
                        self.SyncRemoteDirectory('root', '')
                self.SendlToLog(3,"###############################################")
                self.SendlToLog(3,"End - Syncing remote directory")
                self.SendlToLog(3,"###############################################\n")
                #Sync local directory only initially. The rest should be taken care by
                #the observer.
                self.SendlToLog(3,"###############################################")
                self.SendlToLog(3,"Start - Syncing local directory")
                self.SendlToLog(3,"###############################################")
                self.SyncLocalDirectory()
                self.SendlToLog(3,"###############################################")
                self.SendlToLog(3,"End - Syncing local directory")
                self.SendlToLog(3,"###############################################\n")
                self.initial_run = False

                if self.updates_done:
                    self.SendlToLog(2,"Sync - Some changes were done. Triggering drive usage calculation.\n")
                    self.usageCalculateEvent.set()
                GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_DONE, 0)
            except InternetNotReachable:
                self.SendlToLog(2, "SyncThread - run - Internet not reachable")
                GoSyncEventController().PostEvent(GOSYNC_EVENT_INTERNET_UNREACHABLE, 1)
                self.sync_lock.release()
                self.syncing_now = False
                continue
            except:
                self.SendlToLog(1, "SyncThread - run - Unknown exception")
                GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_DONE, -1)

            self.SendlToLog(2, "SyncThread - run - Sync done")
            self.sync_lock.release()
            self.syncing_now = False

#
#todo to review time to wait
#Half-an-hour. TODO: It should come from settings?
            self.time_left = self.sync_interval

            while (self.time_left):
                if not self.calculatingDriveUsage:
                    if not self.syncRunning.is_set():
                        GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_TIMER,
                                                          {'Sync is paused.'})
                    else:
                        GoSyncEventController().PostEvent(GOSYNC_EVENT_SYNC_TIMER,
                                                          {'Sync starts in %02dm:%02ds' % ((self.time_left/60),
                                                                                           (self.time_left % 60))})
                self.time_left -= 1
                if self.shutting_down:
                    self.SendlToLog(2, "SyncThread - run - GoSync is shutting down!")
                    break
                self.syncRunning.wait()
                time.sleep(1)

#### GetFileSize
    def GetFileSize(self, f):
        try:
            size = f['size']
            return long(size)
        except:
#Migration V3 API
#            self.SendlToLog(1,"Failed to get size of file %s (mime: %s)\n" % (f['title'], f['mimeType']))
            self.SendlToLog(1,"Failed to get size of file %s (mime: %s)\n" % (f['name'], f['mimeType']))
            return 0

#### calculateUsageOfFolder
    def calculateUsageOfFolder(self, folder_id):
        try:
            if self.shutting_down:
                self.SendlToLog(3, "calculateUsageOfFolder: GoSync is shutting down!")
                return

            file_list = self.MakeFileListQuery("'%s' in parents and trashed=false" % folder_id)

            #Folder is empty
            if not file_list:
                return

            for f in file_list:
                if self.shutting_down:
                    self.SendlToLog(3, "calculateUsageOfFolder: GoSync is shutting down!")
                    return

                self.fcount += 1
                self.SendlToLog(3, "Scanning: %s (%s -> %s)\n" % (f['name'], f['id'], folder_id))
                GoSyncEventController().PostEvent(GOSYNC_EVENT_SCAN_UPDATE, {'Scanning Folder: %s' % f['name']})
                if f['mimeType'] == 'application/vnd.google-apps.folder':
                    self.driveTree.AddFolder(folder_id, f['id'], f['name'], f)
                    self.calculateUsageOfFolder(f['id'])
                else:
                    if not self.IsGoogleDocument(f):
                        if any(f['mimeType'] in s for s in audio_file_mimelist):
                            self.driveAudioUsage += self.GetFileSize(f)
                        elif  any(f['mimeType'] in s for s in image_file_mimelist):
                            self.drivePhotoUsage += self.GetFileSize(f)
                        elif any(f['mimeType'] in s for s in movie_file_mimelist):
                            self.driveMoviesUsage += self.GetFileSize(f)
                        elif any(f['mimeType'] in s for s in document_file_mimelist):
                            self.driveDocumentUsage += self.GetFileSize(f)
                        else:
                            self.driveOthersUsage += self.GetFileSize(f)
                            #self.SendlToLog(3,"calculateUsageOfFolder: Unknown Mime %s\n" % f['mimeType'])
                        GoSyncEventController().PostEvent(GOSYNC_EVENT_CALCULATE_USAGE_UPDATE, self.fcount)
        except:
            raise

#### calculateUsage
    def calculateUsage(self):
        while not self.shutting_down:
            self.usageCalculateEvent.wait()
            self.usageCalculateEvent.clear()

            if self.shutting_down:
                self.SendlToLog(2, "calculateUsage - GoSync is shutting down!")
                break

            self.sync_lock.acquire()
            self.SendlToLog(3,"CalculateUsage: SyncLock acquired")

            if self.force_usage_calculation == True:
                # Usage calculation is forced by user, wipe the slate clean
                self.drive_usage_dict = {}
                self.driveTree = GoogleDriveTree()

            if self.drive_usage_dict and not self.updates_done:
                self.SendlToLog(3,"CalculateUsage: No calculation to be done")
                GoSyncEventController().PostEvent(GOSYNC_EVENT_CALCULATE_USAGE_DONE, 0)
                self.sync_lock.release()
                continue

            self.SendlToLog(3,"CalculateUsage: Started")
            self.updates_done = 0
            self.calculatingDriveUsage = True
            self.driveAudioUsage = 0
            self.driveMoviesUsage = 0
            self.driveDocumentUsage = 0
            self.drivePhotoUsage = 0
            self.driveOthersUsage = 0
            self.fcount = 0
            try:
                GoSyncEventController().PostEvent(GOSYNC_EVENT_CALCULATE_USAGE_STARTED, 0)
                self.SendlToLog(3,"CalculateUsage: Scanning files...\n")
                try:
                    self.calculateUsageOfFolder('root')
                    GoSyncEventController().PostEvent(GOSYNC_EVENT_CALCULATE_USAGE_DONE, 0)
                    #self.drive_usage_dict['Total Files'] = self.totalFilesToCheck
                    self.drive_usage_dict['Total Size'] = long(self.about_drive['storageQuota']['limit'])
                    self.drive_usage_dict['Audio Size'] = self.driveAudioUsage
                    self.drive_usage_dict['Movies Size'] = self.driveMoviesUsage
                    self.drive_usage_dict['Document Size'] = self.driveDocumentUsage
                    self.drive_usage_dict['Photo Size'] = self.drivePhotoUsage
                    self.drive_usage_dict['Others Size'] = self.driveOthersUsage
                    pickle.dump(self.driveTree, open(self.tree_pickle_file, "wb"))
                    self.config_dict['Drive Usage'] = self.drive_usage_dict
                    self.SaveConfig()
                except:
                    self.driveAudioUsage = 0
                    self.driveMoviesUsage = 0
                    self.driveDocumentUsage = 0
                    self.drivePhotoUsage = 0
                    self.driveOthersUsage = 0
                    GoSyncEventController().PostEvent(GOSYNC_EVENT_CALCULATE_USAGE_DONE, -1)
            except:
                GoSyncEventController().PostEvent(GOSYNC_EVENT_CALCULATE_USAGE_DONE, -1)
                self.SendlToLog(1,"Failed to get the total number of files in drive\n")

            self.calculatingDriveUsage = False
            self.sync_lock.release()

    def GetDriveDirectoryTree(self):
        self.sync_lock.acquire()
        ref_tree = copy.deepcopy(self.driveTree)
        self.sync_lock.release()
        return ref_tree

    def IsCalculatingDriveUsage(self):
        return self.calculatingDriveUsage

    def GetAudioUsage(self):
        return self.driveAudioUsage

    def GetMovieUsage(self):
        return self.driveMoviesUsage

    def GetDocumentUsage(self):
        return self.driveDocumentUsage

    def GetOthersUsage(self):
        return self.driveOthersUsage

    def GetPhotoUsage(self):
        return self.drivePhotoUsage

    def StartSync(self):
        self.syncRunning.set()

    def StopSync(self):
        self.syncRunning.clear()

    def IsSyncRunning(self):
        return self.syncing_now

    def IsSyncEnabled(self):
        return self.syncRunning.is_set()

    def ForceDriveUsageCalculation(self):
        if self.calculatingDriveUsage:
            return

        self.force_usage_calculation = True
        self.usageCalculateEvent.set()
        self.SendlToLog(3,"ForceDriveUsageCalculation: Marked")

    def RemoveSyncSelection(self, folder):
        if folder == 'root':
            #Cannote remove root
            return
        else:
            for d in self.sync_selection:
                if d[0] == folder.GetPath() and d[1] == folder.GetId():
                    self.sync_selection.remove(d)

            #If no other selection is left, select root
            if not self.sync_selection:
                self.sync_selection = [['root', '']]

            self.config_dict['Sync Selection'] = self.sync_selection
            self.SaveConfig()

    def ClearSyncSelection(self):
        self.sync_selection = [['root', '']]

    def SetSyncSelection(self, folder):
        if folder == 'root':
            self.sync_selection = [['root', '']]
        else:
            for d in self.sync_selection:
                if d[0] == 'root':
                    self.sync_selection = []
            for d in self.sync_selection:
                if d[0] == folder.GetPath() and d[1] == folder.GetId():
                    return
            self.sync_selection.append([folder.GetPath(), folder.GetId()])
        self.config_dict['Sync Selection'] = self.sync_selection
        self.SaveConfig()

    def GetSyncList(self):
        return copy.deepcopy(self.sync_selection)

    def EnableAutoSync(self):
        self.auto_start_sync = True
        self.SaveConfig()

    def DisableAutoSync(self):
        self.auto_start_sync = False
        self.SaveConfig()

    def GetAutoSyncState(self):
        return self.auto_start_sync

    def GetLocalMirrorDirectory(self):
        return self.mirror_directory

    def SetLocalMirrorDirectory(self, new_directory):
        self.base_mirror_directory = os.path.join(new_directory, 'Google Drive')
        self.SaveConfig()

    def SetSyncInterval(self, new_interval):
        self.sync_interval = new_interval
        self.SaveConfig()

    def GetSyncInterval(self):
        return self.sync_interval

    def GetUseSystemNotifSetting(self):
        return self.use_system_notif

    def SetUseSystemNotifSetting(self, new):
        self.use_system_notif = new
        self.SaveConfig()

    def GetLogLevel(self):
        return self.Log_Level

    def SetLogLevel(self, new):
        self.Log_Level = new
        self.SaveConfig()

class FileModificationNotifyHandler(PatternMatchingEventHandler):
    patterns = ["*"]

    def __init__(self, sync_handler):
        super(FileModificationNotifyHandler, self).__init__()
        self.sync_handler = sync_handler

    def on_created(self, evt):
        self.sync_handler.logger.debug("Observer: %s created\n" % evt.src_path)
        self.sync_handler.UploadObservedFile(evt.src_path)

    def on_moved(self, evt):
        self.sync_handler.logger.info("Observer: file %s moved to %s: Not supported yet!\n" % (evt.src_path, evt.dest_path))
        self.sync_handler.HandleMovedFile(evt.src_path, evt.dest_path)

    def on_deleted(self, evt):
        self.sync_handler.logger.info("Observer: file %s deleted on drive.\n" % evt.src_path)
        self.sync_handler.TrashObservedFile(evt.src_path)