#!/usr/bin/env python3
# encoding: utf-8
"""
    = About links =

    Before use this script, you should know that:
     - a file is a pointer to inodes (filesystem datas);
     - hard link is another file that points on the same inodes;
     - symbolic link is another file that points address of the file.

    In both cases,
       You can move the link file without breaking the link.
       When you edit the link file, the linked file is changed.

    Therefore :

     A symbolic link is a pointer to the path of a file.
      -> you can see which file is pointed;
      -> if you move, rename or remove the file,
          then the link is broken and content inaccessible;
      -> you can change the content by moving the file pointed
          and placing another file at the same path.

     An hard link is a pointer to the content (inodes),
      in fact it is interpreted as a standard file.
      -> you can't see the file pointed (you can only find -samefile ./);
      -> the link will be broken only if you remove the file;
         in that case the content remains accessible with the link.
         You can move and rename freely the file pointed.
"""
USAGE = """
# List symlinks
       relinker src -ls [-v]

# Modify symlinks
       relinker src -subst SUBST_PATTERN [-v] [--dry-run]

# Transform duplicated files into links
       relinker src res (-s | -d | -c) [--fullpath] [-v] [--dry-run]

# Help
       relinker -h

"""
from glob import glob
from os import link, symlink, remove, readlink, unlink
from os.path import getsize, realpath, relpath, dirname, isfile, isdir, islink, join
from shutil import copyfile
import argparse
import filecmp
import re

def _filesize(num):
    for unit in ['', 'k', 'M', 'G']:
        if abs(num) < 1024.0:
            return "%3.1f%s" % (num, unit)
        num /= 1024.0
    return "Too big"



def main(src=None, res=None, linkcmd=None, verbose=False, dry=False,
         relative=True, subst_pattern=None, list_symlink=False):
    dirlink = {
            src: [f for f in glob(src + '/**', recursive=True) if islink(f) and isdir(f)]
            }
    if res:
        dirlink[res] = [f for f in glob(res + '/**', recursive=True) if islink(f) and isdir(f)]

    def belongtolink(f, ref):
        nonlocal dirlink
        res = False
        for fref in dirlink[ref]:
            if f.startswith(fref):
                res = True
                break
        return res

    def found(fsrc, fname):
        rpath = realpath(join(dirname(fsrc), fname))
        return ('%s' if isfile(rpath) else "%s (not found)") % rpath

    if list_symlink:
        files_src = [f for f in glob(src + '/**', recursive=True) if islink(f) and isfile(f) and not belongtolink(f, src)]
        for fsrc in files_src:
            if verbose:
                orig_link = readlink(fsrc)
                print("[Link]  %s\n       -> %s\nTarget  %s\n" % (
                    fsrc, orig_link, found(fsrc, orig_link)
                    ))
            else:
                print(fsrc)

    elif subst_pattern:
        files_src = [f for f in glob(src + '/**', recursive=True) if islink(f) and isfile(f)  and not belongtolink(f, src)]
        pat_from, pat_to = eval('(' + subst_pattern + ')')
        def found(fsrc, fname):
            rpath = realpath(join(dirname(fsrc), fname))
            return ('%s' if isfile(rpath) else "%s (not found)") % rpath
        for fsrc in files_src:
            orig_link = readlink(fsrc)
            target_link = re.sub(pat_from, pat_to, orig_link)
            if orig_link == target_link:
                continue
            if verbose:
                print("[Link]  %s\n       -> %s\nOld target  %s\nNew target  %s\n" % (
                    fsrc, target_link, found(fsrc, orig_link), found(fsrc, target_link)
                    ))
            if not dry:
                unlink(fsrc)
                symlink(target_link, fsrc)
    else:
        files_src = [f for f in glob(src + '/**', recursive=True) if isfile(f) and not belongtolink(f, src)]
        files_res = [f for f in glob(res + '/**', recursive=True) if isfile(f) and not belongtolink(f, res)]

        totalgained = 0
        for fsrc in files_src:
            for fres in [f for f in files_res if filecmp.cmp(fsrc, f)]:
                if verbose:
                    print("%s\n-> %s" % (fres, fsrc))
                totalgained += getsize(fsrc)
                if callable(linkcmd) and not dry:
                    remove(fsrc)
                    linkcmd(realpath(fres) if not relative else
                              relpath(realpath(fres), dirname(realpath(fsrc))),
                       fsrc)
                break
        print(
            "Total %s : %s" % (
                'loss expected' if linkcmd.__name__[:4] == 'copy'
                else 'gain expected',
                _filesize(totalgained)
        ))


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        usage=USAGE
    )
    parser.add_argument(
        'src',
        help="(project) contains copied files for purpose")
    parser.add_argument(
        'res',nargs='?',
        help="(library) contains ressources files")
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument(
        '-ls' ,'--list-symlink', action='store_true', dest='list_symlink',
        help="list all symlink in dir"
    )
    group.add_argument(
        '-subst', action='store', dest='subst_pattern',
        help="a python substitution \"'from','to'\" (see /usr/lib/python*/re.py for special chars)"
    )
    group.add_argument(
        '-s', '--symbolic', action='store_const', dest='linkcmd', const=symlink,
        help="link points to the path of the file"
    )
    group.add_argument(
        '-d', '--hard', action='store_const', dest='linkcmd', const=link,
        help="link is exactly the same file accessible from an another path"
    )
    group.add_argument(
        '-c', '--copy', action='store_const', dest='linkcmd', const=copyfile,
        help="make a copy instead of a link"
    )
    parser.add_argument(
        '--dry-run', action='store_true', dest='dry', default=False
    )
    parser.add_argument(
        '--fullpath', action='store_false', dest='relative', default=True,
        help="make links from full path instead of relative path"
    )
    parser.add_argument(
        '-v', '--verbose', action='store_true', dest='verbose', default=False
    )
    main(**vars(parser.parse_args()))

