#!/usr/bin/env python

# Use of this source code is governed by a BSD-style license that can be
# found in the COPYING file.

"""
Quick script to replace stuff in files

"""

from __future__ import print_function

import sys
import os
import re
import random
import logging
import fnmatch

from optparse import OptionParser

COLORS = {
    "clear"         : "\033[0m"  ,
    "bold"          : "\033[1m"  ,
    "underline"     : "\033[4m"  ,

    "red"           : "\033[0;31m" ,
    "light-red"     : "\033[1;31m" ,

    "green"         : "\033[0;32m" ,
    "light-green"   : "\033[1;32m" ,

    "blue"          : "\033[0;34m" ,
    "light-blue"    : "\033[1;34m" ,

    "magenta"       : "\033[0;36m" ,
    "light-magenta" : "\033[1;36m" ,

}

COLORS_REPLACE = {
    "pyctx" : COLORS["clear"] + COLORS["light-magenta"],
    "line1" : COLORS["clear"],
    "line2" : COLORS["clear"],
    "line1start" : COLORS["clear"] + COLORS["light-red"],
    "line2start" : COLORS["clear"] + COLORS["light-green"],
    "word1" : COLORS["clear"] + COLORS["underline"] + COLORS["light-red"],
    "word2" : COLORS["clear"] + COLORS["underline"] + COLORS["light-green"],
}

FILTER_OUT = (
    "build-*" ,
    ".git"    ,
    ".svn"    ,
    "*.py[co]",
    "*.[oa]"  ,
    "*.back"  ,
    "*~"      ,
    "*.so"    ,
    "*.a"
)

__usage__ = """
replacer [options]  PATTERN REPL [files]

eg:
  replacer 'toto' 'titi'
  replacer '(.*)toto([0-9]{0,3})' '\\1titi\\2'

Files matching %s are discarded.
""" % (str(FILTER_OUT))

LOGGER = logging.getLogger("replacer")

def is_binary(filename):
    """ Returns True if the file is binary

    """
    with open(filename, 'rb') as fp:
        data = fp.read(1024)
        if not data:
            return False
        if b'\0' in data:
            return True
        return False

def recurse_file(opts, directory, action):
    """
    Recusively go do the subdirectories of the directory,
    calling the action on each file

    """
    for f in os.listdir(directory):
        if opts.get("no_hidden") and f.startswith("."):
            LOGGER.info("filter hidden  : %s/%s", directory, f)
            continue
        filter_out = False
        if not opts.get("no_filter"):
            for fo in FILTER_OUT:
                if fnmatch.fnmatch(f, fo):
                    LOGGER.info("filter %s: %s/%s", fo, directory, f)
                    filter_out = True
                    break
        if filter_out:
            continue
        if opts.get("file_filter"):
            filter_out = True
            for fo in opts.get("file_filter"):
                if fnmatch.fnmatch(f, fo):
                    filter_out = False
                    break
        f = os.path.join(directory, f)
        if os.path.isdir(f):
            recurse_file(opts, f, action)
        if os.path.isfile(f):
            if filter_out:
                continue
            if is_binary(f):
                continue
            action(f)

class Context:
    """ regexp context """
    def __init__(self, filename):
        self.filename = filename

    #VIRTUAL
    def search(self, line):
        """ search for a context line """
        pass

    #VIRTUAL
    def display(self):
        """ display a context if needed """
        pass

class PyContext(Context):
    def __init__(self, filename):
        Context.__init__(self, filename)
        self.regexp     = re.compile("[ \t]*def[ \t].*\(.*\)[ \t]*:")
        self.match      = None
        self.displayed  = False

    def search(self, line):
        """ search for a function or class name """
        if self.regexp.search(line):
            self.match     = line
            self.displayed = False

    def display(self):
        """ display the current function/class name """
        if self.displayed:
            return
        if not self.match:
            return
        print("%sIn: %s%s" % (COLORS_REPLACE["pyctx"], self.match.strip(), COLORS["clear"]))
        self.displayed = True

def find_in_file(opts, in_file, regexp):
    """ display math """
    #print "find in file:", in_file
    in_fd = open(in_file, "r")
    in_lines = in_fd.readlines()
    in_fd.close()

    if opts.get("pyctx"):
        pyctx = PyContext(in_file)
    else:
        pyctx = Context(in_file)

    display_header = True
    for out_line, ln in zip(in_lines, range(len(in_lines))):
        pyctx.search(out_line)
        if re.search(regexp, out_line):
            if display_header:
                if not opts.get("quiet"):
                    print()
                    print(COLORS["bold"] + COLORS["light-blue"] + "file: " + os.path.relpath(in_file) + COLORS["clear"])
                display_header = False

            pyctx.display()
            out_line = out_line.rstrip()
            match    = re.search(regexp, out_line)
            out_line_color  = out_line[0:match.start()] + COLORS_REPLACE["word1"]
            out_line_color  = out_line_color + out_line[match.start():match.end()]
            out_line_color  = out_line_color + COLORS_REPLACE["line2"] + out_line[match.end():]
            print("%s%s: %s%s%s" % (COLORS_REPLACE["line2start"], ln, COLORS_REPLACE["line2"], out_line_color, COLORS["clear"]))

