#!/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
