"""Bring time stamps of generated checked-in files into the right order

A versioned configuration file .hgtouch specifies generated files, in the
syntax of make rules.

  output:    input1 input2

In addition to the dependency syntax, #-comments are supported.
"""
from __future__ import with_statement
import errno
import os
import time

def parse_config(repo):
    try:
        fp = repo.wfile(".hgtouch")
    except IOError, e:
        if e.errno != errno.ENOENT:
            raise
        return {}
    result = {}
    with fp:
        for line in fp:
            # strip comments
            line = line.split('#')[0].strip()
            if ':' not in line:
                continue
            outputs, inputs = line.split(':', 1)
            outputs = outputs.split()
            inputs = inputs.split()
            for o in outputs:
                try:
                    result[o].extend(inputs)
                except KeyError:
                    result[o] = inputs
    return result

def check_rule(ui, repo, modified, basedir, output, inputs):
    """Verify that the output is newer than any of the inputs.
    Return (status, stamp), where status is True if the update succeeded,
    and stamp is the newest time stamp assigned  to any file (might be in
    the future).

    If basedir is nonempty, it gives a directory in which the tree is to
    be checked.
    """
    f_output = repo.wjoin(os.path.join(basedir, output))
    try:
        o_time = os.stat(f_output).st_mtime
    except OSError:
        ui.warn("Generated file %s does not exist\n" % output)
        return False, 0
    youngest = 0   # youngest dependency
    backdate = None
    backdate_source = None
    for i in inputs:
        f_i = repo.wjoin(os.path.join(basedir, i))
        try:
            i_time = os.stat(f_i).st_mtime
        except OSError:
            ui.warn(".hgtouch input file %s does not exist\n" % i)
            return False, 0
        if i in modified:
            # input is modified. Need to backdate at least to i_time
            if backdate is None or backdate > i_time:
                backdate = i_time
                backdate_source = i
            continue
        youngest = max(i_time, youngest)
    if backdate is not None:
        ui.warn("Input %s for file %s locally modified\n" % (backdate_source, output))
        # set to 1s before oldest modified input
        backdate -= 1
        os.utime(f_output, (backdate, backdate))
        return False, 0
    if youngest >= o_time:
        ui.note("Touching %s\n" % output)
        youngest += 1
        os.utime(f_output, (youngest, youngest))
        return True, youngest
    else:
        # Nothing to update
        return True, 0

def do_touch(ui, repo, basedir):
    if basedir:
        if not os.path.isdir(repo.wjoin(basedir)):
            ui.warn("Abort: basedir %r does not exist\n" % basedir)
            return
        modified = []
    else:
        modified = repo.status()[0]
    dependencies = parse_config(repo)
    success = True
    tstamp = 0       # newest time stamp assigned
    # try processing all rules in topological order
    hold_back = {}
    while dependencies:
        output, inputs = dependencies.popitem()
        # check whether any of the inputs is generated
        for i in inputs:
            if i in dependencies:
                hold_back[output] = inputs
                continue
        _success, _tstamp = check_rule(ui, repo, modified, basedir, output, inputs)
        success = success and _success
        tstamp = max(tstamp, _tstamp)
        # put back held back rules
        dependencies.update(hold_back)
        hold_back = {}
    now = time.time()
    if tstamp > now:
        # wait until real time has passed the newest time stamp, to
        # avoid having files dated in the future
        time.sleep(tstamp-now)
    if hold_back:
        ui.warn("Cyclic dependency involving %s\n" % (' '.join(hold_back.keys())))
        return False
    return success

def touch(ui, repo, basedir):
    "touch generated files that are older than their sources after an update."
    do_touch(ui, repo, basedir)

cmdtable = {
    "touch": (touch,
              [('b', 'basedir', '', 'base dir of the tree to apply touching')],
              "hg touch [-b BASEDIR]")
}
