class SyncDaemon(object):
    """Interface to Ubuntu One's SyncDaemon."""

    def __init__(self, dbus_class=DBusInterface):
        logger.info("SyncDaemon interface started!")

        # set up dbus and related stuff
        self.dbus = dbus_class(self)

        # attributes for GUI, definition and filling
        self.current_state = State()
        self.folders = None
        self.shares_to_me = None
        self.shares_to_others = None
        self.public_files = None
        self.queue_content = QueueContent(home=user.home)

        # callbacks for GUI to hook in
        self.status_changed_callback = NO_OP
        self.on_started_callback = NO_OP
        self.on_stopped_callback = NO_OP
        self.on_connected_callback = NO_OP
        self.on_disconnected_callback = NO_OP
        self.on_online_callback = NO_OP
        self.on_offline_callback = NO_OP
        self.on_folders_changed_callback = NO_OP
        self.on_shares_to_me_changed_callback = NO_OP
        self.on_shares_to_others_changed_callback = NO_OP
        self.on_public_files_changed_callback = NO_OP
        self.on_metadata_ready_callback = mandatory_callback(
            'on_metadata_ready_callback')
        self.on_initial_data_ready_callback = NO_OP
        self.on_initial_online_data_ready_callback = NO_OP
        self.on_share_op_error_callback = mandatory_callback(
            'on_share_op_error_callback')
        self.on_folder_op_error_callback = mandatory_callback(
            'on_folder_op_error_callback')
        self.on_public_op_error_callback = mandatory_callback(
            'on_public_op_error_callback')
        self.on_node_ops_changed_callback = NO_OP
        self.on_internal_ops_changed_callback = NO_OP
        self.on_transfers_callback = NO_OP

        # poller
        self.transfers_poller = Poller(TRANSFER_POLL_INTERVAL,
                                       self.get_current_transfers)
        self._check_started()

    @defer.inlineCallbacks
    def _check_started(self):
        """Check if started and load initial data if yes."""
        # load initial data if ubuntuone-client already started
        started = yield self.dbus.is_sd_started()
        if started:
            self.current_state.set(is_started=True)
            self._get_initial_data()
        else:
            self.current_state.set(is_started=False)

    def shutdown(self):
        """Shut down the SyncDaemon."""
        logger.info("SyncDaemon interface going down")
        self.transfers_poller.run(False)
        self.dbus.shutdown()

    @defer.inlineCallbacks
    def get_current_transfers(self):
        """Get downloads and uploads."""
        uploads = yield self.dbus.get_current_uploads()
        downloads = yield self.dbus.get_current_downloads()
        self.on_transfers_callback(uploads + downloads)

    @defer.inlineCallbacks
    def _get_initial_data(self):
        """Get the initial SD data."""
        logger.info("Getting offline initial data")

        status_data = yield self.dbus.get_status()
        self._send_status_changed(*status_data)

        # queue content stuff
        shares_real_dir = yield self.dbus.get_real_shares_dir()
        shares_link_dir = yield self.dbus.get_link_shares_dir()
        self.queue_content.set_shares_dirs(shares_link_dir, shares_real_dir)
        content = yield self.dbus.get_queue_content()
        self.queue_content.set_content(content)
        self.transfers_poller.run(self.queue_content.transferring)

        self.folders = yield self.dbus.get_folders()

        self.shares_to_me = yield self.dbus.get_shares_to_me()
        self.shares_to_others = yield self.dbus.get_shares_to_others()

        # let frontend know that we have all the initial offline data
        logger.info("All initial offline data is ready")
        self.on_initial_data_ready_callback()

        logger.info("Getting online initial data")
        self.public_files = yield self.dbus.get_public_files()

        # let frontend know that we have all the initial online data
        logger.info("All initial online data is ready")
        self.on_initial_online_data_ready_callback()

    @defer.inlineCallbacks
    def on_sd_public_files_changed(self, pf=None, is_public=False):
        """Update the Public Files list."""
        data = yield self.dbus.get_public_files()
        logger.info("Got new Public Files list (%d items)", len(data))
        self.public_files = data
        self.on_public_files_changed_callback(self.public_files)

    @defer.inlineCallbacks
    def on_sd_shares_changed(self):
        """Shares changed, ask for new information."""
        logger.info("SD Shares changed")

        # to me
        new_to_me = yield self.dbus.get_shares_to_me()
        if new_to_me != self.shares_to_me:
            self.shares_to_me = new_to_me
            self.on_shares_to_me_changed_callback(new_to_me)

        # to others
        new_to_others = yield self.dbus.get_shares_to_others()
        if new_to_others != self.shares_to_others:
            self.shares_to_others = new_to_others
            self.on_shares_to_others_changed_callback(new_to_others)

    @defer.inlineCallbacks
    def on_sd_folders_changed(self):
        """Folders changed, ask for new information."""
        logger.info("SD Folders changed")
        self.folders = yield self.dbus.get_folders()
        self.on_folders_changed_callback(self.folders)

    def on_sd_status_changed(self, *status_data):
        """The Status of SD changed.."""
        logger.info("SD Status changed")
        self._send_status_changed(*status_data)

    def _send_status_changed(self, name, description, is_error, is_connected,
                             is_online, queues, connection):
        """Send status changed signal."""
        kwargs = dict(name=name, description=description,
                      is_error=is_error, is_connected=is_connected,
                      is_online=is_online, queues=queues,
                      connection=connection)

        # check status changes to call other callbacks
        if is_connected and not self.current_state.is_connected:
            self.on_connected_callback()
        if not is_connected and self.current_state.is_connected:
            self.on_disconnected_callback()
        if is_online and not self.current_state.is_online:
            self.on_online_callback()
        if not is_online and self.current_state.is_online:
            self.on_offline_callback()

        # state of SD
        if name == "SHUTDOWN":
            state = STATE_STOPPED
            self.current_state.set(is_started=False)
            self.on_stopped_callback()
        elif name in ('READY', 'WAITING'):
            if connection == 'With User With Network':
                state = STATE_CONNECTING
            else:
                state = STATE_DISCONNECTED
        elif name == 'STANDOFF':
            state = STATE_DISCONNECTED
        else:
            if is_connected:
                if name == "QUEUE_MANAGER":
                    if queues == "IDLE":
                        state = STATE_IDLE
                    else:
                        state = STATE_WORKING
                else:
                    state = STATE_CONNECTING
            else:
                state = STATE_STARTING
                # check if it's the first time we flag starting
                if self.current_state.state != STATE_STARTING:
                    self.current_state.set(is_started=True)
                    self.on_started_callback()
                    self._get_initial_data()
        kwargs['state'] = state
        xs = sorted(kwargs.iteritems())
        logger.debug("    new status: %s", ', '.join('%s=%r' % i for i in xs))

        # set current state to new values and call status changed cb
        self.current_state.set(**kwargs)
        self.status_changed_callback(**kwargs)

    def on_sd_queue_added(self, op_name, op_id, op_data):
        """A command was added to the Request Queue."""
        logger.info("Queue content: added %r [%s] %s", op_name, op_id, op_data)
        r = self.queue_content.add(op_name, op_id, op_data)
        if r == NODE_OP:
            self.on_node_ops_changed_callback(self.queue_content.node_ops)
        elif r == INTERNAL_OP:
            self.on_internal_ops_changed_callback(
                self.queue_content.internal_ops)
        self.transfers_poller.run(self.queue_content.transferring)

    def on_sd_queue_removed(self, op_name, op_id, op_data):
        """A command was removed from the Request Queue."""
        logger.info("Queue content: removed %r [%s] %s",
                    op_name, op_id, op_data)
        r = self.queue_content.remove(op_name, op_id, op_data)
        if r == NODE_OP:
            self.on_node_ops_changed_callback(self.queue_content.node_ops)
        elif r == INTERNAL_OP:
            self.on_internal_ops_changed_callback(
                self.queue_content.internal_ops)
        self.transfers_poller.run(self.queue_content.transferring)

    def start(self):
        """Start the SyncDaemon."""
        logger.info("Starting u1.SD")
        d = self.dbus.start()
        self._get_initial_data()
        return d

    def quit(self):
        """Stop the SyncDaemon and makes it quit."""
        logger.info("Stopping u1.SD")
        return self.dbus.quit()

    def connect(self):
        """Tell the SyncDaemon that the user wants it to connect."""
        logger.info("Telling u1.SD to connect")
        return self.dbus.connect()

    def disconnect(self):
        """Tell the SyncDaemon that the user wants it to disconnect."""
        logger.info("Telling u1.SD to disconnect")
        return self.dbus.disconnect()

    @defer.inlineCallbacks
    def get_metadata(self, path):
        """Get the metadata for given path."""
        resp = yield self.dbus.get_metadata(os.path.realpath(path))
        if resp == NOT_SYNCHED_PATH:
            self.on_metadata_ready_callback(path, resp)
            return

        # have data! store it in raw, and process some
        result = dict(raw_result=resp)

        # stat
        if resp['stat'] == u'None':
            stat = None
        else:
            items = re.match(".*\((.*)\)", resp['stat']).groups()[0]
            items = [x.split("=") for x in items.split(", ")]
            stat = dict((a, int(b[:-1] if b[-1] == 'L' else b))
                        for a, b in items)
        result['stat'] = stat

        # changed
        is_partial = resp['info_is_partial'] != u'False'
        if resp['local_hash'] == resp['server_hash']:
            if is_partial:
                logger.warning("Bad 'changed' values: %r", resp)
                changed = None
            else:
                changed = CHANGED_NONE
        else:
            if is_partial:
                changed = CHANGED_SERVER
            else:
                changed = CHANGED_LOCAL
        result['changed'] = changed

        # path
        processed_path = resp['path']
        if processed_path.startswith(user.home):
            processed_path = "~" + processed_path[len(user.home):]
        result['path'] = processed_path

        self.on_metadata_ready_callback(path, result)

    @defer.inlineCallbacks
    def get_free_space(self, volume_id):
        """Get the free space for a volume."""
        free_space = yield self.dbus.get_free_space(volume_id)
        defer.returnValue(int(free_space))

    def _answer_share(self, share_id, method, action_name):
        """Effectively accept or reject a share."""
        def error(failure):
            """Operation failed."""
            if failure.check(ShareOperationError):
                error = failure.value.error
                logger.info("%s share %s finished with error: %s",
                            action_name, share_id, error)
                self.on_share_op_error_callback(share_id, error)
            else:
                logger.error("Unexpected error when %s share %s: %s %s",
                             action_name.lower(), share_id,
                             failure.type, failure.value)

        def success(_):
            """Operation finished ok."""
            logger.info("%s share %s finished successfully",
                        action_name, share_id)

        logger.info("%s share %s started", action_name, share_id)
        d = method(share_id)
        d.addCallbacks(success, error)

    def accept_share(self, share_id):
        """Accept a share."""
        self._answer_share(share_id, self.dbus.accept_share, "Accepting")

    def reject_share(self, share_id):
        """Reject a share."""
        self._answer_share(share_id, self.dbus.reject_share, "Rejecting")

    def subscribe_share(self, share_id):
        """Subscribe a share."""
        self._answer_share(share_id, self.dbus.subscribe_share, "Subscribing")

    def unsubscribe_share(self, share_id):
        """Unsubscribe a share."""
        self._answer_share(share_id, self.dbus.unsubscribe_share,
                           "Unsubscribing")

    def send_share_invitation(self, path, mail_address, sh_name, access_level):
        """Send a share invitation."""
        def error(failure):
            """Operation failed."""
            if failure.check(ShareOperationError):
                error = failure.value.error
                logger.info("Sending share invitation finished with error %s "
                            "(path=%r mail_address=%s share_name=%r "
                            "access_level=%s)", error, path, mail_address,
                            sh_name, access_level)
            else:
                logger.error("Unexpected error when sending share invitation "
                             "%s %s (path=%r mail_address=%s share_name=%r "
                             "access_level=%s)", failure.type, failure.value,
                             path, mail_address, sh_name, access_level)

        def success(_):
            """Operation finished ok."""
            logger.info("Sending share invitation finished successfully "
                        "(path=%r mail_address=%s share_name=%r "
                        "access_level=%s", path, mail_address,
                        sh_name, access_level)

        logger.info("Sending share invitation: path=%r mail_address=%s "
                    "share_name=%r access_level=%s", path, mail_address,
                    sh_name, access_level)
        d = self.dbus.send_share_invitation(path, mail_address,
                                            sh_name, access_level)
        d.addCallbacks(success, error)

    @defer.inlineCallbacks
    def _folder_operation(self, operation, value, op_name):
        """Generic folder operation."""
        try:
            result = yield operation(value)
        except FolderOperationError, e:
            logger.info("%s folder (on %r) finished with error: %s",
                        op_name, value, e)
            self.on_folder_op_error_callback(e)
        else:
