################ QUICK AND NASTY RELATIVE INDENTATION TEMPLATES ################

import re

################################################################################

def strip_common_preceding_space(input_str):
    "Strips preceding common space so only relative indentation remains"

    preceding_whitespace = re.compile("^(?:(\s*?)\S)?")
    common_start = len(input_str)

    split = input_str.split("\n")
    for line in (l for l in split if l.strip()):
        for match in preceding_whitespace.finditer(line):
            common_start = min(match.span(1)[1], common_start)

    return "\n".join( [l[common_start:] for l in split] )

def pad_secondary_lines(input_str, padding):
    split = input_str.split('\n')
    return '\n'.join( [split[0]] + [(padding+l) for l in split[1:]] )

################################################################################

ph_re = re.compile("\${(.*?)}")
multi_line_re = re.MULTILINE | re.DOTALL

################################################################################

class Template(object):
    def __call__(self): 
        return self

    def __init__(self, template, strip_common = True, strip_excess = True):
        if strip_common: template = strip_common_preceding_space(template)
        if strip_excess: template = template.strip() + '\n'

        self.template = template

        self.find_ph_offsets()

    def find_ph_offsets(self):
        self.ph_offsets = dict()

        for lines in self.template.split('\n'):
            for match in ph_re.finditer(lines):
                self.ph_offsets[match.group(1)] = match.span()[0]

    def render(self, replacements = None, **kw):
        if not replacements: replacements = kw
        
        # missing_ph = [k for k in self.ph_offsets if k not in replacements]
        # excess_repl = [k for k in replacements if k not in self.ph_offsets]
        
        # A lot more performant
        assert len(replacements) == len(self.ph_offsets)

        # errors = []        
        # if missing_ph:  errors.append (
        #     'Missing replacements: %s' % ', '.join(missing_ph)
        # )
        # if excess_repl: errors.append (
        #     'Excess replacements: %s'  % ', '.join(excess_repl)
        # )
        # if errors: raise ValueError("\n".join(errors))
                 
        template = self.template[:]
        for ph_name, replacement in replacements.iteritems():
            ph_offset = self.ph_offsets[ph_name]
    
            ph_search = re.search ("\${%s}" % ph_name, template, multi_line_re)
            
            ph_start, ph_end = ph_search.span()
            
            padded = pad_secondary_lines(replacement, ph_offset * ' ')
    
            template = template[0:ph_start] + padded + template[ph_end:]
    
        return template

if __name__ == "__main__":
    
    print Template( '''

        def test_${test_name}(self):
            
            """
            
            ${docstring}
            
            """
            
            ${comments}
            
            self.assert_(not_completed() ''' 
    
    ).render( test_name = 'this_please', 
              docstring = 'Heading:\n\n    Indented Line\n    More',
              comments  = '# some comment\n# another comment', )

    check_that = Template('works with ${one} on each line, or even ${two_four}')

    print check_that.render(one='one', two_four='two')
    
    # BUG: Multiline replacements with 2 ph per line will go awry -> .ph_offsets
    #      Outside scope of stubber
    
    print check_that.render(one='one\n    one', two_four='two\n    not_right\n')
    
################################################################################