#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# flo-link-dirtree --- Recursively link a directory with character translation
#
# Copyright (c) 2009 Florent Rougon
#
# This program 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; version 2 dated June, 1991.
#
# 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; see the file COPYING. If not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA  02110-1301 USA.

from __future__ import nested_scopes, division
import sys, os, locale, getopt
import flo_small_funcs

progname = os.path.basename(sys.argv[0])
progversion = "0.2"
version_blurb = """Written by Florent Rougon.

Copyright (c) 2008 Florent Rougon
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."""

usage = """Usage: %(progname)s [option ...] SRC DEST
Create a mirror of a directory tree with links, doing character translation.

Directory DEST is created and filled with symbolic or hard links (depending on
option --symbolic) to files under SRC, so that DEST mirrors the directory tree
SRC at the end of the process.

Files and directories under DEST are created with the same names as their
counterparts under SRC, except that a translation table is applied in order to
obtain the destination names. The default translation table can be overridden
by setting 'translation_table' in the configuration file.

Options:
      --config-file=FILE       use FILE instead of the default configuration
                               file, ~/.%(progname)s/config.py
      --filesystem-encoding=ENCODING  use ENCODING as the filesystem encoding
                               (default: sys.getfilesystemencoding())
  -s, --symbolic               use symbolic links instead of hard links
      --help                   display this message and exit
      --version                output version information and exit""" \
  % { "progname": progname }

params = {}


def os_walk_dumb_error_handling(exception):
    raise exception


def translate_path(path, translation_table):
    for a, b in translation_table:
        path = path.replace(a, b)

    return path


def mirror_dirtree(srcdir, dest):
    if os.path.exists(dest):
        sys.exit("%s: '%s' already exists. Aborting." % (progname, dest))

    encode_func = lambda s: s.encode(params["filesystem_encoding"])
    decode_func = lambda s: s.decode(params["filesystem_encoding"])

    srcdir = os.path.abspath(srcdir)

    # os.walk() returns the same type of string (byte or Unicode) as given in
    # its first argument
    for root_b, dirs_b, files_b in os.walk(encode_func(srcdir),
                                           topdown=True,
                                           onerror=os_walk_dumb_error_handling):
        root = decode_func(root_b)
        dirs = map(decode_func, dirs_b)
        files = map(decode_func, files_b)

        # root, dirs and files are Unicode strings
        assert root.startswith(srcdir)

        rel_src_root = root.replace(srcdir, u"", 1)
        while rel_src_root.startswith(os.sep):
            rel_src_root = rel_src_root[1:]

        rel_dst_root = translate_path(rel_src_root,
                                      params["translation_table"])
        dst_root = os.path.join(dest, rel_dst_root)
        os.mkdir(encode_func(dst_root))

        for f in files:
            src_path = os.path.join(root, f)
            dst_filename = translate_path(f, params["translation_table"])
            dst_path = os.path.join(dst_root, dst_filename)

            if params["symbolic"]:
                os.symlink(encode_func(src_path), encode_func(dst_path))
            else:
                os.link(encode_func(src_path), encode_func(dst_path))


def process_command_line_and_config_file():
    global params

    try:
        opts, args = getopt.getopt(sys.argv[1:], "s",
                                   ["config-file=",
                                    "symbolic",
                                    "filesystem-encoding=",
                                    "help",
                                    "version"])
    except getopt.GetoptError, message:
        sys.stderr.write(usage + "\n")
        return ("exit", 1)

    # Let's start with the options that don't require any non-option argument
    # to be present
    for option, value in opts:
        if option == "--help":
            print usage
            return ("exit", 0)
        elif option == "--version":
            print "%s %s\n%s" % (progname, progversion, version_blurb)
            return ("exit", 0)

    # Now, require a correct invocation.
    if len(args) != 2:
        sys.stderr.write(usage + "\n")
        return ("exit", 1)

    args_encoding = locale.getpreferredencoding()

    params = {"srcdir": args[0].decode(args_encoding),
              "dest": args[1].decode(args_encoding)}

    # Get the home directory, if any, and store it in params (often useful).
    try:
        home_dir = os.environ["HOME"]
    except KeyError:
        home_dir = None
    params["home_dir"] = home_dir

    # Default values for options
    params["symbolic"] = False
    params["filesystem_encoding"] = sys.getfilesystemencoding()
    # params["translation_table"] = ((u"í", u"i"),
    #                                (u":", u"_"))
    params["translation_table"] = ((u":", u"_"),)

    # Parameters recognized in the configuration file(s)
    recognized_params = ("translation_table",
                         "filesystem_encoding")

    # Check if --config-file was used
    cfg_file_specified = None
    for option, value in opts:
        if option == "--config-file":
            if cfg_file_specified is not None:
                raise SeveralConfigFileOptionsSupplied()
            else:
                cfg_file_specified = value
                if not os.path.exists(cfg_file_specified):
                    raise NoSuchConfigurationFile(cfg_file_specified)

    if cfg_file_specified is not None:
        cfg_files = [ cfg_file_specified ]
    else:
        cfg_files = [ "/etc/%s/config.py" % progname ]
        if home_dir is not None:
            cfg_files.append(
                os.path.join(home_dir, ".%s" % progname, "config.py"))

    # Update 'params' with those set in the config files, if any
    flo_small_funcs.import_params_from_python_cfg_files(
        namespace=params, cfg_files=cfg_files,
        recognized_params=recognized_params)

    # General option processing
    for option, value in opts:
        if option in ("-s", "--symbolic"):
            params["symbolic"] = True
        elif option in ("filesystem-encoding",):
            params["filesystem_encoding"] = value
        elif option == "--config-file":
            # Special option, was handled earlier
            pass
        else:
            # The options (such as --help) that cause immediate exit
            # were already checked, and caused the function to return.
            # Therefore, if we are here, it can't be due to any of these
            # options.
            raise ProgramError("Unexpected option received from the "
                               "getopt module: '%s'" % option)

    return ("continue", params)


def main():
    locale.setlocale(locale.LC_ALL, '')
#     try:

    action, params = process_command_line_and_config_file()
    if action == "exit":
        sys.exit(params)

    mirror_dirtree(params["srcdir"], params["dest"])

    sys.exit(0)

#     except error, exc_instance:
#         sys.stderr.write("Error: %s\n" % exc_instance.complete_message())
#         sys.exit(2)
#     except (IOError, OSError), exc_instance:
#         sys.stderr.write("Error: %s\n" % exc_instance)
#         sys.exit(2)
    # I prefer letting other exceptions generate a traceback, because they
    # would probably reveal a bug (besides being more useful that way).

if __name__ == "__main__": main()
