#!/usr/bin/env python

"""
fvi

Searches through a list of files to find a string, and then sequentially opens
each file that contains that string in vim.

Usage:
    fvi [options] <pattern> [<file>...]

Options:
    -w, --word          match a word
    -i, --ignore-case   ignore case
    -m, --magic         treat a pattern as a vim magic or grep basic regular 
                        expression
    -v, --very-magic    treat a pattern as a vim very magic or grep extended 
                        regular expression
    -g, --gvim          open files in gvim rather than vim
    -h, --help          show help message and exit
"""
__version__ = '1.1.1'

# License {{{1
# Copyright (C) 2013-16 Kenneth S. Kundert
#
# 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, either version 3 of the License, or (at your option) any later
# version.
#
# 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.  If not, see http://www.gnu.org/licenses/.


# Imports {{{1
try:
    from docopt import docopt
    from inform import Inform, codicil, display, fatal, os_error, terminate, warn
    from shlib import Run
    import re
    import codecs
    import os

    # Eliminate duplicate files {{{1
    def eliminateDuplicateFiles(files):
        seen = set()
        todo = []
        ignore = []
        for fn in files:
            nfo = os.stat(fn)
            if nfo.st_ino in seen:
                ignore.append(fn)
            else:
                seen.add(nfo.st_ino)
                todo.append(fn)
        if ignore:
            display('Ignoring duplicate files:\n    %s' %  '\n    '.join(ignore))
        return todo

# Read the command line {{{1
    cmdline = docopt(__doc__)

    # Initialization {{{1
    vim_flags = ['aw', 'nofen']  # enable autowrite and disable folds in vim
    grep_flags = ['--files-with-matches']
    use_grep = False
    re_flags = 0
    cmd = None

    # Process the command line {{{1
    pattern = cmdline['<pattern>']
    re_pattern = re.escape(pattern)
    if cmdline['--word']:
        grep_flags += ['--word-regexp']
        vim_pattern_prefix = r'\<'
        vim_pattern_suffix = r'\m\>'
        re_pattern = r'\b' + pattern + r'\b'
    else:
        vim_pattern_prefix = ''
        vim_pattern_suffix = ''
    if cmdline['--ignore-case']:
        grep_flags += ['--ignore-case']
        re_flags += re.IGNORECASE
        vim_pattern_prefix += r'\c'
    if cmdline['--very-magic']:
        use_grep = True
        grep_flags += ['--extended-regexp']
        vim_pattern_prefix += r'\v'
    elif cmdline['--magic']:
        use_grep = True
        grep_flags += ['--basic-regexp']
        vim_pattern_prefix += r'\m'
    else:
        vim_pattern_prefix += '\V'
    editor = 'gvim' if cmdline['--gvim'] else 'vim'

    # Find files to edit {{{1
    if cmdline['<file>']:
        # User has given a list of files.
        # Try to open each and eliminate if it is undesirable (is a directory,
        # is unreadable, or is a binary file).
        # Then, if doing a non-magic search, also filter out any files that do
        # not contain the search string.
        # If doing a magic search, build the command line for grep that will be
        # used to filter out files that do not contain the search pattern.
        files = []
        regex = re.compile(re_pattern, re_flags)
        for each in cmdline['<files>']:
            try:
                eh = 'ignore' if cmdline.binary else 'strict'
                with codecs.open(each, 'r', 'utf-8', eh) as f:
                    contents = f.read()
                    if use_grep or regex.search(contents, re_flags):
                        files += [each]
            except OSError as err:
                warn(os_error(err), 'skipping ...')
            except UnicodeDecodeError as err:
                warn("is a binary file, skipping ...", culprit=each)
                codicil(str(err))
                codicil('    ', err.object[err.start-25:err.end+25])
                codicil('    ', 27*' ' + (err.end-err.start)*'^')
        if use_grep:
            cmd = ['grep'] + grep_flags + ['--regexp', pattern] + files
    else:
        # The user gave us no files to search, so use ack to find them
        ack_flags = [
            each
            for each in grep_flags
            if each not in ['--basic-regexp', '--extended-regexp']
        ]
        if set(ack_flags) != set(grep_flags):
            warn("ack does not support the magic flags, ignored.")
        cmd = ['ack', '--follow'] + ack_flags + [pattern]

    # Run either grep to filter out any files that do not contain the search
    # pattern or ack to find any files that contain the pattern.
    if cmd:
        try:
            process = Run(cmd, modes='sOeW1')
            files = process.stdout.split()
        except OSError as err:
            fatal(os_error(err))

    # Exit if there are no files {{{1
    if not files:
        display('None of the files searched contain the pattern.')

    # Edit the files {{{1
    files = eliminateDuplicateFiles(files)
    vim_options = 'set %s' % ' '.join(vim_flags)
    # Configure ctrl-N to move to first occurrence of search string in next file   
    # while suppressing the annoying 'press enter' message and echoing the
    # name of the new file so you know where you are.
    next_file_map = 'map <C-N> :silent next +//<CR> :file<CR>'
    search_pattern = 'silent /%s' % (
        vim_pattern_prefix + pattern + vim_pattern_suffix
    )
    cmd = (
        [editor]
        + ['+%s' % '|'.join([vim_options, next_file_map, search_pattern])]
        + files
    )
    vim = Run(cmd, modes='soeW')
    terminate(vim.status)

except KeyboardInterrupt:
    terminate('Killed by user')

# vim: set sw=4 sts=4 et:
