"""
Ported to Python 3.
"""
from __future__ import unicode_literals
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from future.utils import PY2
if PY2:
    from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min  # noqa: F401

import os.path
from urllib.parse import quote as url_quote
from collections import defaultdict
from io import BytesIO

from twisted.python.failure import Failure
from allmydata.scripts.common import get_alias, escape_path, \
                                     DefaultAliasMarker, TahoeError
from allmydata.scripts.common_http import do_http, HTTPError
from allmydata import uri
from allmydata.util import fileutil
from allmydata.util.fileutil import abspath_expanduser_unicode, precondition_abspath
from allmydata.util.encodingutil import unicode_to_url, listdir_unicode, quote_output, \
    quote_local_unicode_path, to_bytes
from allmydata.util.assertutil import precondition, _assert
from allmydata.util import jsonbytes as json


class MissingSourceError(TahoeError):
    def __init__(self, name, quotefn=quote_output):
        TahoeError.__init__(self, "No such file or directory %s" % quotefn(name))

class FilenameWithTrailingSlashError(TahoeError):
    def __init__(self, name, quotefn=quote_output):
        TahoeError.__init__(self, "source '%s' is not a directory, but ends with a slash" % quotefn(name))

class WeirdSourceError(TahoeError):
    def __init__(self, absname):
        quoted = quote_local_unicode_path(absname)
        TahoeError.__init__(self, "source '%s' is neither a file nor a directory, I can't handle it" % quoted)

def GET_to_file(url):
    resp = do_http("GET", url)
    if resp.status == 200:
        return resp
    raise HTTPError("Error during GET", resp)

def GET_to_string(url):
    f = GET_to_file(url)
    return f.read()

def PUT(url, data):
    resp = do_http("PUT", url, data)
    if resp.status in (200, 201):
        return resp.read()
    raise HTTPError("Error during PUT", resp)

def POST(url, data):
    resp = do_http("POST", url, data)
    if resp.status in (200, 201):
        return resp.read()
    raise HTTPError("Error during POST", resp)

def mkdir(targeturl):
    url = targeturl + "?t=mkdir"
    resp = do_http("POST", url)
    if resp.status in (200, 201):
        return resp.read().strip()
    raise HTTPError("Error during mkdir", resp)

def make_tahoe_subdirectory(nodeurl, parent_writecap, name):
    url = nodeurl + "/".join(["uri",
                              url_quote(parent_writecap),
                              url_quote(unicode_to_url(name)),
                              ]) + "?t=mkdir"
    resp = do_http("POST", url)
    if resp.status in (200, 201):
        return resp.read().strip()
    raise HTTPError("Error during mkdir", resp)


class LocalFileSource(object):
    def __init__(self, pathname, basename):
        precondition_abspath(pathname)
        self.pathname = pathname
        self._basename = basename

    def basename(self):
        return self._basename

    def need_to_copy_bytes(self):
        return True

    def open(self, caps_only):
        return open(self.pathname, "rb")

class LocalFileTarget(object):
    def __init__(self, pathname):
        precondition_abspath(pathname)
        self.pathname = pathname

    def put_file(self, inf):
        fileutil.put_file(self.pathname, inf)

class LocalMissingTarget(object):
    def __init__(self, pathname):
        precondition_abspath(pathname)
        self.pathname = pathname

    def put_file(self, inf):
        fileutil.put_file(self.pathname, inf)

class LocalDirectorySource(object):
    def __init__(self, progressfunc, pathname, basename):
        precondition_abspath(pathname)

        self.progressfunc = progressfunc
        self.pathname = pathname
        self.children = None
        self._basename = basename

    def basename(self):
        return self._basename

    def populate(self, recurse):
        if self.children is not None:
            return
        self.children = {}
        children = listdir_unicode(self.pathname)
        for i,n in enumerate(children):
            self.progressfunc("examining %d of %d" % (i+1, len(children)))
            pn = os.path.join(self.pathname, n)
            if os.path.isdir(pn):
                child = LocalDirectorySource(self.progressfunc, pn, n)
                self.children[n] = child
                if recurse:
                    child.populate(recurse=True)
            elif os.path.isfile(pn):
                self.children[n] = LocalFileSource(pn, n)
            else:
                # Could be dangling symlink; probably not copy-able.
                # TODO: output a warning
                pass

