# -*- coding: utf-8 -*-

#-------------------------------------------------------------------------
# drawElements Quality Program utilities
# --------------------------------------
#
# Copyright 2015 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#-------------------------------------------------------------------------

import shlex
import sys
import xml.dom.minidom

class StatusCode:
    PASS = 'Pass'
    FAIL = 'Fail'
    QUALITY_WARNING = 'QualityWarning'
    COMPATIBILITY_WARNING = 'CompatibilityWarning'
    PENDING = 'Pending'
    NOT_SUPPORTED = 'NotSupported'
    RESOURCE_ERROR = 'ResourceError'
    INTERNAL_ERROR = 'InternalError'
    CRASH = 'Crash'
    TIMEOUT = 'Timeout'

    STATUS_CODES = [
        PASS,
        FAIL,
        QUALITY_WARNING,
        COMPATIBILITY_WARNING,
        PENDING,
        NOT_SUPPORTED,
        RESOURCE_ERROR,
        INTERNAL_ERROR,
        CRASH,
        TIMEOUT
        ]
    STATUS_CODE_SET = set(STATUS_CODES)

    @staticmethod
    def isValid (code):
        return code in StatusCode.STATUS_CODE_SET

class TestCaseResult:
    def __init__ (self, name, statusCode, statusDetails, log):
        self.name = name
        self.statusCode = statusCode
        self.statusDetails = statusDetails
        self.log = log

    def __str__ (self):
        return "%s: %s (%s)" % (self.name, self.statusCode, self.statusDetails)

class ParseError(Exception):
    def __init__ (self, filename, line, message):
        self.filename = filename
        self.line = line
        self.message = message

    def __str__ (self):
        return "%s:%d: %s" % (self.filename, self.line, self.message)

def splitContainerLine (line):
    if sys.version_info > (3, 0):
        # In Python 3, shlex works better with unicode.
        return shlex.split(line)
    else:
        # In Python 2, shlex works better with bytes, so encode and decode again upon return.
        return [w.decode('utf-8') for w in shlex.split(line.encode('utf-8'))]

def getNodeText (node):
    rc = []
    for node in node.childNodes:
        if node.nodeType == node.TEXT_NODE:
            rc.append(node.data)
    return ''.join(rc)

class BatchResultParser:
    def __init__ (self):
        pass

    def parseFile (self, filename):
        self.init(filename)

        f = open(filename, 'rb')
        for line in f:
            self.parseLine(line)
            self.curLine += 1
        f.close()

        return self.testCaseResults

    def getNextTestCaseResult (self, file):
        try:
            del self.testCaseResults[:]
            self.curResultText = None

            isNextResult = self.parseLine(next(file))
            while not isNextResult:
                isNextResult = self.parseLine(next(file))

            # Return the next TestCaseResult
            return self.testCaseResults.pop()

        except StopIteration:
            # If end of file was reached and there is no log left, the parsing finished successful (return None).
            # Otherwise, if there is still log to be parsed, it means that there was a crash.
            if self.curResultText:
                return TestCaseResult(self.curCaseName, StatusCode.CRASH, StatusCode.CRASH, self.curResultText)
            else:
                return None

    def init (self, filename):
        # Results
        self.sessionInfo = []
        self.testCaseResults = []

        # State
        self.curResultText = None
        self.curCaseName = None

        # Error context
        self.curLine = 1
        self.filename = filename

    def parseLine (self, line):
        # Some test shaders contain invalid characters.
        text = line.decode('utf-8', 'ignore')
        if len(text) > 0 and text[0] == '#':
            return self.parseContainerLine(line)
        elif self.curResultText != None:
            self.curResultText += line
            return None
        # else: just ignored

    def parseContainerLine (self, line):
        isTestCaseResult = False
        # Some test shaders contain invalid characters.
        text = line.decode('utf-8', 'ignore')
        args = splitContainerLine(text)
        if args[0] == "#sessionInfo":
            if len(args) < 3:
                print(args)
                self.parseError("Invalid #sessionInfo")
            self.sessionInfo.append((args[1], ' '.join(args[2:])))
        elif args[0] == "#beginSession" or args[0] == "#endSession":
            pass # \todo [pyry] Validate
        elif args[0] == "#beginTestCaseResult":
            if len(args) != 2 or self.curCaseName != None:
                self.parseError("Invalid #beginTestCaseResult")
            self.curCaseName = args[1]
            self.curResultText = b""
        elif args[0] == "#endTestCaseResult":
            if len(args) != 1 or self.curCaseName == None:
                self.parseError("Invalid #endTestCaseResult")
            self.parseTestCaseResult(self.curCaseName, self.curResultText)
            self.curCaseName = None
            self.curResultText = None
            isTestCaseResult = True
        elif args[0] == "#terminateTestCaseResult":
            if len(args) < 2 or self.curCaseName == None:
                self.parseError("Invalid #terminateTestCaseResult")
            statusCode = ' '.join(args[1:])
            statusDetails = statusCode

            if not StatusCode.isValid(statusCode):
                # Legacy format
                if statusCode == "Watchdog timeout occurred.":
                    statusCode = StatusCode.TIMEOUT
                else:
                    statusCode = StatusCode.CRASH

            # Do not try to parse at all since XML is likely broken
            self.testCaseResults.append(TestCaseResult(self.curCaseName, statusCode, statusDetails, self.curResultText))

            self.curCaseName = None
            self.curResultText = None
            isTestCaseResult = True
        else:
            # Assume this is result text
            if self.curResultText != None:
                self.curResultText += line

        return isTestCaseResult

    def parseTestCaseResult (self, name, log):
        try:
            # The XML parser has troubles with invalid characters deliberately included in the shaders.
            # This line removes such characters before calling the parser
            log = log.decode('utf-8','ignore').encode("utf-8")
            doc = xml.dom.minidom.parseString(log)
            resultItems = doc.getElementsByTagName('Result')
            if len(resultItems) != 1:
                self.parseError("Expected 1 <Result>, found %d" % len(resultItems))

            statusCode = resultItems[0].getAttributeNode('StatusCode').nodeValue
            statusDetails = getNodeText(resultItems[0])
        except Exception as e:
            statusCode = StatusCode.INTERNAL_ERROR
            statusDetails = "XML parsing failed: %s" % str(e)

        self.testCaseResults.append(TestCaseResult(name, statusCode, statusDetails, log))

    def parseError (self, message):
        raise ParseError(self.filename, self.curLine, message)