def replace_in_file(opts, in_file, regexp, repl):
    """
    Perfoms re.sub(regexp, repl, line) for each line in
    in_file
    """
    in_lines = []
    try:
        with open(in_file, "r") as in_fd:
            in_lines = in_fd.readlines()
    except:
        print("Cant open file: ", in_file)
        return

    out_lines = in_lines[:]
    out_lines = [re.sub(regexp, repl, l) for l in in_lines]

    diff = False

    # See if there's a diff first:
    for (in_line, out_line) in zip(in_lines, out_lines):
        if in_line != out_line:
            diff = True

    if not diff:
        return

    if not opts.get("quiet"):
        print(COLORS["bold"] + COLORS["light-blue"] + "patching: " + os.path.relpath(in_file) + COLORS["clear"])
    if opts.get("go"):
        if opts.get("backup"):
            rand_int = random.randint(100,999)
            back_file = "%s-%i.back" % (in_file, rand_int)
            back_file_fd = open(back_file, "w")
            back_file_fd.writelines(in_lines)
            back_file_fd.close()
        out_fd = open(in_file, "w")
        out_fd.writelines(out_lines)
        out_fd.close()

    if opts.get("quiet"):
        return

    for (in_line, out_line) in zip(in_lines, out_lines):
        if in_line != out_line:
            in_line  = in_line.strip()
            out_line = out_line.strip()
            match    = re.search(regexp, in_line)
            in_line_color  = in_line[0:match.start()] + COLORS_REPLACE["word1"]
            in_line_color  = in_line_color + in_line[match.start():match.end()]
            in_line_color  = in_line_color + COLORS_REPLACE["line1"] + in_line[match.end():]
            out_line_color = re.sub(regexp, COLORS_REPLACE["word2"] + repl + COLORS_REPLACE["line2"], in_line)

            print("%s--%s %s%s" % (COLORS_REPLACE["line1start"], COLORS_REPLACE["line1"], in_line_color, COLORS["clear"]))
            print("%s++%s %s%s" % (COLORS_REPLACE["line2start"], COLORS_REPLACE["line2"], out_line_color, COLORS["clear"]))
            print()

def find_main(opts, args):
    """ find main """
    if len(args) < 1:
        print("Wrong number of arguments")
        print(__usage__)
        sys.exit(2)

    pattern = args[0]
    regexp = re.compile(pattern)

    def find_action(f):
        return find_in_file(opts, f, regexp)

    if len(args) > 1:
        files = args[1:]
        for f in files:
            find_action(f)
    else:
        recurse_file(opts, os.getcwd(), find_action)



def repl_main(opts, args):
    """ replacer main """
    if len(args) < 2:
        print("Wrong number of arguments")
        print(__usage__)
        sys.exit(2)

    pattern = args[0]
    repl    = args[1]
    regexp  = re.compile(pattern)

    def repl_action(f):
        return replace_in_file(opts, f, regexp, repl)

    if len(args) > 2:
        files = args[2:]
        for f in files:
            repl_action(f)
    else:
        recurse_file(opts, os.getcwd(), repl_action)

    if not opts.get("go") and not opts.get("quiet"):
        print()
        print("To apply change, run again:")
        print("$ %s %s --go\n" % (os.path.basename(sys.argv[0]), ' '.join(sys.argv[1:])))
        print("To backup altered files, add '--backup' to the above command line.")
        print()


def main():
    """
    manages options when called from command line

    """
    option_parser = OptionParser(usage = __usage__)
    option_parser.add_option("--no-skip-hidden",
        action = "store_false", dest = "no_hidden",
        help = "Do not skip hidden files. Use this if you know what you are doing...")
    option_parser.add_option("--file-filter", dest = "file_filter", action = "append",
                             help = "File filter to apply (multiple filters can be specified)")
    option_parser.add_option("--no-filter", action = "store_true", dest = "no_filter",
                             help = "Do not skip files that match the filter")
    option_parser.add_option("-d", "--debug",
        action = "store_true", dest = "debug",
        help = "Enable debug output")
    option_parser.add_option("--backup",
        action = "store_true", dest = "backup",
        help = "Create a backup for each file. By default, files are modified in place")
    option_parser.add_option("--go",
        action = "store_true", dest = "go",
        help = "Perform changes rather than just printing then")
    option_parser.add_option("--find",
        action = "store_true", dest = "find",
        help = "Only search for match")
    option_parser.add_option("--dry-run", "-n",
        action = "store_false", dest = "go",
        help = "Do not change anything. This is the default")
    option_parser.add_option("--color",
        action = "store_false", dest = "color",
        help = "Colorize output. This is the default")
    option_parser.add_option("--no-color",
        action = "store_false", dest = "color",
        help = "Do not colorize output")
    option_parser.add_option("--quiet", "-q",
        action = "store_true", dest = "quiet",
        help = "Do not produce any output")
    option_parser.add_option("--no-py-ctx",
        action = "store_false", dest = "pyctx",
        help = "Do not use the python context")

    option_parser.set_defaults(
        no_hidden = True,
        no_filter = False,
        backup    = False,
        go        = False,
        color     = True,
        debug     = False,
        quiet     = False,
        pyctx     = True)

    (opts_obj, args) = option_parser.parse_args()

    opts = vars(opts_obj)

    if not opts.get("color") or not sys.stdout.isatty():
        for k in COLORS.iterkeys():
            COLORS[k] = ""
        for k in COLORS_REPLACE.iterkeys():
            COLORS_REPLACE[k] = ""

    if opts.get("debug"):
        logging.basicConfig(level=logging.DEBUG)

    if opts.get("find"):
        find_main(opts, args)
    else:
        repl_main(opts, args)



if __name__ == "__main__":
    main()
