#!/usr/bin/env python3.6
# git-annex-remote-googledrive adds direct support for Google Drive to git annex using the PyDrive lib
#
# Install in PATH as git-annex-remote-googledrive
#
# Copyright (C) 2017-2018  Silvio Ankermann
#
# This program is free software: you can redistribute it and/or modify it under the terms of version 3 of the GNU
# General Public License as published by the Free Software Foundation.
#
# 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.
#

from abc import ABC, abstractmethod

import os
import sys

from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from oauth2client.client import OAuth2Credentials

from annexremote import Master
from annexremote import ExportRemote
from annexremote import RemoteError

# The Google class
class GoogleRemote(ExportRemote):

    def __init__(self, annex):
        super().__init__(annex)
        self.presence_cache = dict()
        self.folder_cache = dict()
        # Authentication
        self.gauth = GoogleAuth()
        self.gauth.settings['client_config_backend'] = 'settings'
        self.gauth.settings['client_config'] = {'client_id': '275666578511-ndjt6mkns3vgb60cbo7csrjn6mbh8gbf.apps.googleusercontent.com', 'client_secret': 'Den2tu08pRU4s5KeCp5whas_', 'auth_uri':'https://accounts.google.com/o/oauth2/auth', 'token_uri':'https://accounts.google.com/o/oauth2/token', 'revoke_uri': None, 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob'}
        
    def setup(self):
        self.gauth.LoadCredentialsFile("token.json")
        
        if self.gauth.credentials is None:
            self.gauth.CommandLineAuth()
            #self.gauth.LocalWebserverAuth()
        elif self.gauth.access_token_expired:
            self.gauth.Refresh()
        else:
            self.gauth.Authorize()
        
        self.gauth.SaveCredentialsFile("token.json")
        print("Setup complete. An auth token was stored in token.json. Now run 'git annex initremote' with your desired parameters. If you don't run it from the same folder, specify via token=path/to/token.json")

    def initremote(self):
        self.prefix = self.annex.getconfig("prefix")

        token_file = self.annex.getconfig("token") or "token.json"
        self.gauth.LoadCredentialsFile(token_file)

        if self.gauth.credentials is None:
            credentials = self.annex.getcreds("credentials")["user"]
            if credentials:
                self.gauth.credentials = OAuth2Credentials.from_json(credentials)
        
        try:
            if self.gauth.access_token_expired:
                self.gauth.Refresh()
            else:
                self.gauth.Authorize()
            self.drive = GoogleDrive(self.gauth)
            self.root = self._getfolder(self.prefix)
        except:
            raise RemoteError("Failed to create directory on remote. Ensure that you are connected to the internet and have successfully run 'git-annex-remote-gdrive setup'.")

        self.annex.setconfig("root_id", self.root['id'])
        credentials = ''.join(self.gauth.credentials.to_json().split())
        self.annex.setcreds("credentials", credentials, "")

    def prepare(self):
        self.prefix = self.annex.getconfig("prefix")
        credentials = self.annex.getcreds("credentials")["user"]

        try:
            self.gauth.credentials = OAuth2Credentials.from_json(credentials)
            if self.gauth.access_token_expired:
                self.gauth.Refresh()
            else:
                self.gauth.Authorize()
            
            self.drive = GoogleDrive(self.gauth)
            self.root = self._getfolder(self.prefix)
        except:
            raise RemoteError("Failed to create directory on remote. Ensure that gdrive has been configured correctly and has permission to access your Drive.")
        if self.root['id'] != self.annex.getconfig("root_id"):
            raise RemoteError("ID of root folder changed. Was the repo moved? Please check remote and re-run git annex enableremote")

        credentials = ''.join(self.gauth.credentials.to_json().split())
        self.annex.setcreds("credentials", credentials, "")

    def transfer_store(self, key, file_):
        if key not in self.presence_cache:
            self.checkpresent(key)
        if not self.presence_cache[key]:
            newfile = self.drive.CreateFile({'title': key, 'parents': [{'kind': 'drive#parentReference', 'id': self.root['id']}] })
            newfile.SetContentFile(file_)
            try:
                newfile.Upload()
            except:
                raise RemoteError("Upload error")
            else:
                self.presence_cache[key] = True
    
    def transfer_retrieve(self, key, file_):
        try:
            newfile = self._getfile(key)
            newfile.GetContentFile(file_)
        except:
            raise RemoteError("Download error")
    
    def checkpresent(self, key):
        try:
            file_ = self._getfile(key)
        except:
            raise RemoteError("Connection Error")
        else:
            if file_:
                self.presence_cache[key] = True
            else:
                self.presence_cache[key] = False
            return self.presence_cache[key]

    def remove(self, key):
        try:
            file_ = self._getfile(key)
            if file_:
                file_.Delete()
        except:
            raise RemoteError("Error deleting key")

    def transferexport_store(self, key, file_, name):
        if name not in self.presence_cache:
            self.checkpresentexport(key, name)
        if not self.presence_cache[name]:
            fileinfo = self._splitpath(name)
            parent = self._getfolder(fileinfo['path'], root=self.root, create=True)
            newfile = self.drive.CreateFile({'title': fileinfo['filename'], 'parents': [{'kind': 'drive#parentReference', 'id': parent['id']}] })
            newfile.SetContentFile(file_)
            try:
                newfile.Upload()
            except Exception as e:
                raise RemoteError("Upload error", e)
            else:
                self.presence_cache[name] = True

    def transferexport_retrieve(self, key, file_, name):
        fileinfo = self._splitpath(name)
        parent = self._getfolder(fileinfo['path'], root=self.root, create=False)
        if not parent:
            raise RemoteError("File not present")
        try:
            newfile = self._getfile(fileinfo['filename'], parent=parent)
            newfile.GetContentFile(file_)
        except Exception as e:
            raise RemoteError("Download error", e)
            
    def checkpresentexport(self, key, name):
        fileinfo = self._splitpath(name)
        parent = self._getfolder(fileinfo['path'], root=self.root, create=False)
        if not parent:
            self.presence_cache[name] = False
            return False

        try:
            file_ = self._getfile(fileinfo['filename'], parent=parent)
        except:
            raise RemoteError("Connection Error")
        else:
            if file_:
                self.presence_cache[name] = True
            else:
                self.presence_cache[name] = False
            return self.presence_cache[name]

    def removeexport(self, key, name):
        fileinfo = self._splitpath(name)
        parent = self._getfolder(fileinfo['path'], root=self.root, create=False)
        if not parent:
            return

        try:
            file_ = self._getfile(fileinfo['filename'], parent=parent)
            if file_:
                file_.Delete()
        except:
            raise RemoteError("Error deleting file")

    def removeexportdirectory(self, directory):
        try:
            file_ = self._getfolder(directory, root=self.root, create=False)
            if file_:
                file_.Delete()
        except:
            raise RemoteError("Error deleting directory")

    def renameexport(self, key, name, new_name):
        oldfileinfo = self._splitpath(name)
        newfileinfo = self._splitpath(new_name)
        oldparent = self._getfolder(oldfileinfo['path'], root=self.root, create=False)
        newparent = self._getfolder(newfileinfo['path'], root=self.root, create=True)
        try:
            file_ = self._getfile(oldfileinfo['filename'], parent=oldfileinfo['parent'])
            if oldfileinfo['path'] != newfileinfo['path']:
                file_['parents'] = [{'kind': 'drive#parentReference', 'id': newparent['id']}]
            if oldfileinfo['filename'] != newfileinfo['filename']:
                file_['title'] = newfileinfo['filename']
            file_.Upload()
        except:
            raise RemoteError("Error renaming file")


    def _getfile(self, filename, parent=None):
        #id_ = getstate(key)
        #if id_:
        #    return id_
        if not parent:
            parent = self.root
        file_list = self.drive.ListFile({'q': f"'{parent['id']}' in parents and title='{filename}' and trashed=false"}).GetList()
        if (len(file_list) == 1):
            return file_list[0]
        elif (len(file_list) == 0):
            return None
        else:
            raise RemoteError (f"There are two or more files named {key}")
    
    def _getfolder(self, path, root=None, create=True):
        path_list = path.strip('/').split('/')
        if root:
            current_folder = root
            current_path = self.prefix
        else:
            current_folder = self.drive.CreateFile({'id': 'root'})
            current_path = ""

        if path == "":
            return current_folder
        for folder in path_list:
            current_path = "/".join([current_path, folder])
            if current_path in self.folder_cache:
                current_folder = self.folder_cache[current_path]
                continue
                
            file_list = self.drive.ListFile({'q': f"'{current_folder['id']}' in parents and title='{folder}' and trashed=false"}).GetList()
            if (len(file_list) == 1):
                current_folder = file_list[0]
            elif (len(file_list) == 0):
                if(create):
                    current_folder = self.drive.CreateFile({'title': folder, 'parents': [{'kind': 'drive#parentReference', 'id': current_folder['id']}], 'mimeType': "application/vnd.google-apps.folder"})
                    current_folder.Upload()
                else:
                    return None
            else:
                raise self.AmbiguousFilenameException (f"There are two or more folders named {current_path}")
            self.folder_cache[current_path] = current_folder
                
        return current_folder

    def _splitpath(self, filename):
        splitpath = filename.rsplit('/',1)
        exportfile = dict()
        if len(splitpath) == 2:
            exportfile['path'] = splitpath[0]
            exportfile['filename'] = splitpath[1]
        else:
            exportfile['path'] = ''
            exportfile['filename'] = splitpath[0]
        return exportfile

def main():
    if len(sys.argv) == 2 and sys.argv[1] == "setup":
        with open(os.devnull, 'w') as devnull:
            master = Master(devnull)
            remote = GoogleRemote(master)
            remote.setup()
        return


    # redirect all non-protocol-output to stderr
    output = sys.stdout
    sys.stdout = sys.stderr

    master = Master(output)
    master.LinkRemote(GoogleRemote(master))
    master.Listen()

if __name__ == "__main__":
    main()


