#!/usr/bin/env python2
# This file is part of GUFI, which is part of MarFS, which is released
# under the BSD license.


# Copyright (c) 2017, Los Alamos National Security (LANS), LLC
# All rights reserved.

# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:

# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.

# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.

# 3. Neither the name of the copyright holder nor the names of its contributors
# may be used to endorse or promote products derived from this software without
# specific prior written permission.

# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# -----
# NOTE:
# -----

# GUFI uses the C-Thread-Pool library.  The original version, written by
# Johan Hanssen Seferidis, is found at
# https://github.com/Pithikos/C-Thread-Pool/blob/master/LICENSE, and is
# released under the MIT License.  LANS, LLC added functionality to the
# original work.  The original work, plus LANS, LLC added functionality is
# found at https://github.com/jti-lanl/C-Thread-Pool, also under the MIT
# License.  The MIT License can be found at
# https://opensource.org/licenses/MIT.


# From Los Alamos National Security, LLC:
# LA-CC-15-039

# Copyright (c) 2017, Los Alamos National Security, LLC All rights reserved.
# Copyright 2017. Los Alamos National Security, LLC. This software was produced
# under U.S. Government contract DE-AC52-06NA25396 for Los Alamos National
# Laboratory (LANL), which is operated by Los Alamos National Security, LLC for
# the U.S. Department of Energy. The U.S. Government has rights to use,
# reproduce, and distribute this software.  NEITHER THE GOVERNMENT NOR LOS
# ALAMOS NATIONAL SECURITY, LLC MAKES ANY WARRANTY, EXPRESS OR IMPLIED, OR
# ASSUMES ANY LIABILITY FOR THE USE OF THIS SOFTWARE.  If software is
# modified to produce derivative works, such modified software should be
# clearly marked, so as not to confuse it with the version available from
# LANL.

# THIS SOFTWARE IS PROVIDED BY LOS ALAMOS NATIONAL SECURITY, LLC AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL LOS ALAMOS NATIONAL SECURITY, LLC OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
# OF SUCH DAMAGE.



import argparse
import os
import re
import subprocess
import sys
import time

import query_builder as qb

SECONDS_PER_DAY = str(24 * 60 * 60)

'''file size units specified in GNU find.'''
filesize = {'b' : 512,
            'c' : 1,
            'w' : 2,
            'k' : 1024,
            'M' : 1048576,
            'G' : 1048576}

def build_select(args):
    '''Build the SELECT portion of the query.'''
    if args.select:
        return [args.select]
    return None

def build_where(args):
    '''Build the WHERE portion of the query.'''
    where = []

    if args.amin is not None:
        where += ['({} - atime) / 60 == {}'.format(time.time(), args.amin)]

    # if args.anewer is not None:

    if args.atime is not None:
        where += ['({} - atime) / {} == {} '.format(time.time(), SECONDS_PER_DAY, args.atime)]

    if args.cmin is not None:
        where += ['({} - ctime) / 60 == {}'.format(time.time(), args.cmin)]

    # if args.cnewer is not None:

    if args.ctime is not None:
        where += ['({} - ctime) / {} == {} '.format(time.time(), SECONDS_PER_DAY, args.ctime)]

    if args.empty == True:
        where += ['size == 0']

    # if args.executable:

    # if args.false is not None:

    if args.gid is not None:
        where += ['gid == {}'.format(args.gid)]

    if args.group is not None:
        where += ['gid == {}'.format(args.group)]

    # if args.ilname is not None:

    # if args.iname is not None:

    if args.inum is not None:
        where += ['inode == {}'.format(args.inum)]

    # if args.ipath is not None:

    # if args.iregex is not None:

    # if args.iwholename is not None:

    if args.links is not None:
        where += ['nlinks == {}'.format(args.links)]

    # if args.lname is not None:

    if args.mmin is not None:
        where += ['({} - mtime) / 60 == {}'.format(time.time(), args.mmin)]

    if args.mtime is not None:
        where += ['({} - mtime) / {} == {} '.format(time.time(), SECONDS_PER_DAY, args.mtime)]

    if args.name is not None:
        where += ['name REGEXP "{}"'.format(args.name)]

    # if args.newer is not None:

    # if args.newerXY is not None:

    # if args.nouser is not None:

    # if args.path is not None:
    #     raise Exception("Need to modify GUFI to store/access path names when selecting file names")
    #     # where += ['name REGEXP "{}"'.format(args.regex)]

    # if args.readable is not None:

    # if args.regex is not None:
    #     raise Exception("Need to modify GUFI to store/access path names when selecting file names")
    #     # where += ['name REGEXP "{}"'.format(args.regex)]

    # if args.samefile is not None:

    if args.size is not None:
        unit = 'b'
        size = args.size
        if args.size[-1] in 'bcwkMG':
            unit = args.size[-1]
            size = args.size[:-1]

        comp = '=='
        if size[0] == '+':
            comp = '>'
        elif size[0] == '-':
            comp = '<'

        where += ['size {} {}'.format(comp, int(size[1:]) * filesize[unit])]

    # if args.true:

    if args.type is not None:
        where += ['type == "{}"'.format(args.type)]

    if args.uid is not None:
        where += ['uid == {}'.format(args.uid)]

    # if args.used is not None:

    if args.user is not None:
        where += ['uid == {}'.format(args.user)]

    # if args.wholename is not None:

    # if args.writable is not None:

    # if args.xtype is not None:

    # if args.context is not None:

    return where

