#!/usr/bin/python3.5

"""
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
    -b, --binary        do not skip binary files or those not cannot be decode as utf-8
    -h, --help          show help message and exit

Vim will open the first file that contains the pattern and place the cursor on
the first occurrence. Use n to go to the next occurrence and ctrl-n to go to the
next file. Autowrite is set by default, so any changes you make are
automatically written out before moving to the next file. Run 'man fvi' for more
information.
"""
__version__ = '1.2.0'

# 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, cull, display, fatal, os_error, terminate, warn
    )
    from shlib import Run
    import re
    import codecs
    import os

    # Globals {{{1
    VIM = os.environ.get('VIM', 'vim')
    GVIM = os.environ.get('GVIM', 'gvim')
    GREP = os.environ.get('GREP', 'grep')
    ACK = os.environ.get('ACK', 'ack')

    # Eliminate duplicate files {{{1
    def eliminate_duplicates(files):
        seen = set()
        todo = []
        ignore = []
        for fn in files:
            try:
                nfo = os.stat(fn)
                if nfo.st_ino in seen:
                    ignore.append(fn)
                else:
                    seen.add(nfo.st_ino)
                    todo.append(fn)
            except OSError as err:
                warn(os_error(err), 'Skipping ...')
                ignore.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['<file>']:
            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))
                begin = max(err.start-25, 0)
                codicil('    ', err.object[begin:err.end+25])
                codicil('    ', (2+err.start-begin)*' ' + (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("%s does not support the magic flags, ignored.", ACK)
        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.strip().split('\n')

        except OSError as err:
            fatal(os_error(err))

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

    # Edit the files {{{1
    files = eliminate_duplicates(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:
    display('Killed by user.')
    terminate(0)
except OSError as err:
    fatal(os_error(err))

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