# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is the Python Computer Graphics Kit.
#
# The Initial Developer of the Original Code is Matthias Baas.
# Portions created by the Initial Developer are Copyright (C) 2004
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
# $Id: asfamc.py,v 1.3 2005/02/06 22:24:27 mbaas Exp $

## \file asfamc.py
## Contains the ASFReader and AMCReader class.

import string

# ReaderBase
class ReaderBase:
    """Base class for the ASFReader and AMCReader classes.

    A derived class must implement readXyz() method where Xyz is
    the section name (the keyword in the file is ':xyz').
    If such a read method is not implemented, the corresponding
    section is ignored.
    """

    def __init__(self, filename):

        self.filename = filename
        # A list of unprocessed tokens (strings)
        self.tokenlist = []
        # The last returned token (string)
        self.lasttok = None
        self.lastline = None
        self.undoline = None
        # The current line number
        self.linenr = 0

    # read
    def read(self):
        """Read the entire file.
        """
        self.fhandle = file(self.filename)

        while 1:
            try:
                tok = self.token()
            except StopIteration:
                break

            # Check if the keyword starts with ':' (a new section)
            if tok[0]!=":":
                raise SyntaxError, "Syntax error in line %d: Invalid keyword '%s'"%(self.linenr, tok)

            # Read the current section...

            # Determine the method name (only letters and digits allowed,
            # everything else will be removed)
            mn = tok[1:].capitalize()
            methodname = ""
            for c in mn:
                if c not in string.ascii_letters and c not in string.digits:
                    continue
                methodname += c

            handler = getattr(self, "read"+methodname, None)
            if handler!=None:
                handler()
            else:
                try:
                    self.skipSection()
                except:
                    break        

    # intToken
    def intToken(self):
        """Return the next token which must be an int.
        """

        tok = self.token()
        try:
            return int(tok)
        except ValueError:
            raise SyntaxError, "Syntax error in line %d: Integer expected, got '%s' instead"%(self.linenr, tok)

    # floatToken
    def floatToken(self):
        """Return the next token which must be a float.
        """

        tok = self.token()
        try:
            return float(tok)
        except ValueError:
            raise SyntaxError, "Syntax error in line %d: Float expected, got '%s' instead"%(self.linenr, tok)

    # token
    def token(self):
        """Return the next token."""

        # Are there still some tokens left? then just return the next one
        if self.tokenlist!=[]:
            tok = self.tokenlist[0]
            self.lasttok = tok
            self.tokenlist = self.tokenlist[1:]
            return tok

        # Read a new line
        s = self.readLine()
        self.createTokens(s)
        return self.token()

    # undoToken
    def undoToken(self):
        """Put back the last token.
        """
        self.tokenlist = [self.lasttok]+self.tokenlist
        self.lasttok = None

    # undoLine
    def undoLine(self):
        """Put back the last line.
        """
        self.undoline = self.lastline
        self.lastline = None
        self.tokenlist = []

    # skipSection
    def skipSection(self):
        """Read and ignore data until the next section is reached.

        The next call of token() will result in a ':' keyword.
        """
        while 1:
            s = self.readLine().strip()
            if s[0]==':':
                self.createTokens(s)
                return
        

    # readLine
    def readLine(self):
        """Return the next line containing data.

        Empty lines and comments are skipped. If the end of the file
        has been reached, a StopIteration exception is thrown.
        The return value is the next line containing data (this will
        never be an empty string).
        """
        # Discard any remaining tokens
        self.tokenlist = []

        # Is there a line that was undone?
        if self.undoline!=None:
            self.lastline = self.undoline
            self.undoline = None
            return self.lastline
        
        # Read the next line
        while 1:
            s = self.fhandle.readline()
            self.linenr += 1
            if s=="":
                raise StopIteration
            if s[0]!="#":
                self.lastline = s
                return s

    # createTokens
    def createTokens(self, s):
        """Populate the token list from the content of s.
        """
        s = s.strip()
        s = s.replace("(", " ")
        s = s.replace(")", " ")
        s = s.replace(",", " ")
        a = s.split()
        self.tokenlist = a

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