def build_group_by(args):
    '''Build the GROUP BY portion of the query.'''
    if args.group_by:
        return [args.group_by]
    return None

def build_order_by(args):
    '''Build the ORDER BY portion of the query.'''
    order_by = []

    if args.smallest:
        order_by += ['size ASC']

    if args.largest:
        order_by += ['size DESC']

    return order_by

def check_size(value):
    '''Make sure the value matches [+/-]n[bcwkMG].'''
    if not re.match('^[+-]?[0-9]+[bcwkMG]?$', value):
        raise argparse.ArgumentTypeError("%s is not a valid size" % value)

    return value

if __name__=='__main__':
    cwd = os.getcwd()

    parser = argparse.ArgumentParser(description='GUFI version of find')
    parser.add_argument('-amin',               metavar='n',         dest='amin',          type=qb.check_non_negative,                  help='File was last accessed n minutes ago.')
    # parser.add_argument('-anewer',             metavar='file',      dest='anewer',        type=qb.check_non_negative,                  help='File was last accessed more recently than file was modified.')
    parser.add_argument('-atime',              metavar='n',         dest='atime',         type=qb.check_non_negative,                  help='File was last accessed n*24 hours ago.')
    parser.add_argument('-cmin',               metavar='n',         dest='cmin',          type=qb.check_non_negative,                  help='File\'s status was last changed n minutes ago.')
    # parser.add_argument('-cnewer',             metavar='file',      dest='cnewer',        type=qb.check_non_negative,                  help='File\'s status was last changed more recently than file was modified.')
    parser.add_argument('-ctime',              metavar='n',         dest='ctime',         type=qb.check_non_negative,                  help='File\'s status was last changed n*24 hours ago.')
    parser.add_argument('-empty',                                   dest='empty',         action='store_true',                         help='File is empty and is either a regular file or a directory.')
    # parser.add_argument('-executable',                              dest='executable',    action='store_true',                         help='Matches files which are executable and directories which are searchable (in a file name resolution sense).')
    # parser.add_argument('-false',                                   dest='false',         action='store_true',                         help='File is false and is either a regular file or a directory.')
    # parser.add_argument('-fstype',             metavar='n',         dest='fstype',        type=str,                                    help='File is on a filesystem of type type.')
    parser.add_argument('-gid',                metavar='n',         dest='gid',           type=qb.check_non_negative,                  help='File\'s numeric group ID is n.')
    parser.add_argument('-group',              metavar='gname',     dest='group',         type=qb.get_gid,                             help='File belongs to group gname (numeric group ID allowed).')
    # parser.add_argument('-ilname',             metavar='pattern',   dest='ilname',        type=str,                                    help='Like -lname, but the match is case insensitive.')
    # parser.add_argument('-iname',              metavar='pattern',   dest='iname',         type=str,                                    help='Like -name, but the match is case insensitive.')
    parser.add_argument('-inum',               metavar='n',         dest='inum',          type=qb.check_non_negative,                  help='File has inode number n. It is normally easier to use the -samefile test instead.')
    # parser.add_argument('-ipath',              metavar='pattern',   dest='ipath',         type=str,                                    help='Behaves in the same way as -iwholename. This option is deprecated, so please do not use it.')
    # parser.add_argument('-iregex',             metavar='pattern',   dest='iregex',        type=str,                                    help='Like -regex, but the match is case insensitive.')
    # parser.add_argument('-iwholename',         metavar='pattern',   dest='iwholename',    type=str,                                    help='Like -wholename, but the match is case insensitive.')
    parser.add_argument('-links',              metavar='n',         dest='links',         type=qb.check_non_negative,                  help='File has n links.')
    # parser.add_argument('-lname',              metavar='pattern',   dest='lname' ,        type=str,                                    help='File is a symbolic link whose contents match shell pattern pattern.')
    parser.add_argument('-mmin',               metavar='n',         dest='mmin',          type=qb.check_non_negative,                  help='File\'s data was last modified n minutes ago.')
    parser.add_argument('-mtime',              metavar='n',         dest='mtime',         type=qb.check_non_negative,                  help='File\'s data was last modified n*24 hours ago.')
    parser.add_argument('-name',               metavar='pattern',   dest='name',          type=str,                                    help='Base of file name (the path with the leading directories removed) matches shell pattern pattern.')
    # parser.add_argument('-newer',              metavar='file',      dest='newer',         type=str,                                    help='File was modified more recently than file.')
    # parser.add_argument('-newerXY',            metavar='reference', dest='newerXY',       type=str,                                    help='Compares the timestamp of the current file with reference.')
    # parser.add_argument('-nouser',                                  dest='nouser',        type=qb.check_bool,                          help='No user corresponds to file\'s numeric user ID.')
    # parser.add_argument('-path',               metavar='pattern',   dest='path',          type=str,                                    help='File name matches shell pattern pattern.')
    # parser.add_argument('-readable',                                dest='readable',      type=qb.check_bool,                          help='Matches files which are readable.')
    # parser.add_argument('-regex',              metavar='pattern',   dest='regex',         type=str,                                    help='File name matches regular expression pattern.')
    # parser.add_argument('-samefile',           metavar='name',      dest='samefile',      type=qb.check_bool,                          help='File refers to the same inode as name.')
    parser.add_argument('-size',               metavar='n',         dest='size',          type=check_size,                             help='')
    # parser.add_argument('-true',                                    dest='true',          type=qb.check_bool,                          help='Always true/')
    parser.add_argument('-type',               metavar='c',         dest='type',          type=qb.check_char,                          help='File is of type c')
    parser.add_argument('-uid',                metavar='n',         dest='uid',           type=qb.check_non_negative,                  help='File\'s numeric user ID is n.')
    # parser.add_argument('-used',               metavar='n',         dest='used',          type=qb.check_non_negative,                  help='File was last accessed n days after its status was last changed.')
    parser.add_argument('-user',               metavar='uname',     dest='user',          type=qb.get_uid,                             help='File is owned by user uname (numeric user ID allowed).')
    # parser.add_argument('-wholename',          metavar='pattern',   dest='wholename',     type=str,                                    help='See -path. This alternative is less portable than -path.')
    # parser.add_argument('-writable',                                dest='writable',      type=qb.check_bool,                          help='Matches files which are writable.')
    # parser.add_argument('-xtype',              metavar='c',         dest='xtype',         type=qb.check_char,                          help='The same as -type unless the file is a symbolic link.')
    # parser.add_argument('-context',            metavar='pattern',   dest='context',       type=str,                                    help='(SELinux only) Security context of the file matches glob pattern.')

    parser.add_argument('-maxdepth',           metavar='levels',    dest='maxdepth',      type=qb.check_non_negative,                  help='Descend at most levels (a non-negative integer) levels of directories below the command line arguments. -maxdepth 0 means only apply the tests and actions to the command line arguments.')
    parser.add_argument('-mindepth',           metavar='levels',    dest='mindepth',      type=qb.check_non_negative,                  help='Do not apply any tests or actions at levels less than levels (a non-negative integer). -mindepth 1 means process all files except the command line arguments.')

    # GUFI specific arguments
    parser.add_argument('--root',              metavar='path',      dest='root',          type=str,               default=cwd,         help='top-most directory to search from (default: ' + cwd + ')')
    parser.add_argument('--select',            metavar='cols',      dest='select',        type=str,               default='*',         help='comma separated columns (default: *)')
    parser.add_argument('--group_by',          metavar='cols',      dest='group_by',      type=str,                                    help='comma separated columns (default: None)')
    parser.add_argument('--delim',             metavar='c',         dest='delim',         type=qb.check_char,                          help='delimiter separating output columns')
    parser.add_argument('--num_results',       metavar='n',         dest='num_results',   type=qb.check_non_negative,                  help='first n results')
    parser.add_argument('--smallest',                               dest='smallest',      action='store_true',                         help='top n smallest files')
    parser.add_argument('--largest',                                dest='largest',       action='store_true',                         help='top n largest files')
    parser.add_argument('--threads', '-t',     metavar='n',         dest='threads',       type=qb.check_positive, default=qb.cpus(),   help='number of threads')
    parser.add_argument('--intermediate_dbs',  metavar='n',         dest='intermediates', type=qb.check_non_negative,                  help='number of intermediate databases')
    parser.add_argument('--intermediate_skip', metavar='n',         dest='skip',          type=qb.check_non_negative,                  help='number of intermediate databases to skip when selecting the next database')
    parser.add_argument('--no-aggregate',                           dest='no_aggregate',  action='store_true',                         help='do not aggregate results before printing')
    parser.add_argument('GUFI_tree',                                                                              nargs='+',           help='path containing GUFI tree database file')

    args = parser.parse_args();

    # create the find command
    find = qb.find_dir(args.root, [os.path.normpath(path) for path in args.GUFI_tree])
    find_out, find_err = find.communicate()

    # check for data in stdout, not the return code of find
    if len(find_out) == 0:
        # if there are errors, print them
        if len(find_err):
            sys.stderr.write('Error: find returned: {}\n{}\n'.format(os.strerror(find.returncode), find_err))
        # otherwise, nothing was found
        else:
            sys.stderr.write('Error: No directories found\n')
        sys.exit(find.returncode)
    # if there is data and errors, ignore the errors

    initial = qb.build_query(build_select(args),
                             ['entries'],
                             build_where(args),
                             build_group_by(args),
                             build_order_by(args),
                             args.num_results)

    intermediate = final = qb.build_query(['*'],
                                          ['entries'],
                                          build_where(args),
                                          build_group_by(args),
                                          build_order_by(args),
                                          args.num_results)

    # create the bfq command
    bfq_cmd = [os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bfq'),
               '-p', '-P',
               '-e', str(int(args.no_aggregate)),
               '-n', str(args.threads),
               '-E', initial,
               '-J', intermediate,
               '-G', final] + find_out.split()

    if args.delim:
        bfq_cmd += ['-d', args.delim]

    if args.intermediates:
        bfq_cmd += ['-v', str(args.intermediates)]

    if args.skip:
        bfq_cmd += ['-w', str(args.skip)]

    if args.maxdepth:
        bfq_cmd += ['-z', str(args.maxdepth)]

    if args.mindepth:
        bfq_cmd += ['-y', str(args.mindepth)]

    bfq = subprocess.Popen(bfq_cmd)
    bfq.communicate() # block until bfq finishes

    sys.exit(bfq.returncode)