class LocalDirectoryTarget(object):
    def __init__(self, progressfunc, pathname):
        precondition_abspath(pathname)

        self.progressfunc = progressfunc
        self.pathname = pathname
        self.children = None

    def populate(self, recurse):
        if self.children is not None:
            return
        self.children = {}
        children = listdir_unicode(self.pathname)
        for i,n in enumerate(children):
            self.progressfunc("examining %d of %d" % (i+1, len(children)))
            pn = os.path.join(self.pathname, n)
            if os.path.isdir(pn):
                child = LocalDirectoryTarget(self.progressfunc, pn)
                self.children[n] = child
                if recurse:
                    child.populate(recurse=True)
            else:
                assert os.path.isfile(pn)
                self.children[n] = LocalFileTarget(pn)

    def get_child_target(self, name):
        precondition(isinstance(name, str), name)
        precondition(len(name), name) # don't want ""
        if self.children is None:
            self.populate(recurse=False)
        if name in self.children:
            return self.children[name]
        pathname = os.path.join(self.pathname, name)
        os.makedirs(pathname)
        child = LocalDirectoryTarget(self.progressfunc, pathname)
        self.children[name] = child
        return child

    def put_file(self, name, inf):
        precondition(isinstance(name, str), name)
        pathname = os.path.join(self.pathname, name)
        fileutil.put_file(pathname, inf)

    def set_children(self):
        pass


class TahoeFileSource(object):
    def __init__(self, nodeurl, mutable, writecap, readcap, basename):
        self.nodeurl = nodeurl
        self.mutable = mutable
        self.writecap = writecap
        self.readcap = readcap
        self._basename = basename # unicode, or None for raw filecaps

    def basename(self):
        return self._basename

    def need_to_copy_bytes(self):
        if self.mutable:
            return True
        return False

    def open(self, caps_only):
        if caps_only:
            return BytesIO(self.readcap)
        url = self.nodeurl + "uri/" + url_quote(self.readcap)
        return GET_to_file(url)

    def bestcap(self):
        return self.writecap or self.readcap


def seekable(file_like):
    """Return whether the file-like object is seekable."""
    return hasattr(file_like, "seek") and (
        not hasattr(file_like, "seekable") or file_like.seekable()
    )


class TahoeFileTarget(object):
    def __init__(self, nodeurl, mutable, writecap, readcap, url):
        self.nodeurl = nodeurl
        self.mutable = mutable
        self.writecap = writecap
        self.readcap = readcap
        self.url = url

    def put_file(self, inf):
        # We want to replace this object in-place.
        assert self.url
        # our do_http() call currently requires a string or a filehandle with
        # a real .seek
        if not seekable(inf):
            inf = inf.read()
        PUT(self.url, inf)
        # TODO: this always creates immutable files. We might want an option
        # to always create mutable files, or to copy mutable files into new
        # mutable files. ticket #835

