#!/usr/bin/python

import sys
if sys.version_info < (2, 7):
    sys.path.insert(0, '.config')

##########################################################
###   Arg Parser                                       ###
##########################################################

import argparse, textwrap

class ToggleAction(argparse.Action):
    def __init__(self, **kw):
        name = kw['option_strings'][0][2:]
        if name.startswith('no-'):
            name = name[3:]
        kw['dest'] = name
        kw['option_strings'] = [ '--no-' + name, '--' + name ]
        kw['nargs'] = 0
        kw['metavar'] = None
        super(ToggleAction, self).__init__(**kw)

    def __call__(self, parser, namespace, values, option_string=None):
        if option_string and option_string.startswith('--no-'):
            setattr(namespace, self.dest, False)
        else:
            setattr(namespace, self.dest, True)


class SmartHelpFmt(argparse.RawDescriptionHelpFormatter):
    def _format_action(self, action):
        if type(action) == ToggleAction:
            opts = action.option_strings
            action.option_strings = [ '--[no-]' + action.dest ]
            parts = super(SmartHelpFmt, self)._format_action(action)
            action.option_strings = opts
        else:
            parts = super(SmartHelpFmt, self)._format_action(action)
        return parts

    def _format_usage(self, usage, actions, groups, prefix):
        for g in groups:
            print(g)
        text = super(SmartHelpFmt, self)._format_usage(usage,
            actions, groups, prefix)
        # print "Usage", text
        return text

    def format_help(self):
        text = super(SmartHelpFmt, self).format_help()
        return text


# Create help group with general '--help' option and with
# specific '--help-GROUP' for every argument group
class ArgParse(argparse.ArgumentParser):
    def __init__(self, **kw):
        self.help_names = []
        self.help_groups = {}
        self.default_group = None
        kw['add_help'] = False
        kw['formatter_class'] = SmartHelpFmt
        super(ArgParse, self).__init__(**kw)
        self.add_argument_group('help', 'Help options', None)
        self.add_argument("-h", "--help", help = "Print help and exit",
            action = 'store_true', group = 'help')

    def add_argument_group(self, name, title=None, description=None,
        default = None):
        # print "add_argument_group: '%s'" % name, title, description, default
        if name in self.help_groups:
            raise BaseException("help group %s already exists" % name)
        self.help_groups[name] = super(ArgParse,
            self).add_argument_group(title, description)
        if name != 'help' and len(name.split()) == 1:
            self.add_argument("--help-" + name, group = 'help',
                action = 'store_true',
                help = "Help on " + title)
        if default:
            self.default_group = self.help_groups[name]
        return self.help_groups[name]

    def add_argument(self, *args, **kw):
        # print "add_argument:", args, kw
        if 'group' in kw:
            group = self.help_groups[kw['group']]
            del kw['group']
            return group.add_argument(*args, **kw)
        elif self.default_group:
            return self.default_group.add_argument(*args, **kw)
        else:
            return self.add_argument(*args, **kw)


    def format_help(self):
        all = 'help' in self.help_names
        if not all:
            self.help_names = [ n[5:] for n in self.help_names]

        formatter = self._get_formatter()

        if all:
            u = "%(prog)s "
            for g in self._action_groups:
                if g.title and g._group_actions:
                    u += '[_%s_] ' % g.title.replace(' ', '_')
            u = textwrap.fill(u,
                initial_indent = ' ' * 15,
                subsequent_indent = ' ' * 13,
                break_long_words = False).strip().replace('_', ' ')

            formatter.add_usage(u, None, [])
            formatter.add_text(self.description)

        if all:
            self.help_names = self.help_groups.keys()

        for name in self.help_names:
            group = self.help_groups[name]
            formatter.start_section(group.title)
            formatter.add_text(group.description)
            formatter.add_arguments(group._group_actions)
            formatter.end_section()

        if all:
            formatter.add_text(self.epilog)

        return formatter.format_help()

    def parse_args(self, args=None, namespace=None):
        a = super(ArgParse, self).parse_args(args, namespace)
        self.help_names = [ n for n in dir(a) if
            (n == 'help' or n.startswith("help_")) and getattr(a, n) ]
        if self.help_names:
            self.print_help()
            exit(0)

        return a


##########################################################
###   log                                              ###
##########################################################

import subprocess as sp, re, sys, os, datetime
import traceback, tempfile, shutil
import logging
x = logging.getLogger('app')
def init_log(debug, verbose, *args):
    for name in args:
        x = logging.getLogger(name)
        if debug:
            f = logging.Formatter("%(name)s (%(funcName)s:%(lineno)d) " +
                ":: %(message)s")
            verbose = True
        else:
            f = logging.Formatter("%(message)s")

        h = logging.StreamHandler()
        h.setFormatter(f)
        x.addHandler(h)
        if verbose:
            x.setLevel(logging.DEBUG)
        else:
            x.setLevel(logging.INFO)

