# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
import re
import sys
import os
import pkg_resources
from command import Command, BadCommand
import copydir
import pluginlib
import fnmatch
try:
    set
except NameError:
    from sets import Set as set

class CreateDistroCommand(Command):

    usage = 'PACKAGE_NAME [VAR=VALUE VAR2=VALUE2 ...]'
    summary = "Create the file layout for a Python distribution"
    short_description = summary

    description = """\
    Create a new project.  Projects are typically Python packages,
    ready for distribution.  Projects are created from templates, and
    represent different kinds of projects -- associated with a
    particular framework for instance.
    """

    parser = Command.standard_parser(
        simulate=True, no_interactive=True, quiet=True, overwrite=True)
    parser.add_option('-t', '--template',
                      dest='templates',
                      metavar='TEMPLATE',
                      action='append',
                      help="Add a template to the create process")
    parser.add_option('-o', '--output-dir',
                      dest='output_dir',
                      metavar='DIR',
                      default='.',
                      help="Write put the directory into DIR (default current directory)")
    parser.add_option('--svn-repository',
                      dest='svn_repository',
                      metavar='REPOS',
                      help="Create package at given repository location (this will create the standard trunk/ tags/ branches/ hierarchy)")
    parser.add_option('--list-templates',
                      dest='list_templates',
                      action='store_true',
                      help="List all templates available")
    parser.add_option('--list-variables',
                      dest="list_variables",
                      action="store_true",
                      help="List all variables expected by the given template (does not create a package)")
    parser.add_option('--inspect-files',
                      dest='inspect_files',
                      action='store_true',
                      help="Show where the files in the given (already created) directory came from (useful when using multiple templates)")
    parser.add_option('--config',
                      action='store',
                      dest='config',
                      help="Template variables file")

    _bad_chars_re = re.compile('[^a-zA-Z0-9_]')

    default_verbosity = 1
    default_interactive = 1

    def command(self):
        if self.options.list_templates:
            return self.list_templates()
        asked_tmpls = self.options.templates or ['basic_package']
        templates = []
        for tmpl_name in asked_tmpls:
            self.extend_templates(templates, tmpl_name)
        if self.options.list_variables:
            return self.list_variables(templates)
        if self.verbose:
            print 'Selected and implied templates:'
            max_tmpl_name = max([len(tmpl_name) for tmpl_name, tmpl in templates])
            for tmpl_name, tmpl in templates:
                print '  %s%s  %s' % (
                    tmpl_name, ' '*(max_tmpl_name-len(tmpl_name)),
                    tmpl.summary)
            print
        if not self.args:
            if self.interactive:
                dist_name = self.challenge('Enter project name')
            else:
                raise BadCommand('You must provide a PACKAGE_NAME')
        else:
            dist_name = self.args[0].lstrip(os.path.sep)

        templates = [tmpl for name, tmpl in templates]
        output_dir = os.path.join(self.options.output_dir, dist_name)
        
        pkg_name = self._bad_chars_re.sub('', dist_name.lower())
        vars = {'project': dist_name,
                'package': pkg_name,
                'egg': pluginlib.egg_name(dist_name),
                }
        vars.update(self.parse_vars(self.args[1:]))
        if self.options.config and os.path.exists(self.options.config):
            for key, value in self.read_vars(self.options.config).items():
                vars.setdefault(key, value)
        
        if self.verbose: # @@: > 1?
            self.display_vars(vars)

        if self.options.inspect_files:
            self.inspect_files(
                output_dir, templates, vars)
            return
        if not os.path.exists(output_dir):
            # We want to avoid asking questions in copydir if the path
            # doesn't exist yet
            copydir.all_answer = 'y'
        
        if self.options.svn_repository:
            self.setup_svn_repository(output_dir, dist_name)

        # First we want to make sure all the templates get a chance to
        # set their variables, all at once, with the most specialized
        # template going first (the last template is the most
        # specialized)...
        for template in templates[::-1]:
            vars = template.check_vars(vars, self)

        # Gather all the templates egg_plugins into one var
        egg_plugins = set()
        for template in templates:
            egg_plugins.update(template.egg_plugins)
        egg_plugins = list(egg_plugins)
        egg_plugins.sort()
        vars['egg_plugins'] = egg_plugins
            
        for template in templates:
            self.create_template(
                template, output_dir, vars)

        found_setup_py = False
        paster_plugins_mtime = None
        if os.path.exists(os.path.join(output_dir, 'setup.py')):
            # Grab paster_plugins.txt's mtime; used to determine if the
            # egg_info command wrote to it
            try:
                egg_info_dir = pluginlib.egg_info_dir(output_dir, dist_name)
            except IOError:
                egg_info_dir = None
            if egg_info_dir is not None:
                plugins_path = os.path.join(egg_info_dir, 'paster_plugins.txt')
                if os.path.exists(plugins_path):
                    paster_plugins_mtime = os.path.getmtime(plugins_path)

            self.run_command(sys.executable, 'setup.py', 'egg_info',
                             cwd=output_dir,
                             # This shouldn't be necessary, but a bug in setuptools 0.6c3 is causing a (not entirely fatal) problem that I don't want to fix right now:
                             expect_returncode=True)
            found_setup_py = True
        elif self.verbose > 1:
            print 'No setup.py (cannot run egg_info)'

        package_dir = vars.get('package_dir', None)
        if package_dir:
            output_dir = os.path.join(output_dir, package_dir)

        # With no setup.py this doesn't make sense:
        if found_setup_py:
            # Only write paster_plugins.txt if it wasn't written by
            # egg_info (the correct way). leaving us to do it is
            # deprecated and you'll get warned
            egg_info_dir = pluginlib.egg_info_dir(output_dir, dist_name)
            plugins_path = os.path.join(egg_info_dir, 'paster_plugins.txt')
            if len(egg_plugins) and (not os.path.exists(plugins_path) or \
                    os.path.getmtime(plugins_path) == paster_plugins_mtime):
                if self.verbose:
                    print >> sys.stderr, \
                        ('Manually creating paster_plugins.txt (deprecated! '
                         'pass a paster_plugins keyword to setup() instead)')
                for plugin in egg_plugins:
                    if self.verbose:
                        print 'Adding %s to paster_plugins.txt' % plugin
                    if not self.simulate:
                        pluginlib.add_plugin(egg_info_dir, plugin)
        
        if self.options.svn_repository:
            self.add_svn_repository(vars, output_dir)

        if self.options.config:
            write_vars = vars.copy()
            del write_vars['project']
            del write_vars['package']
            self.write_vars(self.options.config, write_vars)
        
    def create_template(self, template, output_dir, vars):
        if self.verbose:
            print 'Creating template %s' % template.name
        template.run(self, output_dir, vars)

    def setup_svn_repository(self, output_dir, dist_name):
        # @@: Use subprocess
        svn_repos = self.options.svn_repository
        svn_repos_path = os.path.join(svn_repos, dist_name).replace('\\','/')
        svn_command = 'svn'
        if sys.platform == 'win32':
            svn_command += '.exe'
        # @@: The previous method of formatting this string using \ doesn't work on Windows
        cmd = '%(svn_command)s mkdir %(svn_repos_path)s' + \
            ' %(svn_repos_path)s/trunk %(svn_repos_path)s/tags' + \
            ' %(svn_repos_path)s/branches -m "New project %(dist_name)s"'
        cmd = cmd % {
            'svn_repos_path': svn_repos_path, 
            'dist_name': dist_name,
            'svn_command':svn_command,
        }
        if self.verbose:
            print "Running:"
            print cmd
        if not self.simulate:
            os.system(cmd)
        svn_repos_path_trunk = os.path.join(svn_repos_path,'trunk').replace('\\','/')
        cmd = svn_command+' co "%s" "%s"' % (svn_repos_path_trunk, output_dir)
        if self.verbose:
            print "Running %s" % cmd
        if not self.simulate:
            os.system(cmd)

    ignore_egg_info_files = [
        'top_level.txt',
        'entry_points.txt',
        'requires.txt',
        'PKG-INFO',
        'namespace_packages.txt',
        'SOURCES.txt',
        'dependency_links.txt',
        'not-zip-safe']

    def add_svn_repository(self, vars, output_dir):
        svn_repos = self.options.svn_repository
        egg_info_dir = pluginlib.egg_info_dir(output_dir, vars['project'])
        svn_command = 'svn'
        if sys.platform == 'win32':
            svn_command += '.exe'
        self.run_command(svn_command, 'add', '-N', egg_info_dir)
        paster_plugins_file = os.path.join(
            egg_info_dir, 'paster_plugins.txt')
        if os.path.exists(paster_plugins_file):
            self.run_command(svn_command, 'add', paster_plugins_file)
        self.run_command(svn_command, 'ps', 'svn:ignore',
                         '\n'.join(self.ignore_egg_info_files),
                         egg_info_dir)
        if self.verbose:
            print ("You must next run 'svn commit' to commit the "
                   "files to repository")

    def extend_templates(self, templates, tmpl_name):
        if '#' in tmpl_name:
            dist_name, tmpl_name = tmpl_name.split('#', 1)
        else:
            dist_name, tmpl_name = None, tmpl_name
        if dist_name is None:
            for entry in self.all_entry_points():
                if entry.name == tmpl_name:
                    tmpl = entry.load()(entry.name)
                    dist_name = entry.dist.project_name
                    break
            else:
                raise LookupError(
                    'Template by name %r not found' % tmpl_name)
        else:
            dist = pkg_resources.get_distribution(dist_name)
            entry = dist.get_entry_info(
                'paste.paster_create_template', tmpl_name)
            tmpl = entry.load()(entry.name)
        full_name = '%s#%s' % (dist_name, tmpl_name)
        for item_full_name, item_tmpl in templates:
            if item_full_name == full_name:
                # Already loaded
                return
        for req_name in tmpl.required_templates:
            self.extend_templates(templates, req_name)
        templates.append((full_name, tmpl))
        
    def all_entry_points(self):
        if not hasattr(self, '_entry_points'):
            self._entry_points = list(pkg_resources.iter_entry_points(
            'paste.paster_create_template'))
        return self._entry_points

    def display_vars(self, vars):
        vars = vars.items()
        vars.sort()
        print 'Variables:'
        max_var = max([len(n) for n, v in vars])
        for name, value in vars:
            print '  %s:%s  %s' % (
                name, ' '*(max_var-len(name)), value)
        
    def list_templates(self):
        templates = []
        for entry in self.all_entry_points():
            try:
                templates.append(entry.load()(entry.name))
            except Exception, e:
                # We will not be stopped!
                print 'Warning: could not load entry point %s (%s: %s)' % (
                    entry.name, e.__class__.__name__, e)
        max_name = max([len(t.name) for t in templates])
        templates.sort(lambda a, b: cmp(a.name, b.name))
        print 'Available templates:'
        for template in templates:
            # @@: Wrap description
            print '  %s:%s  %s' % (
                template.name,
                ' '*(max_name-len(template.name)),
                template.summary)
        
    def inspect_files(self, output_dir, templates, vars):
        file_sources = {}
        for template in templates:
            self._find_files(template, vars, file_sources)
        self._show_files(output_dir, file_sources)
        self._show_leftovers(output_dir, file_sources)

    def _find_files(self, template, vars, file_sources):
        tmpl_dir = template.template_dir()
        self._find_template_files(
            template, tmpl_dir, vars, file_sources)

    def _find_template_files(self, template, tmpl_dir, vars,
                             file_sources, join=''):
        full_dir = os.path.join(tmpl_dir, join)
        for name in os.listdir(full_dir):
            if name.startswith('.'):
                continue
            if os.path.isdir(os.path.join(full_dir, name)):
                self._find_template_files(
                    template, tmpl_dir, vars, file_sources,
                    join=os.path.join(join, name))
                continue
            partial = os.path.join(join, name)
            for name, value in vars.items():
                partial = partial.replace('+%s+' % name, value)
            if partial.endswith('_tmpl'):
                partial = partial[:-5]
            file_sources.setdefault(partial, []).append(template)

    _ignore_filenames = ['.*', '*.pyc', '*.bak*']
    _ignore_dirs = ['CVS', '_darcs', '.svn']

    def _show_files(self, output_dir, file_sources, join='', indent=0):
        pad = ' '*(2*indent)
        full_dir = os.path.join(output_dir, join)
        names = os.listdir(full_dir)
        dirs = [n for n in names
                if os.path.isdir(os.path.join(full_dir, n))]
        fns = [n for n in names
               if not os.path.isdir(os.path.join(full_dir, n))]
        dirs.sort()
        names.sort()
        for name in names:
            skip_this = False
            for ext in self._ignore_filenames:
                if fnmatch.fnmatch(name, ext):
                    if self.verbose > 1:
                        print '%sIgnoring %s' % (pad, name)
                    skip_this = True
                    break
            if skip_this:
                continue
            partial = os.path.join(join, name)
            if partial not in file_sources:
                if self.verbose > 1:
                    print '%s%s (not from template)' % (pad, name)
                continue
            templates = file_sources.pop(partial)
            print '%s%s from:' % (pad, name)
            for template in templates:
                print '%s  %s' % (pad, template.name)
        for dir in dirs:
            if dir in self._ignore_dirs:
                continue
            print '%sRecursing into %s/' % (pad, dir)
            self._show_files(
                output_dir, file_sources,
                join=os.path.join(join, dir),
                indent=indent+1)

    def _show_leftovers(self, output_dir, file_sources):
        if not file_sources:
            return
        print 
        print 'These files were supposed to be generated by templates'
        print 'but were not found:'
        file_sources = file_sources.items()
        file_sources.sort()
        for partial, templates in file_sources:
            print '  %s from:' % partial
            for template in templates:
                print '    %s' % template.name

    def list_variables(self, templates):
        for tmpl_name, tmpl in templates:
            if not tmpl.read_vars():
                if self.verbose > 1:
                    self._show_template_vars(
                        tmpl_name, tmpl, 'No variables found')
                continue
            self._show_template_vars(tmpl_name, tmpl)

    def _show_template_vars(self, tmpl_name, tmpl, message=None):
        title = '%s (from %s)' % (tmpl.name, tmpl_name)
        print title
        print '-'*len(title)
        if message is not None:
            print '  %s' % message
            print
            return
        tmpl.print_vars(indent=2)