class DeliverNodeDataTestCase(unittest.TestCase):
    """Send the node data without the home."""

    def setUp(self):
        """Set up the test."""
        self.qc = QueueContent(home='/a/b')
        self.qc.set_shares_dirs('/a/b/link', '/a/b/real')

    def test_share_link_inside_home(self):
        """Assure the share link is inside home."""
        self.assertRaises(ValueError, self.qc.set_shares_dirs,
                          share_link='/a/k', share_real='/a/b/r')

    def test_share_real_inside_home(self):
        """Assure the share real is inside home."""
        self.assertRaises(ValueError, self.qc.set_shares_dirs,
                          share_link='/a/b/r', share_real='/a/k')

    def test_none(self):
        """Test getting with nothing."""
        self.assertEqual(self.qc.node_ops[0][0], ROOT_HOME)
        self.assertEqual(self.qc.node_ops[0][1], {})

    def test_one_home(self):
        """Test getting with one in home."""
        self.qc.add('MakeFile', '67', {'path': '/a/b/foo/fighters'})
        self.assertEqual(self.qc.node_ops[0][0], ROOT_HOME)
        node = self.qc.node_ops[0][1]['foo']
        self.assertEqual(node.last_modified, None)
        self.assertEqual(node.kind, KIND_DIR)
        self.assertEqual(node.operations, [])
        self.assertEqual(node.done, None)
        self.assertEqual(len(node.children), 1)

    def test_two_home(self):
        """Test getting with two in home."""
        self.qc.set_content([('MakeFile', '67',
                             {'path': '/a/b/foo/fighters'})])
        self.qc.set_content([('MakeFile', '99', {'path': '/a/b/bar'})])
        self.assertEqual(self.qc.node_ops[0][0], ROOT_HOME)
        self.assertEqual(len(self.qc.node_ops[0][1]), 2)

        # first node
        node = self.qc.node_ops[0][1]['foo']
        self.assertEqual(node.last_modified, None)
        self.assertEqual(node.kind, KIND_DIR)
        self.assertEqual(node.operations, [])
        self.assertEqual(node.done, None)
        self.assertEqual(len(node.children), 1)

        # second node
        node = self.qc.node_ops[0][1]['bar']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_FILE)
        expected = [('99', 'MakeFile',
                     {'path': '/a/b/bar', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

    def test_one_share(self):
        """Test getting with one share."""
        self.qc.set_content([('MakeFile', '12', {'path': '/a/b/real/foo'})])
        self.assertEqual(self.qc.node_ops[0][0], ROOT_HOME)

        node = self.qc.node_ops[0][1]['link']
        self.assertEqual(node.last_modified, None)
        self.assertEqual(node.kind, KIND_DIR)
        self.assertEqual(node.operations, [])
        self.assertEqual(node.done, None)
        self.assertEqual(len(node.children), 1)

        node = node.children['foo']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_FILE)
        expected = [('12', 'MakeFile',
                     {'path': '/a/b/real/foo', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

    def test_several_mixed(self):
        """Test mixing two nodes in the same share, other share, and home."""
        self.qc.set_content([('MakeDir', '0', {'path': '/a/b/j'})])
        self.qc.set_content([('MakeDir', '1', {'path': '/a/b/real/foo/bar1'})])
        self.qc.set_content([('MakeDir', '2', {'path': '/a/b/real/foo/bar2'})])
        self.qc.set_content([('MakeDir', '3', {'path': '/a/b/real/othr/baz'})])
        self.assertEqual(self.qc.node_ops[0][0], ROOT_HOME)
        self.assertIn('j', self.qc.node_ops[0][1])
        link_dir = self.qc.node_ops[0][1]['link']
        self.assertIn('othr', link_dir.children)
        foo_dir = link_dir.children['foo']
        self.assertIn('bar1', foo_dir.children)
        self.assertIn('bar2', foo_dir.children)