#!/usr/bin/env python
"""Creates symlinks to files in the specified config directories (default "./").

Run from the desired config directory, and files with a second line like

  "dest = ~/.bashrc"

will be symlinked to the specified location.  If a file
already exists, it will be backed up (copied to a file with a .bak extension).
Existing symlinks will be overwritten.
"""
from __future__ import print_function

import os.path
import re
import sys
from optparse import OptionParser, TitledHelpFormatter
import subprocess
import warnings

# Parse arguments
usage = "Usage: %prog [options] dir1 dir2 ..."
parser = OptionParser(
    usage=usage,
    description=__doc__.splitlines()[0],
    epilog="\n".join(__doc__.splitlines()[2:]),
)
parser.add_option(
    "--home",
    type="string",
    dest="home_dir",
    default="~",
    help=(
        "use <home> rather than ~ for installation."
        + "(Used to replace '~' in dest strings.)"
    ),
    metavar="<home>",
)
parser.add_option(
    "-v",
    "--verbose",
    action="store_true",
    dest="verbose",
    default=False,
    help="print lots of information",
)
parser.add_option(
    "-n",
    "--no-action",
    action="store_false",
    dest="action",
    default=True,
    help=("don't do anything:" + "only print commands that would be executed"),
)
parser.add_option(
    "-i",
    "--interactive",
    action="store_true",
    dest="interactive",
    default=False,
    help="prompt before taking action",
)
parser.add_option(
    "-a",
    "--abs-path",
    action="store_true",
    dest="abspath",
    default=False,
    help="Use absolute symlinks (defaults are relative to ~)",
)

(options, args) = parser.parse_args()

# These are directories that should be ignored -- mainly includes
# version control information.
_IGNORE_DIRS = ["CVS", ".svn", ".hg"]


##################
# Helper functions
def yes_no(default=""):
    """This function prompts the user for a yes or no answer and will
    accept a default response.  Exceptions (such as EOF) are not
    caught.  The result is True for 'yes' and False for 'no'."""

    def validate(ans):
        ans = ans.lower()
        yes = ["y", "yes"]
        no = ["n", "no"]
        try:
            yes.index(ans)
            return True
        except:
            pass
        try:
            no.index(ans)
            return False
        except:
            pass
        return None

    def raw_input_default(prompt, default):
        response = raw_input(prompt)
        if 0 == len(response):
            response = default
        return response

    prompts = {None: "yes/no? ", True: "Yes/no? ", False: "yes/No? "}
    prompt = prompts[validate(default)]

    ans = validate(raw_input_default(prompt, default))
    while ans is None:
        print('Please answer "yes" or "no".')
        ans = validate(raw_input_default(prompt, default))
    return ans


class AttributeError(Exception):
    pass


def check_access(path, mode):
    """Check that path has proper access as specified by mode.

    Throws an AttributeError on failure
    """
    if not os.access(path, mode):
        err = "Path " + path + " has invalid permissions:"
        tests = [
            (os.F_OK, "exist"),
            (os.R_OK, "be readable"),
            (os.W_OK, "be writable"),
            (os.X_OK, "be executable"),
        ]
        for (test_mode, msg) in tests:
            if (mode & test_mode) and not os.access(path, test_mode):
                err = err + "\n- Path must " + msg
                raise AttributeError(err)
    else:
        return


def execute(command, cwd=None):
    """This function executes the specified command at the os level."""

    try:
        retcode = subprocess.call(command, shell=True, cwd=cwd)
        if retcode < 0:
            print("Child was terminated by signal {}".format(-retcode), file=sys.stderr)
        else:
            print("Child returned {}".format(retcode), file=sys.stderr)
    except OSError as e:
        print("Execution failed: {}".format(e), file=sys.stderr)


def do(cmds, mesg=None, query=None, options=options):
    """Execute a command after first confirming with the user and
    presenting information (if verbose options are selected).

    Return False if there was an error.
    """
    success = True
    if options.verbose and mesg is not None:
        print(mesg)
    if options.action:
        perform = True
        if options.interactive:
            if query is None:
                print("Perform the following commands?")
                for c in cmds:
                    print(c)
            else:
                print(query)
            perform = yes_no("yes")
        if perform:
            for c in cmds:
                if options.verbose:
                    print(c)
                try:
                    exec(c)
                except Exception as e:
                    print("Command {} failed: {}".format(c, e), file=sys.stderr)
                    success = False
    else:
        for c in cmds:
            print(c)
    return success


