#!/usr/bin/env python3
# FileGenerator.py - implemented 2013 by Neil Hodgson neilh@scintilla.org
# Released to the public domain.

# Generate or regenerate source files based on comments in those files.
# May be modified in-place or a template may be generated into a complete file.
# Requires Python 2.5 or later
# The files are copied to a string apart from sections between a
# ++Autogenerated comment and a --Autogenerated comment which is
# generated by the CopyWithInsertion function. After the whole string is
# instantiated, it is compared with the target file and if different the file
# is rewritten.

from __future__ import with_statement

import codecs, os, re, string, sys

lineEnd = "\r\n" if sys.platform == "win32" else "\n"

def UpdateFile(filename, updated):
    """ If the file contents are different to updated then copy updated into the
    file else leave alone so Mercurial and make don't treat it as modified. """
    newOrChanged = "Changed"
    try:
        with codecs.open(filename, "r", "utf-8") as infile:
            original = infile.read()
        if updated == original:
            # Same as before so don't write
            return
        os.unlink(filename)
    except IOError: # File is not there yet
        newOrChanged = "New"
    with codecs.open(filename, "w", "utf-8") as outfile:
        outfile.write(updated)
    print("%s %s" % (newOrChanged, filename))

# Automatically generated sections contain start and end comments,
# a definition line and the results.
# The results are replaced by regenerating based on the definition line.
# The definition line is a comment prefix followed by "**".
# If there is a digit after the ** then this indicates which list to use
# and the digit and next character are not part of the definition
# Backslash is used as an escape within the definition line.
# The part between \( and \) is repeated for each item in the list.
# \* is replaced by each list item. \t, and \n are tab and newline.
# If there is no definition line than the first list is copied verbatim.
# If retainDefs then the comments controlling generation are copied.
def CopyWithInsertion(input, commentPrefix, retainDefs, lists):
    copying = 1
    generated = False
    listid = 0
    output = []
    for line in input.splitlines(0):
        isStartGenerated = line.lstrip().startswith(commentPrefix + "++Autogenerated")
        if copying and not isStartGenerated:
            output.append(line)
        if isStartGenerated:
            if retainDefs:
                output.append(line)
            copying = 0
            generated = False
        elif not copying and not generated:
            # Generating
            if line.startswith(commentPrefix + "**"):
                # Pattern to transform input data
                if retainDefs:
                    output.append(line)
                definition = line[len(commentPrefix + "**"):]
                if (commentPrefix == "<!--") and (" -->" in definition):
                    definition = definition.replace(" -->", "")
                listid = 0
                if definition[0] in string.digits:
                    listid = int(definition[:1])
                    definition = definition[2:]
                # Hide double slashes as a control character
                definition = definition.replace("\\\\", "\001")
                # Do some normal C style transforms
                definition = definition.replace("\\n", "\n")
                definition = definition.replace("\\t", "\t")
                # Get the doubled backslashes back as single backslashes
                definition = definition.replace("\001", "\\")
                startRepeat = definition.find("\\(")
                endRepeat = definition.find("\\)")
                intro = definition[:startRepeat]
                out = ""
                if intro.endswith("\n"):
                    pos = 0
                else:
                    pos = len(intro)
                out += intro
                middle = definition[startRepeat+2:endRepeat]
                for i in lists[listid]:
                    item = middle.replace("\\*", i)
                    if pos and (pos + len(item) >= 80):
                        out += "\\\n"
                        pos = 0
                    out += item
                    pos += len(item)
                    if item.endswith("\n"):
                        pos = 0
                outro = definition[endRepeat+2:]
                out += outro
                out = out.replace("\n", lineEnd) # correct EOLs in generated content
                output.append(out)
            else:
                # Simple form with no rule to transform input
                output.extend(lists[0])
            generated = True
        if line.lstrip().startswith(commentPrefix + "--Autogenerated") or \
            line.lstrip().startswith(commentPrefix + "~~Autogenerated"):
            copying = 1
            if retainDefs:
                output.append(line)
    output = [line.rstrip(" \t") for line in output] # trim trailing whitespace
    return lineEnd.join(output) + lineEnd

def GenerateFile(inpath, outpath, commentPrefix, retainDefs, *lists):
    """Generate 'outpath' from 'inpath'.
    """

    try:
        with codecs.open(inpath, "r", "UTF-8") as infile:
            original = infile.read()
        updated = CopyWithInsertion(original, commentPrefix,
            retainDefs, lists)
        UpdateFile(outpath, updated)
    except IOError:
        print("Can not open %s" % inpath)

def Generate(inpath, outpath, commentPrefix, *lists):
    """Generate 'outpath' from 'inpath'.
    """
    GenerateFile(inpath, outpath, commentPrefix, inpath == outpath, *lists)

def Regenerate(filename, commentPrefix, *lists):
    """Regenerate the given file.
    """
    Generate(filename, filename, commentPrefix, *lists)

def UpdateLineInPlistFile(path, key, value):
    """Replace a single string value preceded by 'key' in an XML plist file.
    """
    lines = []
    keyCurrent = ""
    with codecs.open(path, "rb", "utf-8") as f:
        for l in f.readlines():
            ls = l.strip()
            if ls.startswith("<key>"):
                keyCurrent = ls.replace("<key>", "").replace("</key>", "")
            elif ls.startswith("<string>"):
                if keyCurrent == key:
                    start, tag, rest = l.partition("<string>")
                    val, etag, end = rest.partition("</string>")
                    l = start + tag + value + etag + end
            lines.append(l)
    contents = "".join(lines)
    UpdateFile(path, contents)

def UpdateLineInFile(path, linePrefix, lineReplace):
    lines = []
    updated = False
    with codecs.open(path, "r", "utf-8") as f:
        for l in f.readlines():
            l = l.rstrip()
            if not updated and l.startswith(linePrefix):
                lines.append(lineReplace)
                updated = True
            else:
                lines.append(l)
    contents = lineEnd.join(lines) + lineEnd
    UpdateFile(path, contents)

def ReadFileAsList(path):
    """Read all the lnes in the file and return as a list of strings without line ends.
    """
    with codecs.open(path, "rU", "utf-8") as f:
        return [l.rstrip('\n') for l in f]

def UpdateFileFromLines(path, lines, lineEndToUse):
    """Join the lines with the lineEndToUse then update file if the result is different.
    """
    contents = lineEndToUse.join(lines) + lineEndToUse
    UpdateFile(path, contents)

def FindSectionInList(lines, markers):
    """Find a section defined by an initial start marker, an optional secondary
    marker and an end marker.
    The section is between the secondary/initial start and the end.
    Report as a slice object so the section can be extracted or replaced.
    Raises an exception if the markers can't be found.
    """
    start = -1
    end = -1
    state = 0
    for i, l in enumerate(lines):
        if markers[0] in l:
            if markers[1]:
                state = 1
            else:
                start = i+1
                state = 2
        elif state == 1:
            if markers[1] in l:
                start = i+1
                state = 2
        elif state == 2:
            if markers[2] in l:
                end = i
                state = 3
    # Check that section was found
    if start == -1:
        raise Exception("Could not find start marker(s) |" + markers[0] + "|" + markers[1] + "|")
    if end == -1:
        raise Exception("Could not find end marker " + markers[2])
    return slice(start, end)

def ReplaceREInFile(path, match, replace, count=0):
    with codecs.open(path, "r", "utf-8") as f:
        contents = f.read()
    contents = re.sub(match, replace, contents, count)
    UpdateFile(path, contents)
