#!/usr/bin/env python
"""
wmlflip -- coordinate transformation for .cfg macro calls.

Modify macro coordinate arguments in a .cfg file.  Use this if you've
mirror-reversed a map and need to change coordinate-using macros, or
cropped one and need to translate map coordinates.

Takes a cross-reference of all known macros and looks for formals that
are either X, Y, *_X, or _Y, so it's guaranteed to catch all relevant
macro arguments.  Also catches WML-fragment arguments of the form
(x,y=mmm,nnn), and x= y= pairs on adjacent lines as in UnitWML
declrations (but will fail if the x= line has a comment).

Two cases this will *not* handle: Interleaved coordinate list
literals like x=1,2,3,4,5, and ranges like x=4-6.

Options:
    -m        Argument of this switch should name the map file.
              Required, because the coordinate transform needs to know the
              map dimensions.

    -x        Coordinate transformation for a horizontally flipped map.

    -t        Translate - shift coordinates by specified xd,yd offsets

    -v        Enable debugging output.

    -h        Emit a help message and quit.

If your offsets are negative (and thus led with '-') insert -- after the
options to pacify the argument parser.

More transformations would be easy to write.
"""

import sys, os, time, getopt, cStringIO, re
from wesnoth.wmltools import *

class ParseArgs:
    "Mine macro argument locations out of a .cfg file."
    def __init__(self, fp, verbose=False):
        self.fp = fp
        self.verbose = verbose
        self.parsed = []
        self.namestack = []
        self.pushback = None
        self.lead = ""
        self.parse_until([''])
    def getchar(self):
        if self.pushback:
            c = self.pushback
            self.pushback = None
            return c
        else:
            return self.fp.read(1)
    def ungetchar(self, c):
        if verbose:
            print "pushing back", c
        self.pushback = c
    def parse_until(self, enders):
        "Parse until we reach specified terminator."
        if self.verbose:
            self.lead += "*"
            print self.lead + " parse_until(%s) starts" % enders
        while True:
            c = self.getchar()
            if self.verbose:
                print self.lead + "I see", c
            if c in enders:
                if self.verbose:
                    print self.lead + "parse_until(%s) ends" % enders
                    self.lead = self.lead[:-1]
                return c
            elif c == '{':
              self.parse_call()
    def parse_call(self):
        "We see a start of call."
        if self.verbose:
            self.lead += "*"
            print self.lead + "parse_call()"
        self.namestack.append(["", []])
        # Fill in the name of the called macro
        while True:
            c =  self.getchar()
            if c.isalnum() or c == '_':
                self.namestack[-1][0] += c
            else:
                break
        if self.verbose:
            print self.lead + "name", self.namestack[-1]
        # Discard if no arguments
        if c == '}':
            self.namestack.pop()
            if self.verbose:
                print self.lead + "parse_call() ends"
                self.lead = self.lead[:-1]
            return
        # If non-space, this is something like a filename include;
        # skip until closing }
        if not c.isspace():
            while True:
                c =  self.getchar()
                if c == '}':
                    if self.verbose:
                        print self.lead + "parse_call() ends"
                        self.lead = self.lead[:-1]
                    return
        # It's a macro call with arguments;
        # parse them, recording the character offsets
        while self.parse_actual():
            continue
        # Discard trailing }
        self.getchar()
        # Record the scope we just parsed
        self.parsed.append(self.namestack.pop())
        if self.verbose:
            print self.lead + "parse_call() ends"
            self.lead = self.lead[:-1]
    def parse_actual(self):
        "Parse an actual argument."
        # Skip leading whitespace
        if self.verbose:
            self.lead += "*"
            print self.lead + "parse_actual() begins"
        while True:
            c = self.getchar()
            if not c.isspace():
                break
        if c == '}':
            if self.verbose:
                print "** parse_actual() returns False"
                self.lead = self.lead[:-1]
            return False
        # Looks like we have a real argument
        argstart = self.fp.tell() - 1
        # Skip leading translation mark, if any
        if c == "_":
            c = self.getchar()
        # Get the argument itself
        if c == '{':
            self.parse_call()
            argend = self.fp.tell()
        elif c == '(':
            self.parse_until([")"])
            argend = self.fp.tell()
        elif c == '"':
            if verbose:
                print self.lead + "starting string argument"
            self.parse_until(['"'])
            argend = self.fp.tell()
        else:
            ender = self.parse_until(['', ' ', '\t', '\r', '\n', '}'])
            argend = self.fp.tell() - 1
            self.ungetchar(ender)
        self.namestack[-1][1].append((argstart, argend))
        if self.verbose:
            print self.lead + "parse_actual() returns True"
            self.lead = self.lead[:-1]
        return True