# ASFReader
class ASFReader(ReaderBase):
    """Read Acclaim Skeleton File Definition (ASF) files.

    This class serves as base class for reading ASF files.
    Derived classes may implement the following methods:

    - onVersion(ver)
    - onName(name)
    - onUnits(units)
    - onDocumentation(doc)
    - onRoot(data)
    - onBonedata(bones)
    - onHierarchy(links)
    """

    def __init__(self, filename):
        ReaderBase.__init__(self, filename)

    def onVersion(self, ver): pass
    def onName(self, name): pass
    def onUnits(self, units): pass
    def onDocumentation(self, doc): pass
    def onRoot(self, data): pass
    def onBonedata(self, bones): pass
    def onHierarchy(self, links): pass

    # readVersion
    def readVersion(self):
        ver = self.floatToken()
        self.onVersion(ver)

    # readName
    def readName(self):
        name = self.token()
        self.onName(name)

    # readUnits
    def readUnits(self):
        units = {}
        while 1:
            s = self.readLine()
            if s[0]==':':
                self.undoLine()
                self.onUnits(units)
                return
            a = s.strip().split()
            # Get the argument
            if len(a)>1:
                val = a[1]
            # Try to convert to float. If it doesn't work, use the string value
            try:
                val = float(val)
            except ValueError:
                pass
            units[a[0]] = val

    # readDocumentation
    def readDocumentation(self):
        lines = []
        while 1:
            s = self.readLine()
            if s[0]==':':
                self.undoLine()
                doc = "\n".join(lines)
                self.onDocumentation(doc)
                return
            lines += [s[:-1]]

    # readRoot
    def readRoot(self):
        data = {}
        while 1:
            s = self.readLine()
            if s[0]==':':
                self.undoLine()
                self.onRoot(data)
                return
            a = s.strip().split()
            val = tuple(a[1:])
            data[a[0]] = val
    
    # readBonedata
    def readBonedata(self):
        bones = []
        while 1:
            stop = False
            try:
                # Get the next token
                begintok = self.token()
            except StopIteration:
                stop = True

            # Is it already the begin of the next section?
            if begintok[0]==':':
                self.undoToken()
                stop = True

            # End of file or next section? -> no more bone data
            if stop:
                self.onBonedata(bones)
                return
            
            if begintok!="begin":
                raise SyntaxError, "Syntax error in line %d: 'begin' expected, got '%s'"%(self.linenr, begintok)

            data = {}

            while 1:
                s = self.readLine().strip()
                if s=='end':
                    break
                a = s.strip().split()
                # Check for 'limits' which is a special case as it might
                # span several lines.
                if a[0]=="limits":
                    if not data.has_key("dof"):
                        raise ValueError, "Line %d: 'dof' settings must appear before the 'limits' settings"%(self.linenr)
                    dof = data["dof"]
                    # Put back the line and use the token mechanism
                    self.undoLine()
                    # Read the 'limits' token
                    tok = self.token()
                    limits = []
                    for i in range(len(dof)):
                        minval = self.token()
                        maxval = self.token()
                        if minval.lower()!="-inf":
                            try:
                                minval = float(minval)
                            except ValueError:
                                raise SyntaxError, "Syntax error in line %d: Float or '-inf' expected, got '%s'"%(self.linenr, minval)
                        if maxval.lower()!="inf":
                            try:
                                maxval = float(maxval)
                            except ValueError:
                                raise SyntaxError, "Syntax error in line %d: Float or 'inf' expected, got '%s'"%(self.linenr, maxval)
                        limits.append((minval,maxval))
                    data["limits"] = limits
                else:
                    val = tuple(a[1:])
                    data[a[0]] = val

            bones.append(data)

    # readHierarchy
    def readHierarchy(self):
        links = []
        while 1:
            stop = False
            try:
                # Get the next token
                begintok = self.token()
            except StopIteration:
                stop = True
            
            # Is it already the begin of the next section?
            if begintok[0]==':':
                self.undoToken()
                stop = True

            # End of file or next section? -> no more bone data
            if stop:
                self.onHierarchy(links)
                return

            if begintok!="begin":
                raise SyntaxError, "Syntax error in line %d: 'begin' expected, got '%s'"%(self.linenr, begintok)

            while 1:
                s = self.readLine().strip()
                if s=='end':
                    break
                a = s.strip().split()
                links.append((a[0], a[1:]))

# AMCReader
class AMCReader(ReaderBase):
    """Read Acclaim Motion Capture Data (AMC) files.

    This class serves as base class for reading AMC files.
    Derived classes have to implement the following method:

    - onFrame(framenr, data)

    """

    def __init__(self, filename):
        ReaderBase.__init__(self, filename)

    def onFrame(self, framenr, data):
        pass
#        print framenr, data

    # read
    def read(self):
        self.fhandle = file(self.filename)

        while 1:
            try:
                tok = self.token()
            except StopIteration:
                return

            # Check if the keyword starts with ':'. If it does, ignore it
            if tok[0]!=":":
                self.undoToken()
                break

        # Read the motion data...
        while 1:
            try:
                framenr = self.intToken()
            except StopIteration:
                return

            data = []
            while 1:
                try:
                    s = self.readLine()
                except StopIteration:
                    self.onFrame(framenr, data)
                    return
                
                a = s.strip().split()
                # Check if it was a frame number
                try:
                    int(a[0])
                    self.undoLine()
                    self.onFrame(framenr, data)
                    break
                except ValueError:
                    pass
                bone = a[0]
                values = map(lambda x: float(x), a[1:])
                data.append((bone, values))


