# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2013 jno <jno@pisem.net>
#
# Version: 0.3
#
# 0. You can make me happy with https://www.dropbox.com/referrals/NTE2ODA0Mzg5
# 1. Most of the code was taken from cli_client.py. The ftpsbackend.py was used as a template
# 2. DPBX & dpbx are used because the use of the actual name is prohibited
#
# This file is part of duplicity.
#
# Duplicity is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# Duplicity 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 duplicity; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

import os
import urllib
import locale
import sys
from functools import reduce

import traceback
import StringIO

import duplicity.backend
from duplicity import log
from duplicity import util
from duplicity.errors import BackendException


# You can register your own developer account with Dropbox and
# register a new application for yourself, obtaining the new
# APP_KEY and APP_SECRET.
# Note 1: you must not store your credentials "as is" in the code.
#         The values must be "processed" at least.
#         This is a must for "production" keys.
# Note 2: the name of the application defines the name of the
#         subfolder in the "Apps" folder.
# http://www.dropbox.com/developers/apps is the place to get the key.

APP_KEY = 'YOUR_APP_KEY'
APP_SECRET = 'YOUR_APP_SECRET'

# Limit file access to Apps/Duplicity (the name of the application).
ACCESS_TYPE = 'app_folder'
# This file will store cached value of oAuth token
_TOKEN_CACHE_FILE = os.path.expanduser("~/.dropbox.token_store.txt")


def log_exception(e):
    log.Error('Exception [%s]:' % (e,))
    f = StringIO.StringIO()
    traceback.print_exc(file=f)
    f.seek(0)
    for s in f.readlines():
        log.Error('| ' + s.rstrip())
    f.close()


def command(login_required=True):
    """a decorator for handling authentication and exceptions"""
    def decorate(f):
        def wrapper(self, *args):
            if login_required and not self.sess.is_linked():
                raise BackendException("dpbx Cannot login: check your credentials", log.ErrorCode.dpbx_nologin)
                return

            try:
                return f(self, *args)
            except TypeError as e:
                log_exception(e)
                raise BackendException('dpbx type error "%s"' % (e,))
            except rest.ErrorResponse as e:
                msg = e.user_error_msg or util.uexc(e)
                log.Error('dpbx error: %s' % (msg,), log.ErrorCode.backend_command_error)
                raise e
            except Exception as e:
                log_exception(e)
                log.Error('dpbx code error "%s"' % (e,), log.ErrorCode.backend_code_error)
                raise e

        wrapper.__doc__ = f.__doc__
        return wrapper
    return decorate


