#!/usr/bin/env python
# encoding: utf-8

"""
Waf tool for defining ardupilot's submodules, so that they are kept up to date.
Submodules can be considered dynamic sources, since they are updated during the
build. Furthermore, they can be used to generate other dynamic sources (mavlink
headers generation, for example). Thus, the correct use of this tool should
have three build groups: first one for updating the submodules, second for
generating any dynamic source from them, and the last one for the build. And
post_mode should be set to POST_LAZY. Example::

    def build(bld):
        bld.post_mode = waflib.Build.POST_LAZY

        bld.add_group('git_submodules')
        # gtest submodule
        bld(
            features='git_submodule'
            git_submodule='gtest',
        )
        # mavlink submodule with syntactic sugar
        bld.git_submodule('mavlink')
        ...

        # now, for the dynamic sources
        bld.add_group('dynamic_sources')
        ...

        # now, below go the task generators for normal build process
        bld.add_group('build')
        ...
"""

from waflib import Context, Logs, Task, Utils
from waflib.Configure import conf
from waflib.TaskGen import before_method, feature, taskgen_method

import os.path
import re

class update_submodule(Task.Task):
    color = 'BLUE'
    run_str = '${GIT} submodule update --recursive --init -- ${SUBMODULE_PATH}'

    fast_forward_diff_re = dict(
        removed=re.compile(r'-Subproject commit ([0-9a-f]+)'),
        added=re.compile(r'\+Subproject commit ([0-9a-f]+)')
    )

    def is_fast_forward(self, path):
        bld = self.generator.bld
        git = self.env.get_flat('GIT')

        cmd = git, 'diff', '--submodule=short', '--', os.path.basename(path)
        cwd = self.cwd.make_node(os.path.dirname(path))
        out = bld.cmd_and_log(cmd, quiet=Context.BOTH, cwd=cwd)

        m = self.fast_forward_diff_re['removed'].search(out)
        n = self.fast_forward_diff_re['added'].search(out)
        if not m or not n:
            bld.fatal('git_submodule: failed to parse diff')

        head = n.group(1)
        wanted = m.group(1)
        cmd = git, 'merge-base', head, wanted
        cwd = self.cwd.make_node(path)
        out = bld.cmd_and_log(cmd, quiet=Context.BOTH, cwd=cwd)

        return out.strip() == head

    def runnable_status(self):
        e = self.env.get_flat
        cmd = e('GIT'), 'submodule', 'status', '--recursive', '--', e('SUBMODULE_PATH')
        out = self.generator.bld.cmd_and_log(cmd, quiet=Context.BOTH, cwd=self.cwd)

        self.non_fast_forward = []

        # git submodule status uses a blank prefix for submodules that are up
        # to date
        r = Task.SKIP_ME
        for line in out.splitlines():
            prefix = line[0]
            path = line[1:].split()[1]
            if prefix == ' ':
                continue
            if prefix == '-':
                r = Task.RUN_ME
            if prefix == '+':
                if not self.is_fast_forward(path):
                    self.non_fast_forward.append(path)
                else:
                    r = Task.RUN_ME

        if self.non_fast_forward:
            r = Task.SKIP_ME

        return r

    def uid(self):
        if not hasattr(self, 'uid_'):
            m = Utils.md5()
            def u(s):
                m.update(s.encode('utf-8'))
            u(self.__class__.__name__)
            u(self.env.get_flat('SUBMODULE_PATH'))
            self.uid_ = m.digest()

        return self.uid_

    def __str__(self):
        return 'Submodule update: %s' % self.submodule

def configure(cfg):
    cfg.find_program('git')

_submodules_tasks = {}

@taskgen_method
def git_submodule_update(self, name):
    if name not in _submodules_tasks:
        module_node = self.bld.srcnode.make_node(os.path.join('modules', name))

        tsk = self.create_task('update_submodule', submodule=name)
        tsk.cwd = self.bld.srcnode
        tsk.env.SUBMODULE_PATH = module_node.abspath()

        _submodules_tasks[name] = tsk

    return _submodules_tasks[name]


@feature('git_submodule')
@before_method('process_source')
def process_module_dependencies(self):
    self.git_submodule = getattr(self, 'git_submodule', '')
    if not self.git_submodule:
        self.bld.fatal('git_submodule: empty or missing git_submodule argument')
    self.git_submodule_update(self.git_submodule)

@conf
def git_submodule(bld, git_submodule, **kw):
    kw['git_submodule'] = git_submodule
    kw['features'] = Utils.to_list(kw.get('features', ''))
    kw['features'].append('git_submodule')

    return bld(**kw)

def _post_fun(bld):
    Logs.info('')
    for name, t in _submodules_tasks.items():
        if not t.non_fast_forward:
            continue
        Logs.warn("Submodule %s not updated: non-fastforward" % name)

@conf
def git_submodule_post_fun(bld):
    bld.add_post_fun(_post_fun)

def _git_head_hash(ctx, path, short=False):
    cmd = [ctx.env.get_flat('GIT'), 'rev-parse']
    if short:
        cmd.append('--short=8')
    cmd.append('HEAD')
    out = ctx.cmd_and_log(cmd, quiet=Context.BOTH, cwd=path)
    return out.strip()

@conf
def git_submodule_head_hash(self, name, short=False):
    module_node = self.srcnode.make_node(os.path.join('modules', name))
    return _git_head_hash(self, module_node.abspath(), short=short)

@conf
def git_head_hash(self, short=False):
    return _git_head_hash(self, self.srcnode.abspath(), short=short)