p = ArgParse()


##########################################################
###   Varables                                         ###
##########################################################

vars = {}

def assign_cmd_vars():
    for v in args.vars:
        x.debug("test %s", v)
        m = re.match('^[A-Z_]+=.*', v)
        if not m:
            continue
        l = v.split('=')
        name = l[0]
        value = l[1]
        vars[name] = value
        x.debug("name %s, value '%s'", name, value)


##########################################################
###   Options                                          ###
##########################################################
def my_check_output(*popenargs, **kwargs):  
    if 'stdout' in kwargs:
        raise ValueError('stdout argument not allowed, it will be overridden.')
    cmd = kwargs.get("args")
    if cmd is None:
        cmd = popenargs[0]
    x.debug("exec: %s", cmd)
    process = sp.Popen(stdout=sp.PIPE, *popenargs, **kwargs)
    output, unused_err = process.communicate()
    retcode = process.poll()
    output = output.decode("utf-8").strip()
    if retcode:
        raise sp.CalledProcessError(retcode, cmd, output=output)
    return output

    
opts = {}
opt_order = []
args = None
help_groups = {}
stack = []
def opt(name):
    global stack, opts, help_groups

    if name in stack:
        raise BaseException("dep loop: second '" + name + "' in "
            + str(stack + [name]))
    stack.append(name)
    x = opts[name]
    if callable(x):
        while callable(x):
            x = x()
        opts[name] = x
    del stack[-1]
    return x


def opt_group_new(name, desc, long_desc = None, default = None):
    global help_groups

    if name in help_groups:
        raise BaseException("Help group %s' already exists" % name)
    help_groups[name] = p.add_argument_group(name, desc, long_desc, default)

def opt_new(name, **kw):
    global opts, opt_order

    if not re.match('^[a-zA-Z_][0-9a-zA-Z_]*$', name):
        raise BaseException("Illegal opt name '%s'" % name)
    if name in opts:
        raise BaseException("opt %s' already exists" % name)
    if 'help' in kw:
        if not 'group' in kw:
            help_groups['app'].add_argument("--" + name, **kw)
        else:
            group = kw['group']
            del kw['group']
            help_groups[group].add_argument("--" + name, **kw)
    d = None
    if 'default' in kw:
        d = kw['default']
        if type(d) == str:
            d = d.strip()
    opts[name] = d
    opt_order.append(name)

# Creates set of variables from pkg-config output.
# First, it creates var for cflags and libs
#   vname_cflags :: pkg-config pname --cflags
#   vname_libs :: pkg-config pname --libs
# Then, for every var in vars
#   vname_var :: pkg-config pname --variable=var
#
# Parameters:
# vname    - prefix for variable names
# pname    - pkg-config package name
# pvars    - list of package variables, eg ['prefix', 'datadir']
# pversion - version condition, eg '--atleast-version=2.14'
# required - true, for mandatory packages 
def opt_new_from_pkg(vname, pname, pvars = [], pversion = "", required = True):
    cmd = ['pkg-config', '--print-errors', pname]
    try:
        my_check_output(cmd + pversion.split(), stderr=sp.STDOUT)
    except sp.CalledProcessError as e:
        if required:
            print(e.output)
            print("This usually means that '" + pname + \
                "' development files are not installed")
            exit(1)
        else:
            return

    opt_new(vname + '_libs',
        default = my_check_output(cmd + ['--libs']))
    opt_new(vname + '_cflags',
        default = my_check_output(cmd + ['--cflags']))
    opt_new(vname + '_version',
        default = my_check_output(cmd + ['--modversion']))
    for v in pvars:
        opt_new(vname + '_' + v,
                default = my_check_output(cmd + ['--variable=' + v]))

def pkg_exists(pname, extra = ""):
    cmd = ['pkg-config', pname] + extra.split()
    return sp.call(cmd) == 0


def opt_set(name, value):
    global opts
    x.debug("set %s = '%s'", name, value)
    opts[name] = value

def assign_cmd_opts():
    global opts
    x.debug("assign")
    for k in [ y for y in dir(args) if y[:1] != '_' and y != 'vars' ]:
        opts[k] = getattr(args, k)
        x.debug("opts[%s] = '%s'", k, opts[k])

def write_config():
    now = datetime.datetime.now()
    smake = "# Generated at %s \n# by command %s\n\n" % (now, sys.argv)
    sc = "// Generated at %s \n// by command %s\n\n" % (now, sys.argv)

    for name in sorted(vars.keys()):
        smake += "%s ?= %s\n" % (name, vars[name])
    smake += "\n"

    for name in opt_order:
        v = opt(name)
        # print name, type(v)
        if not v:
            continue
        if type(v) == str:
            v = v.replace('\n', '\\\n')
            v = v.replace('"', '')
        elif type(v) == bool:
            v = int(v)

        smake += "%s := %s\n" % (name.upper(), str(v))
        if type(v) == str or type(v) == unicode:
            v = '"' + v + '"'
        sc += '#define %s  %s\n' % (name.upper(), str(v))
    smake += "\n"
    sc += "\n"
    print('Created config.mk')
    open('config.mk', 'w').write(smake)
    print('Created config.h')
    open('config.h', 'w').write(sc)
  

