#!/usr/bin/env python
from  __future__ import print_function
import sys
import subprocess
import shlex
import os.path
from itertools import islice,ifilter
from optparse import OptionParser
from collections import namedtuple

from cts.TagScanner import PartialTagScanner, IndexedTagScanner, ENTRY_FIELDS
import ctags

KINDS = {
    "c": "class name",
    "d": "define (from #define XXX)",
    "e": "enumerator",
    "f": "function or method name",
    "F": "file name",
    "g": "enumeration name",
    "m": "member (of structure or class data)",
    "p": "function prototype",
    "s": "structure name",
    "t": "typedef",
    "u": "union name",
    "v": "variable",
}
def build_arg_parser():
    usage = "usage: %prog [-flags] name"
    parser = OptionParser(usage=usage)
    parser.add_option("-p", "--partial", dest="partial", 
                      help="Substring match on a query.  This will perform a linear scan.",
                      action="store_true", default=False)

    parser.add_option("-r", "--prefix", dest="prefix", 
                      help="Does a prefix search on query.  This is logarithmic if case "\
                           "sensitive",
                      action="store_true", default=False)

    parser.add_option("-i", "--insensitive", dest="insensitive", 
                      help="Case insensitive search on ctag, linear scan.",
                      action="store_true", default=False)

    parser.add_option("-c", "--ctag_file", dest="tag_file",
                      help="If provided, specifies a CTAG file to use instead "\
                           "of recursing up from the current directory", default="")

    parser.add_option("-f", "--force", dest="force",
                      help="Forces cts to use a ctag file even if it looks invalid", 
                      action="store_true", default=False)

    parser.add_option("-k", "--restrict-kinds", dest="kinds",
                      help="Restricts tags to only show certain one or more tag types", 
                      default=None)

    parser.add_option("-l", "--list-kinds", dest="listkinds", 
                      help="Lists default descriptive ctags types.  This is not an " \
                           "exhaustive list",
                      action="store_true", default=False)

    parser.add_option("-t", "--file-types", dest="types", 
                      help="CSV list of file suffixes to include in search",
                      default=None)
    
    parser.add_option("-e", "--exec", dest="execute",
                      help="Executes a command using execv, filling in the "\
                           "appropriate variables.  Default opens in $EDITOR", 
                      default='%s +"{pattern}" {file}' % os.environ['EDITOR'])

    parser.add_option("-s", "--print-std", dest="stdout",
                      help="Instead of executing program, prints the formated string "\
                           "to stdout", default=None)

    parser.add_option("-n", "--num", dest="num", type="int",
                      help="Number of tags to show at a time.  Default is 5",
                      default=5)

    parser.add_option("-R", "--no-read", dest="readline", 
                      help="Does not attempt to read the real line if provided a line number",
                      action="store_false", default=True)

    return parser

class EntryFilter(object):
    kinds = None
    fileTypes = None
    def __init__(self, kinds, fileTypes):
        if kinds:
            self.add_kinds_filter(kinds)

        if fileTypes:
            self.add_file_types_filter(fileTypes)

    def add_kinds_filter(self, kinds):
        self.kinds = frozenset(kinds)

    def add_file_types_filter(self, fts):
        if isinstance(fts, basestring):
            fts = [f.strip() for f in fts.split(",")]

        self.fileTypes = set(fts)

    def ifilter(self, entry_set):
        # Filter out entries for files that no longer exist
        entries = ifilter(lambda e: os.path.isfile(e.file), entry_set)
        if self.kinds is not None:
            entry_set = ifilter(lambda e: e.kind in self.kinds, entry_set)

        if self.fileTypes is not None:
            sp = os.path.splitext
            filterfn = lambda e: sp(e.file)[1][1:] in self.fileTypes 
            entry_set = ifilter(filterfn, entry_set)

        return entry_set

def rec_dir_up(dir):
    if os.path.isdir(dir):
        while True:
            yield dir
            newdir = os.path.split(dir)[0]
            if newdir == dir: break
            dir = newdir

def find_ctag_file():
    directory = os.getcwd()
    for dir in rec_dir_up(directory):
        path = os.path.join(dir, "tags")
        if os.path.isfile(path):
            return path

    return None

class EntryFormatter(object):
    def format(self, entry):
        raise NotImplementedError()