class TahoeDirectorySource(object):
    def __init__(self, nodeurl, cache, progressfunc, basename):
        self.nodeurl = nodeurl
        self.cache = cache
        self.progressfunc = progressfunc
        self._basename = basename # unicode, or None for raw dircaps

    def basename(self):
        return self._basename

    def init_from_grid(self, writecap, readcap):
        self.writecap = writecap
        self.readcap = readcap
        bestcap = writecap or readcap
        url = self.nodeurl + "uri/%s" % url_quote(bestcap)
        resp = do_http("GET", url + "?t=json")
        if resp.status != 200:
            raise HTTPError("Error examining source directory", resp)
        parsed = json.loads(resp.read())
        nodetype, d = parsed
        assert nodetype == "dirnode"
        self.mutable = d.get("mutable", False) # older nodes don't provide it
        self.children_d = dict( [(str(name),value)
                                 for (name,value)
                                 in d["children"].items()] )
        self.children = None

    def init_from_parsed(self, parsed):
        nodetype, d = parsed
        self.writecap = to_bytes(d.get("rw_uri"))
        self.readcap = to_bytes(d.get("ro_uri"))
        self.mutable = d.get("mutable", False) # older nodes don't provide it
        self.children_d = dict( [(str(name),value)
                                 for (name,value)
                                 in d["children"].items()] )
        self.children = None

    def populate(self, recurse):
        if self.children is not None:
            return
        self.children = {}
        for i,(name, data) in enumerate(self.children_d.items()):
            self.progressfunc("examining %d of %d" % (i+1, len(self.children_d)))
            if data[0] == "filenode":
                mutable = data[1].get("mutable", False)
                writecap = to_bytes(data[1].get("rw_uri"))
                readcap = to_bytes(data[1].get("ro_uri"))
                self.children[name] = TahoeFileSource(self.nodeurl, mutable,
                                                      writecap, readcap, name)
            elif data[0] == "dirnode":
                writecap = to_bytes(data[1].get("rw_uri"))
                readcap = to_bytes(data[1].get("ro_uri"))
                if writecap and writecap in self.cache:
                    child = self.cache[writecap]
                elif readcap and readcap in self.cache:
                    child = self.cache[readcap]
                else:
                    child = TahoeDirectorySource(self.nodeurl, self.cache,
                                                 self.progressfunc, name)
                    child.init_from_grid(writecap, readcap)
                    if writecap:
                        self.cache[writecap] = child
                    if readcap:
                        self.cache[readcap] = child
                    if recurse:
                        child.populate(recurse=True)
                self.children[name] = child
            else:
                # TODO: there should be an option to skip unknown nodes.
                raise TahoeError("Cannot copy unknown nodes (ticket #839). "
                                 "You probably need to use a later version of "
                                 "Tahoe-LAFS to copy this directory.")

class TahoeMissingTarget(object):
    def __init__(self, url):
        self.url = url

    def put_file(self, inf):
        # We want to replace this object in-place.
        if not seekable(inf):
            inf = inf.read()
        PUT(self.url, inf)
        # TODO: this always creates immutable files. We might want an option
        # to always create mutable files, or to copy mutable files into new
        # mutable files.

    def put_uri(self, filecap):
        # I'm not sure this will always work
        return PUT(self.url + "?t=uri", filecap)