def write_repl():
    s = ''
    for name in opt_order:
        s += "    '%s' : '%s',\n" % (name, str(opt(name)))
    s = re.sub('#repl_dict#', s, open('.config/repl.py', 'r').read())
    open('repl.py', 'w').write(s)
    os.chmod('repl.py', 0o755)
    
    
def init():
    pass

def resolve():
    pass

def report():
    pass

##########################################################
###   Self Install                                     ###
##########################################################

def mc_update():
    x.info("Downloading latest code")

    dtmp = tempfile.mkdtemp()
    repo = 'https://github.com/aanatoly/miniconf.git'

    cmd = 'git clone -q %s %s' % (repo, dtmp)
    my_check_output(cmd, shell = True)

    cmd = 'rsync -a %s/engine/ .' % dtmp
    my_check_output(cmd, shell = True)

    shutil.rmtree(dtmp)

def mc_makefiles():
    header_text = '## miniconf makefiles ## %d.%d ##\n'
    header_re = '(?m)^## miniconf makefiles ## (\d+)\.(\d+) ##\n'
    def makefile_needed(dirname):
        x.debug("makefile_needed '%s'", dirname)
        if dirname.split('/')[-1] == '.config':
            x.debug("no - this dir is ignored")
            return False
        try:
            h = open(os.path.join(dirname, 'Makefile'), 'r').readline()
        except BaseException as e:
            x.debug("yes - no read - %s", e)
            return True
        m = re.match(header_re, h)
        if not m:
            x.debug("yes - no header - %s", h)
            return True
        x.debug("no - header is ok")
        return False

    x.info("Creating makefiles")
    for dirname, dirs, filenames in os.walk('./'):
        # remove starting './'
        dirname = dirname[2:]
        x.debug("dirname %s", dirname)
        if dirname == '':
            try:
                dirs.remove('.config')
            except:
                pass

        for d in ['.git', '.svn', 'CVS']:
            try:
                dirs.remove(d)
            except:
                pass   
        x.debug("dirs: %s", dirs)
        if not makefile_needed(dirname):
            continue
        level = len([ v for v in dirname.split('/') if v ])
        topdir = '/'.join(['..'] * level)
        if not topdir: topdir = '.'
        text = header_text % (1, 1)
        text += '\nTOPDIR := ' + topdir + '\n\n'
        if dirs:
            text += 'SUBDIRS := ' + ' \\\n    '.join(sorted(dirs)) + '\n\n'

        cfiles = sorted([ f for f in filenames if f.endswith('.c') ])
        cfiles = ' \\\n    '.join(cfiles)
        x.debug('cfiles %s', cfiles)
        if cfiles:
            name = os.path.basename(dirname)
            text += '%s_src = %s\n' % (name, cfiles)
            text += '%s_cflags = \n' % name
            text += '%s_libs = \n' % name
            text += '%s_type = \n' % name
            text += '\n'
        text += 'include $(TOPDIR)/.config/rules.mk\n'

        path = os.path.join(dirname, 'Makefile')
        x.info("Create %s", path)
        open(path, 'w').write(text)


    print( "\nNow, you can test it. Run the commands:" )
    print( "./configure --help" )
    print( "./configure" )
    print( "make help" )
    print( "make V=1" )


##########################################################
###   main                                             ###
##########################################################

def main():
    global p, x, args

    opt_group_new('miniconf', 'miniconf installation options')
    p.add_argument("--mc-update", group = 'miniconf',
        help = "update or install configure scripts",
        action = 'store_true')
    p.add_argument("--mc-makefiles", group = 'miniconf',
        help = "create Makefiles for a project",
        action = 'store_true', default = False)
    p.add_argument("--mc-debug", group = 'miniconf',
        help = "enable debug output in miniconf",
        action = 'store_true', default = False)
    
    opt_group_new('make', 'Makefile vars')
    p.add_argument("vars", group = 'make',
        help = "makefile vars, like 'CFLAGS=-02'", nargs='*')

    init()
    args = p.parse_args()
    init_log(args.mc_debug, 0, 'app')
    mc = 0
    if args.mc_update:
        mc_update()
        mc = 1
    if args.mc_makefiles:
        mc_makefiles()
        mc = 1

    if mc:
        exit(0)
  
    assign_cmd_vars()
    assign_cmd_opts()
    resolve()
    write_config()
    write_repl()
    report()

if __name__ == "__main__":
    try:
        name = '.config/options.py'
        code = compile(open(name, "r").read(), name, 'exec')
        exec(code)
    except:
        x.info("Can't read .config/options.py")
        pass
    main()
