#!/usr/bin/env python
 
'''  flashbake - wrapper script that will get installed by setup.py into the execution path '''
 
#    copyright 2009 Thomas Gideon
#
#    This file is part of flashbake.
#
#    flashbake 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.
#
#    flashbake 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 flashbake.  If not, see <http://www.gnu.org/licenses/>.
 
 
from flashbake import commit, context, control
from flashbake.plugins import PluginError, PLUGIN_ERRORS
from optparse import OptionParser
from os.path import join, realpath
import flashbake.git
import fnmatch
import logging
import os.path
import sys
 
VERSION = flashbake.__version__
pattern = '.flashbake'
 
 
def main():
    ''' Entry point used by the setup.py installation script. '''
    # handle options and arguments
    parser = _build_main_parser()
 
    (options, args) = parser.parse_args()
 
    if options.quiet and options.verbose:
        parser.error('Cannot specify both verbose and quiet')
 
    # configure logging
    level = logging.INFO
    if options.verbose:
        level = logging.DEBUG
 
    if options.quiet:
        level = logging.ERROR
 
    logging.basicConfig(level=level,
            format='%(message)s')
 
    home_dir = os.path.expanduser('~')
 
    # look for plugin directory
    _load_plugin_dirs(options, home_dir)
 
    if len(args) < 1:
        parser.error('Must specify project directory.')
        sys.exit(1)
 
    project_dir = args[0]
 
    # look for user's default control file
    hot_files, control_config = _load_user_control(home_dir, project_dir, options)
 
    # look for project control file
    control_file = _find_control(parser, project_dir)
    if None == control_file:
        sys.exit(1)
 
    # emit the context message and exit
    if options.context_only:
        sys.exit(_context_only(options, project_dir, control_file, control_config, hot_files))
 
    quiet_period = 0
    if len(args) == 2:
        try:
            quiet_period = int(args[1])
        except:
            parser.error('Quiet minutes, "%s", must be a valid number.' % args[1])
            sys.exit(1)
    try:
        (hot_files, control_config) = control.parse_control(project_dir, control_file, control_config, hot_files)
        control_config.context_only = options.context_only
        control_config.dry_run = options.dryrun
        if (options.dryrun):
            logging.info('========================================')
            logging.info('!!! Running in dry run mode.         !!!')
            logging.info('!!! No changes will be committed.    !!!')
            logging.info('========================================\n\n')
        (hot_files, control_config) = control.prepare_control(hot_files, control_config)
        if options.purge:
            commit.purge(control_config, hot_files)
        else:
            commit.commit(control_config, hot_files, quiet_period)
        if (options.dryrun):
            logging.info('\n\n========================================')
            logging.info('!!! Running in dry run mode.         !!!')
            logging.info('!!! No changes will be committed.    !!!')
            logging.info('========================================')
    except (flashbake.git.VCError, flashbake.ConfigError), error:
        logging.error('Error: %s' % str(error))
        sys.exit(1)
    except PluginError, error:
        _handle_bad_plugin(error)
        sys.exit(1)
 
 
def multiple_projects():
    parser = _build_multi_parser()
    (options, args) = parser.parse_args()
    if len(args) < 1:
        parser.error('Must specify root search directory.')
        sys.exit(1)
 
    flashbake_opts = options.flashbake_options.split()
 
    # verify --options will pass to main flashbake program
    test_argv = sys.argv[0:1] + flashbake_opts + ['.'] + args[1:]
    main_parser = _build_main_parser()
    main_parser.suppress_exit = True
 
    try:
        (test_options, test_args) = main_parser.parse_args(test_argv)
    except ParserError, err:
        msg = "error with arguments passed to main flashbake: %s\n%s" % (
            "'" + "' '".join(
                flashbake_opts + ['<project_dir>'] + args[1:]) + "'",
            err.msg.replace(parser.get_prog_name() + ':', '> '))
        parser.exit(err.code, msg)
    exit_code = 0
    for project in _locate_projects(args[0]):
        print "project: %s" % project
        sys.argv = sys.argv[0:1] + flashbake_opts + [project] + args[1:]
        try:
            main()
        except SystemExit, err:
            if err.code != 0:
                exit_code = err.code
            logging.error("Error: 'flashbake' had an error for '%s'"
                          % project)
    sys.exit(exit_code)
 
 
def _locate_projects(root):
    for path, dirs, files in os.walk(root): #@UnusedVariable
        for project_path in (
            os.path.normpath(path) for filename in files \
                if fnmatch.fnmatch(filename, pattern)):
            yield project_path
 
 
class ParserError(RuntimeError):
 
    def __init__(self, code=0, msg=''):
        RuntimeError.__init__(self, code, msg)
 
    def _get_msg(self):
        return self.args[1]
 
    def _get_code(self):
        return self.args[0]
 
    msg = property(_get_msg)
    code = property(_get_code)
 
 
class FlashbakeOptionParser(OptionParser):
 
    def __init__(self, *args, **kwargs):
        OptionParser.__init__(self, *args, **kwargs)
        self.suppress_exit = False
 
    def print_usage(self, file=None):
        if not self.suppress_exit:
            OptionParser.print_usage(self, file)
 
    def exit(self, status=0, msg=None):
        if self.suppress_exit:
            raise ParserError(status, msg)
        else:
            OptionParser.exit(self, status, msg)
 
 