def relevant_macros():
    "Compute indices of (X, Y) pairs in formals of all mainline macros."
    # Cross-reference all files.
    here = os.getcwd()
    pop_to_top("wmlflip")  
    cref = CrossRef(scopelist())
    os.chdir(here)

    # Look at all definitions.  Extract those with  in "_?X" or "_?Y".
    # Generate a dictionary mapping definition names to the indices
    # the X Y formal arguments.
    relevant = {}
    for name in cref.xref:
        for ref in cref.xref[name]:
            have_x = have_y = None
            for (i, arg) in enumerate(ref.args):
                if arg == "X" or arg.endswith("_X"):
                    have_x = i
                if arg == "Y" or arg.endswith("_Y"):
                    have_y = i
            if have_x is not None and have_y is not None:
                relevant[name] = (have_x, have_y)    
    return relevant

def transformables(filename, relevant, verbose):
    "Return a list of transformable (X,Y) regions in the specified file."
    # Grab the content
    fp = open(filename, "r")
    content = fp.read()
    fp.close()

    # Get argument offsets from it.
    calls = ParseArgs(cStringIO.StringIO(content), verbose)

    # Filter out irrelevant calls.
    parsed = filter(lambda x: x[0] in relevant, calls.parsed)

    # Extract coordinate pair locations from macro arguments.
    pairs = []
    for (name, arglocs) in parsed:
        (have_x, have_y) = relevant[name]
        pairs.append((arglocs[have_x], arglocs[have_y]))

    # Extract spans associated with x,y attributes.
    for m in re.finditer("x,y=([0-9]+),([0-9]+)", content):
        pairs.append(((m.start(1), m.end(1)), (m.start(2), m.end(2))))

    # Extract spans associated with x= y= on adjacent lines.
    for m in re.finditer(r"x=([0-9]+)\s+y=([0-9]+)", content):
        pairs.append(((m.start(1), m.end(1)), (m.start(2), m.end(2))))

    # More pair extractions can go here.

    # Sort by start of the x coordinate, then reverse the list,
    # so later changes won't screw up earlier ones.
    # Presumes that coordinate pairs are never interleaved.
    pairs.sort(lambda p, q: cmp(p[0][0], q[0][0]))
    pairs.reverse()

    # Return the file content as a string and the transformable extents in it.
    return (content, pairs)

def mapsize(filename):
    "Return the size of a specified mapfile."
    x = y = 0
    for line in open(filename):
        if "," in line:
            y += 1
            nx = line.count(",") + 1
            assert(x == 0 or x == nx)
            x = nx
    return (x, y)

if __name__ == '__main__':
    flip_x = flip_y = False
    verbose = 0
    mapfile = None
    translate = False
    (options, arguments) = getopt.getopt(sys.argv[1:], "m:txyv")
    
    for (switch, val) in options:
        if switch in ('-h', '--help'):
            sys.stderr.write(__doc__)
            sys.exit(0)
        elif switch in ('-m'):
            mapfile = val
        elif switch in ('-t'):
            translate = True
        elif switch in ('-x'):
            flip_x = True
        elif switch in ('-y'):
            print >>sys.stderr, "Vertical flip is not yet supported."
            sys.exit(0)
        elif switch == '-v':
            verbose += 1
    if verbose:
        print "Debugging output enabled."

    if mapfile:
        (mx, my) = mapsize(mapfile)
        print >>sys.stderr, "%s is %d wide by %d high" % (mapfile, mx, my)

    if arguments and not flip_x and not translate:
        print >>sys.stderr, "No coordinate transform is specified."
        sys.exit(0)

    if flip_x and not mapfile:
        print >>sys.stderr, "X flip transformation needs to know the map size.."
        sys.exit(0)

    if translate:
        dx = int(arguments.pop(0))
        dy = int(arguments.pop(0))

    # Are we doing file transformations?
    if arguments:
        relevant = relevant_macros()
        # For each file named on the command line...
        for filename in arguments:
            if verbose:
                print >>sys.stderr, "Processing file", filename

            (content, pairs) = transformables(filename, relevant, verbose > 1)

            # Extract the existing coordinates as numbers
            source = []
            for ((xs, xe), (ys, ye)) in pairs:
                x = int(content[xs:xe])
                y = int(content[ys:ye])
                source.append((x, y))

            # Compute the target coordinate pairs
            target = []
            for (x, y) in source:
    
                # Note: This is the *only* part of this code that is
                # specific to a particular transform.  The rest of the
                # code doesn't care how the target pairs are derived
                # from the source ones.  We could do arbitrary matrix
                # algebra here, but beware the effects of hex-grid
                # transformation.
                if flip_x:
                    yn = y
                    xn = mx - x - 1
                if translate:
                    yn = y + dy
                    xn = x + dx

                # This is generic again
                target.append((xn, yn))
                if verbose:
                    print "(%d, %d) -> (%d, %d)" % (x, y, xn, yn)

            # Perform the actual transformation
            for (((xs, xe), (ys, ye)), (xn, yn)) in zip(pairs, target):
                content = content[:ys] + `yn` + content[ye:]
                content = content[:xs] + `xn` + content[xe:]

            fp = open(filename, "w")
            fp.write(content)
            fp.close()