def os_do(action, options=options):
    if options.action:
        perform = True
        if options.interactive:
            print("Perform the following action?")
            print(action)
            perform = yes_no("yes")
        if perform:
            execute(action)
    else:
        print(action)


def backup(file):
    """Backup a file and return backup name."""
    bak = ".".join([file, "bak"])
    n = 0
    while os.path.isfile(bak):
        n = n + 1
        bak = "%s.bak%i" % (file, n)

    cmds = ["os.rename('%s', '%s')" % (file, bak)]
    if do(cmds):
        return bak
    else:
        raise Exception("Could not backup %s to %s." % (file, bak))


def is_not_temp(f):
    return ("#" not in f) and ("~" not in f)


#############
# Script Body
def main():
    global options, args

    # Setup directories
    home_dir = os.path.expanduser(options.home_dir)
    check_access(home_dir, os.F_OK | os.R_OK | os.W_OK | os.X_OK)
    if options.verbose:
        print("Using <home> = {}".format(home_dir))

    if not args:
        args = ["."]

    for src_dir in args:
        src_dir = os.path.expanduser(src_dir)
        if "." == src_dir or ".." in src_dir:
            src_dir = os.path.abspath(src_dir)
        if not os.path.isabs(src_dir):
            src_dir = os.path.abspath(src_dir)
        check_access(src_dir, os.F_OK | os.R_OK)

        if options.verbose:
            print("Using dir = {}".format(src_dir))

        # Get all files in src_dir directory
        for (root, dirs, files) in os.walk(src_dir):
            dirs = set(dirs)
            dirs.difference_update(_IGNORE_DIRS)

            files = filter(is_not_temp, files)
            sys_home = os.path.expanduser("~")

            for f in sorted(files):
                src = os.path.join(root, f)

                # Files are linked when the second line of the file
                # looks like "# dest=~/.xsession" with optional whitespace
                try:
                    with open(src, "r") as fd:
                        fd.readline()
                        dest = re.match(
                            r"\A\s*[#;\\]*\s*dest\s*=\s*(\S*)", fd.readline()
                        )
                except Exception:
                    dest = None

                if dest is None:
                    print(
                        f"Warning: No dest = 2nd line in file '{src}'... ignoring",
                        file=sys.stderr,
                    )
                    continue

                dest = os.path.expanduser(dest.group(1))
                if dest.startswith(sys_home):
                    # Replace with new home.

                    dest = os.path.join(home_dir, dest[len(sys_home + os.sep) :])

                dest_dir = os.path.dirname(dest)
                if options.abspath:
                    ln_src = os.path.abspath(src)
                else:
                    ln_src = os.path.relpath(
                        os.path.realpath(src), os.path.realpath(dest_dir)
                    )

                if not os.path.exists(dest_dir):
                    mesg = "Directory %s does not exist." % (dest_dir,)
                    query = "Create %s ?" % (dest_dir,)
                    cmds = ["os.makedirs('%s')" % (dest_dir,)]
                    do(cmds, mesg, query)
                if os.path.islink(dest):
                    mesg = "Symlink %s exists." % (dest,)
                    query = "Remove and replace with link to %s?" % (ln_src,)
                    cmds = [
                        "os.remove('%s')" % (dest,),
                        "os.symlink('%s', '%s')" % (ln_src, dest),
                    ]
                    do(cmds, mesg, query)
                elif os.path.isfile(dest):
                    mesg = "File %s exists." % (dest,)
                    query = "Backup and symlink '%s' -> '%s'?" % (ln_src, dest)
                    cmds = [
                        "backup('%s')" % (dest,),
                        "os.symlink('%s', '%s')" % (ln_src, dest),
                    ]
                    do(cmds, mesg, query)
                else:
                    query = "Link %s to %s?" % (ln_src, dest)
                    cmds = ["os.symlink('%s', '%s')" % (ln_src, dest)]
                    do(cmds, query=query)


if __name__ == "__main__":
    main()