class TahoeDirectoryTarget(object):
    def __init__(self, nodeurl, cache, progressfunc):
        self.nodeurl = nodeurl
        self.cache = cache
        self.progressfunc = progressfunc
        self.new_children = {}

    def init_from_parsed(self, parsed):
        nodetype, d = parsed
        self.writecap = to_bytes(d.get("rw_uri"))
        self.readcap = to_bytes(d.get("ro_uri"))
        self.mutable = d.get("mutable", False) # older nodes don't provide it
        self.children_d = dict( [(str(name),value)
                                 for (name,value)
                                 in d["children"].items()] )
        self.children = None

    def init_from_grid(self, writecap, readcap):
        self.writecap = writecap
        self.readcap = readcap
        bestcap = writecap or readcap
        url = self.nodeurl + "uri/%s" % url_quote(bestcap)
        resp = do_http("GET", url + "?t=json")
        if resp.status != 200:
            raise HTTPError("Error examining target directory", resp)
        parsed = json.loads(resp.read())
        nodetype, d = parsed
        assert nodetype == "dirnode"
        self.mutable = d.get("mutable", False) # older nodes don't provide it
        self.children_d = dict( [(str(name),value)
                                 for (name,value)
                                 in d["children"].items()] )
        self.children = None

    def just_created(self, writecap):
        # TODO: maybe integrate this with the constructor
        self.writecap = writecap
        self.readcap = uri.from_string(writecap).get_readonly().to_string()
        self.mutable = True
        self.children_d = {}
        self.children = {}

    def populate(self, recurse):
        if self.children is not None:
            return
        self.children = {}
        for i,(name, data) in enumerate(self.children_d.items()):
            self.progressfunc("examining %d of %d" % (i+1, len(self.children_d)))
            if data[0] == "filenode":
                mutable = data[1].get("mutable", False)
                writecap = to_bytes(data[1].get("rw_uri"))
                readcap = to_bytes(data[1].get("ro_uri"))
                url = None
                if self.writecap:
                    url = self.nodeurl + "/".join(["uri",
                                                   url_quote(self.writecap),
                                                   url_quote(unicode_to_url(name))])
                self.children[name] = TahoeFileTarget(self.nodeurl, mutable,
                                                      writecap, readcap, url)
            elif data[0] == "dirnode":
                writecap = to_bytes(data[1].get("rw_uri"))
                readcap = to_bytes(data[1].get("ro_uri"))
                if writecap and writecap in self.cache:
                    child = self.cache[writecap]
                elif readcap and readcap in self.cache:
                    child = self.cache[readcap]
                else:
                    child = TahoeDirectoryTarget(self.nodeurl, self.cache,
                                                 self.progressfunc)
                    child.init_from_grid(writecap, readcap)
                    if writecap:
                        self.cache[writecap] = child
                    if readcap:
                        self.cache[readcap] = child
                    if recurse:
                        child.populate(recurse=True)
                self.children[name] = child
            else:
                # TODO: there should be an option to skip unknown nodes.
                raise TahoeError("Cannot copy unknown nodes (ticket #839). "
                                 "You probably need to use a later version of "
                                 "Tahoe-LAFS to copy this directory.")

    def get_child_target(self, name):
        # return a new target for a named subdirectory of this dir
        precondition(isinstance(name, str), name)
        if self.children is None:
            self.populate(recurse=False)
        if name in self.children:
            return self.children[name]
        writecap = make_tahoe_subdirectory(self.nodeurl, self.writecap, name)
        child = TahoeDirectoryTarget(self.nodeurl, self.cache,
                                     self.progressfunc)
        child.just_created(writecap)
        self.children[name] = child
        return child

    def put_file(self, name, inf):
        precondition(isinstance(name, str), name)
        url = self.nodeurl + "uri"
        if not seekable(inf):
            inf = inf.read()

        if self.children is None:
            self.populate(recurse=False)

        # Check to see if we already have a mutable file by this name.
        # If so, overwrite that file in place.
        if name in self.children and self.children[name].mutable:
            self.children[name].put_file(inf)
        else:
            filecap = PUT(url, inf)
            # TODO: this always creates immutable files. We might want an option
            # to always create mutable files, or to copy mutable files into new
            # mutable files.
            self.new_children[name] = filecap

    def put_uri(self, name, filecap):
        precondition(isinstance(name, str), name)
        self.new_children[name] = filecap

    def set_children(self):
        if not self.new_children:
            return
        url = (self.nodeurl + "uri/" + url_quote(self.writecap)
               + "?t=set_children")
        set_data = {}
        for (name, filecap) in list(self.new_children.items()):
            # it just so happens that ?t=set_children will accept both file
            # read-caps and write-caps as ['rw_uri'], and will handle either
            # correctly. So don't bother trying to figure out whether the one
            # we have is read-only or read-write.
            # TODO: think about how this affects forward-compatibility for
            # unknown caps
            set_data[name] = ["filenode", {"rw_uri": filecap}]
        body = json.dumps_bytes(set_data)
        POST(url, body)

