#!/usr/bin/env python
""" grin searches text files.
"""

from __future__ import print_function

import argparse
import bisect
import fnmatch
import gzip
import io
import itertools
import os
import re
import shlex
import stat
import sys

import colorama
from six import b, int2byte, reraise
from six.moves import map, filter


def int2bytes(ints):
    return b('').join(map(int2byte, ints))


#### Constants ####
__version__ = '1.2.1+xy2'

# Maintain the numerical order of these constants. We use them for sorting.
PRE = -1
MATCH = 0
POST = 1

# Use file(1)'s choices for what's text and what's not.
TEXTCHARS = int2bytes((7, 8, 9, 10, 12, 13, 27) + tuple(range(0x20, 0x100)))
ALLBYTES = int2bytes(range(256))

COLOR_TABLE = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan',
               'white', 'default']
COLOR_STYLE = {
    'filename': dict(fg="green", bold=True),
    'searchterm': dict(fg="black", bg="yellow")
}

# gzip magic header bytes.
GZIP_MAGIC = b('\037\213')

# Target amount of data to read into memory at a time.
READ_BLOCKSIZE = 16 * 1024 * 1024


def is_binary_string(b):
    """Determine if a string is classified as binary rather than text.

    Parameters
    ----------
    bytes : str

    Returns
    -------
    is_binary : bool
    """
    return bool(b.translate(ALLBYTES, TEXTCHARS))


def get_line_offsets(block):
    """Compute the list of offsets in DataBlock 'block' which correspond to
    the beginnings of new lines.

    Parameters
    ----------
    block : DataBlock

    Returns
    -------
    ret : (offset list, count of lines in "current block")
    """
    # Note: this implementation based on string.find() benchmarks about twice
    # as fast as a list comprehension using re.finditer().
    line_offsets = [0]
    line_count = 0  # Count of lines inside range [block.start, block.end) *only*
    s = block.data
    while True:
        next_newline = s.find('\n', line_offsets[-1])
        if next_newline < 0:
            # Tack on a final "line start" corresponding to EOF, if not done
            # already.  This makes it possible to determine the length of each
            # line by computing a difference between successive elements.
            if line_offsets[-1] < len(s):
                line_offsets.append(len(s))
            return line_offsets, line_count
        else:
            line_offsets.append(next_newline + 1)
            # Keep track of the count of lines within the "current block"
            if next_newline >= block.start and next_newline < block.end:
                line_count += 1


def colorize(s, fg=None, bg=None, bold=False, underline=False, reverse=False):
    """Wraps a string with ANSI color escape sequences corresponding to the
    style parameters given.

    All of the color and style parameters are optional.

    Parameters
    ----------
    s : str
    fg : str
        Foreground color of the text.  One of (black, red, green, yellow, blue,
        magenta, cyan, white, default)
    bg : str
        Background color of the text.  Color choices are the same as for fg.
    bold : bool
        Whether or not to display the text in bold.
    underline : bool
        Whether or not to underline the text.
    reverse : bool
        Whether or not to show the text in reverse video.

    Returns
    -------
    A string with embedded color escape sequences.
    """

    style_fragments = []
    if fg in COLOR_TABLE:
        # Foreground colors go from 30-39
        style_fragments.append(COLOR_TABLE.index(fg) + 30)
    if bg in COLOR_TABLE:
        # Background colors go from 40-49
        style_fragments.append(COLOR_TABLE.index(bg) + 40)
    if bold:
        style_fragments.append(1)
    if underline:
        style_fragments.append(4)
    if reverse:
        style_fragments.append(7)
    style_start = '\x1b[' + ';'.join(map(str, style_fragments)) + 'm'
    style_end = '\x1b[0m'
    return style_start + s + style_end


class Options(dict):
    """Simple options."""
    def __init__(self, *args, **kwds):
        super(Options, self).__init__(*args, **kwds)
        self.__dict__ = self


def default_options():
    """Populate the default options."""
    opt = Options(
        before_context=0,
        after_context=0,
        show_line_numbers=True,
        show_match=True,
        show_filename=True,
        show_emacs=False,
        skip_hidden_dirs=False,
        skip_hidden_files=False,