class DPBXBackend(duplicity.backend.Backend):
    """Connect to remote store using Dr*pB*x service"""
    def __init__(self, parsed_url):
        duplicity.backend.Backend.__init__(self, parsed_url)

        global client, rest, session
        from dropbox import client, rest, session

        class StoredSession(session.DropboxSession):
            """a wrapper around DropboxSession that stores a token to a file on disk"""
            TOKEN_FILE = _TOKEN_CACHE_FILE

            def load_creds(self):
                try:
                    f = open(self.TOKEN_FILE)
                    stored_creds = f.read()
                    f.close()
                    self.set_token(*stored_creds.split('|'))
                    log.Info("[loaded access token]")
                except IOError:
                    pass  # don't worry if it's not there

            def write_creds(self, token):
                open(self.TOKEN_FILE, 'w').close()  # create/reset file
                os.chmod(self.TOKEN_FILE, 0o600)  # set it -rw------ (NOOP in Windows?)
                # now write the content
                f = open(self.TOKEN_FILE, 'w')
                f.write("|".join([token.key, token.secret]))
                f.close()

            def delete_creds(self):
                os.unlink(self.TOKEN_FILE)

            def link(self):
                if not sys.stdout.isatty() or not sys.stdin.isatty():
                    log.FatalError('dpbx error: cannot interact, but need human attention', log.ErrorCode.backend_command_error)
                request_token = self.obtain_request_token()
                url = self.build_authorize_url(request_token)
                print
                print '-' * 72
                print "url:", url
                print "Please authorize in the browser. After you're done, press enter."
                raw_input()

                self.obtain_access_token(request_token)
                self.write_creds(self.token)

            def unlink(self):
                self.delete_creds()
                session.DropboxSession.unlink(self)

        self.sess = StoredSession(etacsufbo(APP_KEY),
                                  etacsufbo(APP_SECRET),
                                  access_type=ACCESS_TYPE)
        self.api_client = client.DropboxClient(self.sess)
        self.sess.load_creds()

        self.login()

    def login(self):
        if not self.sess.is_linked():
            try:  # to login to the box
                self.sess.link()
            except rest.ErrorResponse as e:
                log.FatalError('dpbx Error: %s\n' % util.uexc(e), log.ErrorCode.dpbx_nologin)
            if not self.sess.is_linked():  # stil not logged in
                log.FatalError("dpbx Cannot login: check your credentials", log.ErrorCode.dpbx_nologin)

    def _error_code(self, operation, e):
        if isinstance(e, rest.ErrorResponse):
            if e.status == 404:
                return log.ErrorCode.backend_not_found

    @command()
    def _put(self, source_path, remote_filename):
        remote_dir = urllib.unquote(self.parsed_url.path.lstrip('/'))
        remote_path = os.path.join(remote_dir, remote_filename).rstrip()
        from_file = open(source_path.name, "rb")
        resp = self.api_client.put_file(remote_path, from_file)
        log.Debug('dpbx,put(%s,%s): %s' % (source_path.name, remote_path, resp))

    @command()
    def _get(self, remote_filename, local_path):
        remote_path = os.path.join(urllib.unquote(self.parsed_url.path), remote_filename).rstrip()

        to_file = open(local_path.name, 'wb')
        f, metadata = self.api_client.get_file_and_metadata(remote_path)
        log.Debug('dpbx.get(%s,%s): %s' % (remote_path, local_path.name, metadata))
        # print 'Metadata:', metadata
        to_file.write(f.read())
        f.close()
        to_file.close()

        local_path.setdata()

    @command()
    def _list(self):
        # Do a long listing to avoid connection reset
        remote_dir = urllib.unquote(self.parsed_url.path.lstrip('/')).rstrip()
        resp = self.api_client.metadata(remote_dir)
        log.Debug('dpbx.list(%s): %s' % (remote_dir, resp))
        l = []
        if 'contents' in resp:
            encoding = locale.getdefaultlocale()[1]
            if encoding is None:
                encoding = 'LATIN1'
            for f in resp['contents']:
                name = os.path.basename(f['path'])
                l.append(name.encode(encoding))
        return l

    @command()
    def _delete(self, filename):
        remote_dir = urllib.unquote(self.parsed_url.path.lstrip('/')).rstrip()
        remote_name = os.path.join(remote_dir, filename)
        resp = self.api_client.file_delete(remote_name)
        log.Debug('dpbx.delete(%s): %s' % (remote_name, resp))

    @command()
    def _close(self):
        """close backend session? no! just "flush" the data"""
        info = self.api_client.account_info()
        log.Debug('dpbx.close():')
        for k in info:
            log.Debug(':: %s=[%s]' % (k, info[k]))
        entries = []
        more = True
        cursor = None
        while more:
            info = self.api_client.delta(cursor)
            if info.get('reset', False):
                log.Debug("delta returned True value for \"reset\", no matter")
            cursor = info.get('cursor', None)
            more = info.get('more', False)
            entr = info.get('entries', [])
            entries += entr
        for path, meta in entries:
            mm = meta and 'ok' or 'DELETE'
            log.Info(':: :: [%s] %s' % (path, mm))
            if meta:
                for k in meta:
                    log.Debug(':: :: :: %s=[%s]' % (k, meta[k]))

    def _mkdir(self, path):
        """create a new directory"""
        resp = self.api_client.file_create_folder(path)
        log.Debug('dpbx._mkdir(%s): %s' % (path, resp))


def etacsufbo(s):
    return ''.join(reduce(lambda x, y: (x and len(x[-1]) == 1) and
                          (x.append(y + x.pop(-1)) and x or x) or (x + [y]), s, []))

duplicity.backend.register_backend("dpbx", DPBXBackend)