FileSources = (LocalFileSource, TahoeFileSource)
DirectorySources = (LocalDirectorySource, TahoeDirectorySource)
FileTargets = (LocalFileTarget, TahoeFileTarget)
DirectoryTargets = (LocalDirectoryTarget, TahoeDirectoryTarget)
MissingTargets = (LocalMissingTarget, TahoeMissingTarget)

class Copier(object):

    def do_copy(self, options, progressfunc=None):
        if options['quiet']:
            verbosity = 0
        elif options['verbose']:
            verbosity = 2
        else:
            verbosity = 1

        nodeurl = options['node-url']
        if nodeurl[-1] != "/":
            nodeurl += "/"
        self.nodeurl = nodeurl
        self.progressfunc = progressfunc
        self.options = options
        self.aliases = options.aliases
        self.verbosity = verbosity
        self.stdout = options.stdout
        self.stderr = options.stderr
        if verbosity >= 2 and not self.progressfunc:
            def progress(message):
                print(message, file=self.stderr)
            self.progressfunc = progress
        self.caps_only = options["caps-only"]
        self.cache = {}
        try:
            status = self.try_copy()
            return status
        except TahoeError as te:
            if verbosity >= 2:
                Failure().printTraceback(self.stderr)
                print(file=self.stderr)
            te.display(self.stderr)
            return 1

    def try_copy(self):
        """
        All usage errors (except for target filename collisions) are caught
        here, not in a subroutine. This bottoms out in copy_file_to_file() or
        copy_things_to_directory().
        """
        source_specs = self.options.sources
        destination_spec = self.options.destination
        recursive = self.options["recursive"]

        target = self.get_target_info(destination_spec)
        precondition(isinstance(target, FileTargets + DirectoryTargets + MissingTargets), target)
        target_has_trailing_slash = destination_spec.endswith("/")

        sources = [] # list of source objects
        for ss in source_specs:
            try:
                si = self.get_source_info(ss)
            except FilenameWithTrailingSlashError as e:
                self.to_stderr(str(e))
                return 1
            precondition(isinstance(si, FileSources + DirectorySources), si)
            sources.append(si)

        # if any source is a directory, must use -r
        # if target is missing:
        #    if source is a single file, target will be a file
        #    else target will be a directory, so mkdir it
        # if there are multiple sources, target must be a dir
        # if target is a file, source must be a single file
        # if target is directory, sources must be named or a dir

        have_source_dirs = any([isinstance(s, DirectorySources)
                                for s in sources])
        if have_source_dirs and not recursive:
            # 'cp dir target' without -r: error
            self.to_stderr("cannot copy directories without --recursive")
            return 1
        del recursive # -r is only used for signalling errors

        if isinstance(target, FileTargets):
            target_is_file = True
        elif isinstance(target, DirectoryTargets):
            target_is_file = False
        else: # isinstance(target, MissingTargets)
            if len(sources) == 1 and isinstance(sources[0], FileSources):
                target_is_file = True
            else:
                target_is_file = False

        if target_is_file and target_has_trailing_slash:
            self.to_stderr("target is not a directory, but ends with a slash")
            return 1

        if len(sources) > 1 and target_is_file:
            self.to_stderr("copying multiple things requires target be a directory")
            return 1

        if target_is_file:
            _assert(len(sources) == 1, sources)
            if not isinstance(sources[0], FileSources):
                # 'cp -r dir existingfile': error
                self.to_stderr("cannot copy directory into a file")
                return 1
            return self.copy_file_to_file(sources[0], target)

        # else target is a directory, so each source must be one of:
        # * a named file (copied to a new file under the target)
        # * a named directory (causes a new directory of the same name to be
        #   created under the target, then the contents of the source are
        #   copied into that directory)
        # * an unnamed directory (the contents of the source are copied into
        #   the target, without a new directory being made)
        #
        # If any source is an unnamed file, throw an error, since we have no
        # way to name the output file.
        _assert(isinstance(target, DirectoryTargets + MissingTargets), target)

        for source in sources:
            if isinstance(source, FileSources) and source.basename() is None:
                self.to_stderr("when copying into a directory, all source files must have names, but %s is unnamed" % quote_output(source_specs[0]))
                return 1
        return self.copy_things_to_directory(sources, target)

    def to_stderr(self, text):
        print(text, file=self.stderr)

    # FIXME reduce the amount of near-duplicate code between get_target_info
    # and get_source_info.

    def get_target_info(self, destination_spec):
        precondition(isinstance(destination_spec, str), destination_spec)
        rootcap, path_utf8 = get_alias(self.aliases, destination_spec, None)
        path = path_utf8.decode("utf-8")
        if rootcap == DefaultAliasMarker:
            # no alias, so this is a local file
            pathname = abspath_expanduser_unicode(path)
            if not os.path.exists(pathname):
                t = LocalMissingTarget(pathname)
            elif os.path.isdir(pathname):
                t = LocalDirectoryTarget(self.progress, pathname)
            else:
                # TODO: should this be _assert? what happens if the target is
                # a special file?
                assert os.path.isfile(pathname), pathname
                t = LocalFileTarget(pathname) # non-empty
        else:
            # this is a tahoe object
            url = self.nodeurl + "uri/%s" % url_quote(rootcap)
            if path:
                url += "/" + escape_path(path)

            resp = do_http("GET", url + "?t=json")
            if resp.status == 404:
                # doesn't exist yet
                t = TahoeMissingTarget(url)
            elif resp.status == 200:
                parsed = json.loads(resp.read())
                nodetype, d = parsed
                if nodetype == "dirnode":
                    t = TahoeDirectoryTarget(self.nodeurl, self.cache,
                                             self.progress)
                    t.init_from_parsed(parsed)
                else:
                    writecap = to_bytes(d.get("rw_uri"))
                    readcap = to_bytes(d.get("ro_uri"))
                    mutable = d.get("mutable", False)
                    t = TahoeFileTarget(self.nodeurl, mutable,
                                        writecap, readcap, url)
            else:
                raise HTTPError("Error examining target %s"
                                 % quote_output(destination_spec), resp)
        return t

    def get_source_info(self, source_spec):
        """
        This turns an argv string into a (Local|Tahoe)(File|Directory)Source.
        """
        precondition(isinstance(source_spec, str), source_spec)
        rootcap, path_utf8 = get_alias(self.aliases, source_spec, None)
        path = path_utf8.decode("utf-8")
        # any trailing slash is removed in abspath_expanduser_unicode(), so
        # make a note of it here, to throw an error later
        had_trailing_slash = path.endswith("/")
        if rootcap == DefaultAliasMarker:
            # no alias, so this is a local file
            pathname = abspath_expanduser_unicode(path)
            name = os.path.basename(pathname)
            if not os.path.exists(pathname):
                raise MissingSourceError(source_spec, quotefn=quote_local_unicode_path)
            if os.path.isdir(pathname):
                t = LocalDirectorySource(self.progress, pathname, name)
            else:
                if had_trailing_slash:
                    raise FilenameWithTrailingSlashError(source_spec,
                                                         quotefn=quote_local_unicode_path)
                if not os.path.isfile(pathname):
                    raise WeirdSourceError(pathname)
                t = LocalFileSource(pathname, name) # non-empty
        else:
            # this is a tahoe object
            url = self.nodeurl + "uri/%s" % url_quote(rootcap)
            name = None
            if path:
                if path.endswith("/"):
                    path = path[:-1]
                url += "/" + escape_path(path)
                last_slash = path.rfind(u"/")
                name = path
                if last_slash != -1:
                    name = path[last_slash+1:]

            resp = do_http("GET", url + "?t=json")
            if resp.status == 404:
                raise MissingSourceError(source_spec)
            elif resp.status != 200:
                raise HTTPError("Error examining source %s" % quote_output(source_spec),
                                resp)
            parsed = json.loads(resp.read())
            nodetype, d = parsed
            if nodetype == "dirnode":
                t = TahoeDirectorySource(self.nodeurl, self.cache,
                                         self.progress, name)
                t.init_from_parsed(parsed)
            else:
                if had_trailing_slash:
                    raise FilenameWithTrailingSlashError(source_spec)
                writecap = to_bytes(d.get("rw_uri"))
                readcap = to_bytes(d.get("ro_uri"))
                mutable = d.get("mutable", False) # older nodes don't provide it
                t = TahoeFileSource(self.nodeurl, mutable, writecap, readcap, name)
        return t


    def need_to_copy_bytes(self, source, target):
        # This should likley be a method call! but enabling that triggers
        # additional bugs. https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3719
        if source.need_to_copy_bytes:
            # mutable tahoe files, and local files
            return True
        if isinstance(target, (LocalFileTarget, LocalDirectoryTarget)):
            return True
        return False

    def announce_success(self, msg):
        if self.verbosity >= 1:
            print("Success: %s" % msg, file=self.stdout)
        return 0

    def copy_file_to_file(self, source, target):
        precondition(isinstance(source, FileSources), source)
        precondition(isinstance(target, FileTargets + MissingTargets), target)
        if self.need_to_copy_bytes(source, target):
            # if the target is a local directory, this will just write the
            # bytes to disk. If it is a tahoe directory, it will upload the
            # data, and stash the new filecap for a later set_children call.
            f = source.open(self.caps_only)
            target.put_file(f)
            return self.announce_success("file copied")
        # otherwise we're copying tahoe to tahoe, and using immutable files,
        # so we can just make a link. TODO: this probably won't always work:
        # need to enumerate the cases and analyze them.
        target.put_uri(source.bestcap())
        return self.announce_success("file linked")

    def copy_things_to_directory(self, sources, target):
        # step one: if the target is missing, we should mkdir it
        target = self.maybe_create_target(target)
        target.populate(recurse=False)

        # step two: scan any source dirs, recursively, to find children
        for s in sources:
            if isinstance(s, DirectorySources):
                s.populate(recurse=True)
            if isinstance(s, FileSources):
                # each source must have a name, or be a directory
                _assert(s.basename() is not None, s)

        # step three: find a target for each source node, creating
        # directories as necessary. 'targetmap' is a dictionary that uses
        # target Directory instances as keys, and has values of (name:
        # sourceobject) dicts for all the files that need to wind up there.
        targetmap = self.build_targetmap(sources, target)

        # target name collisions are an error
        collisions = []
        for target, sources in list(targetmap.items()):
            target_names = {}
            for source in sources:
                name = source.basename()
                if name in target_names:
                    collisions.append((target, source, target_names[name]))
                else:
                    target_names[name] = source
        if collisions:
            self.to_stderr("cannot copy multiple files with the same name into the same target directory")
            # I'm not sure how to show where the collisions are coming from
            #for (target, source1, source2) in collisions:
            #    self.to_stderr(source1.basename())
            return 1

        # step four: walk through the list of targets. For each one, copy all
        # the files. If the target is a TahoeDirectory, upload and create
        # read-caps, then do a set_children to the target directory.
        self.copy_to_targetmap(targetmap)

        return self.announce_success("files copied")

    def maybe_create_target(self, target):
        if isinstance(target, LocalMissingTarget):
            os.makedirs(target.pathname)
            target = LocalDirectoryTarget(self.progress, target.pathname)
        elif isinstance(target, TahoeMissingTarget):
            writecap = mkdir(target.url)
            target = TahoeDirectoryTarget(self.nodeurl, self.cache,
                                          self.progress)
            target.just_created(writecap)
        # afterwards, or otherwise, it will be a directory
        precondition(isinstance(target, DirectoryTargets), target)
        return target

    def build_targetmap(self, sources, target):
        num_source_files = len([s for s in sources
                                if isinstance(s, FileSources)])
        num_source_dirs = len([s for s in sources
                               if isinstance(s, DirectorySources)])
        self.progress("attaching sources to targets, "
                      "%d files / %d dirs in root" %
                      (num_source_files, num_source_dirs))

        # this maps each target directory to a list of source files that need
        # to be copied into it. All source files have names.
        targetmap = defaultdict(list)

        for s in sources:
            if isinstance(s, FileSources):
                targetmap[target].append(s)
            else:
                _assert(isinstance(s, DirectorySources), s)
                name = s.basename()
                if name is not None:
                    # named sources get a new directory. see #2329
                    new_target = target.get_child_target(name)
                else:
                    # unnamed sources have their contents copied directly
                    new_target = target
                self.assign_targets(targetmap, s, new_target)

        self.progress("targets assigned, %s dirs, %s files" %
                      (len(targetmap), self.count_files_to_copy(targetmap)))
        return targetmap

    def assign_targets(self, targetmap, source, target):
        # copy everything in the source into the target
        precondition(isinstance(source, DirectorySources), source)
        for name, child in list(source.children.items()):
            if isinstance(child, DirectorySources):
                # we will need a target directory for this one
                subtarget = target.get_child_target(name)
                self.assign_targets(targetmap, child, subtarget)
            else:
                _assert(isinstance(child, FileSources), child)
                targetmap[target].append(child)

    def copy_to_targetmap(self, targetmap):
        files_to_copy = self.count_files_to_copy(targetmap)
        self.progress("starting copy, %d files, %d directories" %
                      (files_to_copy, len(targetmap)))
        files_copied = 0
        targets_finished = 0

        for target, sources in list(targetmap.items()):
            _assert(isinstance(target, DirectoryTargets), target)
            for source in sources:
                _assert(isinstance(source, FileSources), source)
                self.copy_file_into_dir(source, source.basename(), target)
                files_copied += 1
                self.progress("%d/%d files, %d/%d directories" %
                              (files_copied, files_to_copy,
                               targets_finished, len(targetmap)))
            target.set_children()
            targets_finished += 1
            self.progress("%d/%d directories" %
                          (targets_finished, len(targetmap)))

    def count_files_to_copy(self, targetmap):
        return sum([len(sources) for sources in targetmap.values()])

    def copy_file_into_dir(self, source, name, target):
        precondition(isinstance(source, FileSources), source)
        precondition(isinstance(target, DirectoryTargets), target)
        precondition(isinstance(name, str), name)
        if self.need_to_copy_bytes(source, target):
            # if the target is a local directory, this will just write the
            # bytes to disk. If it is a tahoe directory, it will upload the
            # data, and stash the new filecap for a later set_children call.
            f = source.open(self.caps_only)
            target.put_file(name, f)
            return
        # otherwise we're copying tahoe to tahoe, and using immutable files,
        # so we can just make a link
        target.put_uri(name, source.bestcap())


    def progress(self, message):
        #print(message)
        if self.progressfunc:
            self.progressfunc(message)


def copy(options):
    return Copier().do_copy(options)

# error cases that need improvement:
#  local-file-in-the-way
#   touch proposed
#   tahoe cp -r my:docs/proposed/denver.txt proposed/denver.txt
#  handling of unknown nodes

# things that maybe should be errors but aren't
#  local-dir-in-the-way
#   mkdir denver.txt
#   tahoe cp -r my:docs/proposed/denver.txt denver.txt
#   (creates denver.txt/denver.txt)

# error cases that look good:
#  tahoe cp -r my:docs/missing missing
#  disconnect servers
#   tahoe cp -r my:docs/missing missing  -> No JSON object could be decoded
#  tahoe-file-in-the-way (when we want to make a directory)
#   tahoe put README my:docs
#   tahoe cp -r docs/proposed my:docs/proposed