def _build_main_parser():
    usage = "usage: %prog [options] <project_dir> [quiet_min]"
 
    parser = FlashbakeOptionParser(
        usage=usage, version='%s %s' % ('%prog', VERSION))
    parser.add_option('-c', '--context', dest='context_only',
            action='store_true', default=False,
            help='just generate and show the commit message, don\'t check for changes')
    parser.add_option('-v', '--verbose', dest='verbose',
            action='store_true', default=False,
            help='include debug information in the output')
    parser.add_option('-q', '--quiet', dest='quiet',
            action='store_true', default=False,
            help='disable all output excepts errors')
    parser.add_option('-d', '--dryrun', dest='dryrun',
            action='store_true', default=False,
            help='execute a dry run')
    parser.add_option('-p', '--plugins', dest='plugin_dir',
            action='store', type='string', metavar='PLUGIN_DIR',
            help='specify an additional location for plugins')
    parser.add_option('-r', '--purge', dest='purge',
            action='store_true', default=False,
            help='purge any files that have been deleted from source control')
    return parser
 
 
def _build_multi_parser():
    usage = "usage: %prog [options] <search_root> [quiet_min]"
    parser = FlashbakeOptionParser(
        usage=usage, version='%s %s' % ('%prog', VERSION))
    parser.add_option('-o', '--options', dest='flashbake_options', default='',
                      action='store', type='string', metavar='FLASHBAKE_OPTS',
                      help=("options to pass through to the 'flashbake' "
                            "command. Use quotes to pass multiple arguments."))
    return parser
 
 
def _load_plugin_dirs(options, home_dir):
    plugin_dir = join(home_dir, '.flashbake', 'plugins')
    if os.path.exists(plugin_dir):
        real_plugin_dir = realpath(plugin_dir)
        logging.debug('3rd party plugin directory exists, adding: %s' % real_plugin_dir)
        sys.path.insert(0, real_plugin_dir)
    else:
        logging.debug('3rd party plugin directory doesn\'t exist, skipping.')
        logging.debug('Only stock plugins will be available.')
 
    if options.plugin_dir != None:
        if os.path.exists(options.plugin_dir):
            logging.debug('Adding plugin directory, %s.' % options.plugin_dir)
            sys.path.insert(0, realpath(options.plugin_dir))
        else:
            logging.warn('Plugin directory, %s, doesn\'t exist.' % options.plugin_dir)
 
 
 
def _load_user_control(home_dir, project_dir, options):
    control_file = join(home_dir, '.flashbake', 'config')
    if os.path.exists(control_file):
        (hot_files, control_config) = control.parse_control(project_dir, control_file)
        control_config.context_only = options.context_only
    else:
        hot_files = None
        control_config = None
    return hot_files, control_config
 
 
def _find_control(parser, project_dir):
    control_file = join(project_dir, '.flashbake')
 
    # look for .control for backwards compatibility
    if not os.path.exists(control_file):
        control_file = join(project_dir, '.control')
 
    if not os.path.exists(control_file):
        parser.error('Could not find .flashbake or .control file in directory, "%s".' % project_dir)
        return None
    else:
        return control_file
 
 
def _context_only(options, project_dir, control_file, control_config, hot_files):
    try:
        (hot_files, control_config) = control.parse_control(project_dir, control_file, control_config, hot_files)
        control_config.context_only = options.context_only
        (hot_files, control_config) = control.prepare_control(hot_files, control_config)
 
        msg_filename = context.buildmessagefile(control_config)
        message_file = open(msg_filename, 'r')
 
        try:
            for line in message_file:
                print line.strip()
        finally:
            message_file.close()
            os.remove(msg_filename)
        return 0
    except (flashbake.git.VCError, flashbake.ConfigError), error:
        logging.error('Error: %s' % str(error))
        return 1
    except PluginError, error:
        _handle_bad_plugin(error)
        return 1
 
 
def _handle_bad_plugin(plugin_error):
    logging.debug('Plugin error, %s.' % plugin_error)
    if plugin_error.reason == PLUGIN_ERRORS.unknown_plugin or plugin_error.reason == PLUGIN_ERRORS.invalid_plugin: #@UndefinedVariable
        logging.error('Cannot load plugin, %s.' % plugin_error.plugin_spec)
        return
 
    if plugin_error.reason == PLUGIN_ERRORS.missing_attribute: #@UndefinedVariable
        logging.error('Plugin, %s, doesn\'t have the needed plugin attribute, %s.' \
                % (plugin_error.plugin_spec, plugin_error.name))
        return
 
    if plugin_error.reason == PLUGIN_ERRORS.invalid_attribute: #@UndefinedVariable
        logging.error('Plugin, %s, has an invalid plugin attribute, %s.' \
                % (plugin_error.plugin_spec, plugin_error.name))
        return
 
    if plugin_error.reason == PLUGIN_ERRORS.missing_property:
        logging.error('Plugin, %s, requires the config option, %s, but it was missing.' \
                % (plugin_error.plugin_spec, plugin_error.name))
        return