class CompactEntryFormatter(object):
    
    def __init__(self, surrounding=True):
        self.detail_cache = {}
        self.surrounding = surrounding
        self.columns = self.get_columns()

    def format(self, i, entry):
        line_prefix = " %d:  %s" % (i, entry.name)
        filename = self.truncate_middle(entry.file, self.columns - len(line_prefix)-2)
        results = ["%s  %s" % (line_prefix, filename)]

        if self.surrounding:
            details = self.get_details(entry)
            if details is not None:
                idetails = self.indent(details, line_prefix.index(':')+5)
                results.append(idetails)

        return '\n'.join(results)


    def read_line_at(self, f, num):
        return next(islice(f, num - 1, num), None)

    def get_details(self, entry):
        # If a file, print nothing
        if entry.kind == 'F':
            return None

        # If -R or not a real line
        if entry.lineNumber == 0:
            return entry.pattern

        if entry not in self.detail_cache:
            lineNum = entry.lineNumber
            with file(entry.file) as f:
                response = self.read_line_at(f, lineNum)
                if response is None:
                    response = entry.pattern
                self.detail_cache[entry] = response.strip()
        
        return self.detail_cache[entry]

    def truncate_middle(self, line, length=70):
        reduce_amt = len(line) - length

        # If it already fits
        if reduce_amt <= 0 or length <= 0:
            return filename

        reduce_amt += 3 # for the ellipsis

        start = (len(line) / 2) - (reduce_amt / 2)
        end = start + reduce_amt
        return "%s...%s" % (line[:start], line[end:])

    def indent(self, s, indent):
        return s.rjust(len(s) + indent)

    
    def get_columns(self):
        # Attempt to get the columns of the terminal session
        try:
            return int(subprocess.check_output(['tput', 'cols']).strip())
        except OSError:
            return float('inf')
        except ValueError:
            # tput doesn't return an int?  
            return float('inf')

class Paginator(object):
    curPage = 0
    def __init__(self, entries, pageSize):
        self.entries = list(entries)
        self.pageSize = pageSize

    def __len__(self):
        return len(self.entries)

    def __iter__(self):
        for i in xrange(self.getStartIndex(), self.getEndIndex()):
            yield (i, self.entries[i])

    def __getitem__(self, i):
        return self.entries[i]

    def getStartIndex(self):
        return self.curPage * self.pageSize

    def getEndIndex(self):
        return min(self.getStartIndex() + self.pageSize, len(self.entries))

    def getCurrentPage(self):
        return self.entries[self.getStartIndex():self.getEndIndex()]

    def hasNextPage(self):
        return (self.pageSize * (self.curPage + 1)) <= len(self.entries)

    def nextPage(self):
        self.curPage += 1

    def hasPrevPage(self):
        return self.curPage > 0

    def prevPage(self):
        self.curPage -= 1

def select_entry(paginator, formatter):

    while True:
        print_set = []
        for i, entry in paginator:
            print_set.append(formatter.format(i, entry))

        instructions = ["Select #"]
        if paginator.hasNextPage():
            instructions.append("'m' for more")
        if paginator.hasPrevPage():
            instructions.append("'p' for prev")

        print('\n\n'.join(print_set))
        print()
        print("%d - %d of %d entries" % (paginator.getStartIndex(), 
                                         paginator.getEndIndex(),
                                         len(paginator)))
        print(', '.join(instructions))

        # Get the input
        results = raw_input("> ")
        if results.isdigit():
            linenum = int(results)
            if linenum < len(paginator):
                return paginator[linenum]

        elif results == 'm' and paginator.hasNextPage():
            paginator.nextPage()
        elif results == 'p' and paginator.hasPrevPage():
            paginator.prevPage()

def validate(tag_file):
    # we need attempt to verify that the file is actually a ctags file
    # since 'python-ctags' segfaults if it isn't
    valid = 1
    with file(tag_file) as f:
        valid &= "_TAG_FILE_FORMAT" in f.readline()
        valid &= "_TAG_FILE_SORTED" in f.readline()

    return valid

if __name__ == '__main__':
    parser = build_arg_parser()
    opts, args = parser.parse_args()

    if opts.listkinds:
        print("Tag Kinds")
        for k in sorted(KINDS.iterkeys()):
            print("%s\t%s" % (k, KINDS[k]))

        sys.exit(0)

    if not args:
        print("Missing search string")
        parser.print_usage()
        sys.exit(1)

    # find the nearest ctag file
    tag_file = opts.tag_file or find_ctag_file()
    if tag_file is None or not os.path.isfile(tag_file):
        print("Could not find tag_file")
        sys.exit(1)
    
    if not validate(tag_file) and not options.force:
        print("file %s Does not look like a ctag file.  Aborting." % tag_file)
        sys.exit(1)

    query = args[0]
    tags = ctags.CTags(tag_file)
    baseDir = os.path.split(tag_file)[0]

    if opts.partial:
        ts = PartialTagScanner(tags, baseDir, opts.insensitive)
    else:
        ts = IndexedTagScanner(tags, baseDir, opts.prefix, opts.insensitive)

    # filter results
    entryFilter = EntryFilter(opts.kinds, opts.types)
    results = list(entryFilter.ifilter(ts.query(query)))
    
    if not results:
        print("No tags for `%s` found." % query)
        sys.exit(0)
    
    entry = None
    if len(results) == 1:
        entry = results[0]
    else:
        try:
            formatter = CompactEntryFormatter(opts.readline)
            paginator = Paginator(results, opts.num)
            entry = select_entry(paginator, formatter)
        except (KeyboardInterrupt,EOFError),e:
            sys.exit(1)

    # Prepare execution of script
    variables = dict((k,getattr(entry,k)) for k in ENTRY_FIELDS)

    # Override file for absolute path file
    tag_dir = os.path.split(tag_file)[0]
    filename = os.path.join(tag_dir, entry.file)
    variables['file'] = filename 
    if opts.stdout is not None:
        print(opts.stdout.format(**variables))
        sys.exit(0)

    pieces = shlex.split(opts.execute.format(**variables))
    if pieces:
        os.execvp(pieces[0], pieces)

