#!/usr/bin/env python
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Write a Mochitest manifest for WebGL conformance test files.

import os
import re

# All paths in this file are based where this file is run.
WRAPPER_TEMPLATE_FILE = 'mochi-wrapper.html.template'
MANIFEST_TEMPLATE_FILE = 'mochitest.ini.template'
ERRATA_FILE = 'mochitest-errata.ini'
DEST_MANIFEST_PATHSTR = 'generated-mochitest.ini'

BASE_TEST_LIST_PATHSTR = 'checkout/00_test_list.txt'
GENERATED_PATHSTR = 'generated'
WEBGL2_TEST_MANGLE = '2_'
PATH_SEP_MANGLING = '__'
WEBGL2_SKIP_IF_CONDITION = "(os == 'android' || os == 'linux')"

SUPPORT_DIRS = [
    'checkout',
]

EXTRA_SUPPORT_FILES = [
    'always-fail.html',
    'iframe-passthrough.css',
    'mochi-single.html',
]

ACCEPTABLE_ERRATA_KEYS = set([
  'fail-if',
  'skip-if',
  'subsuite',
])

########################################################################
# GetTestList

def GetTestList():
    split = BASE_TEST_LIST_PATHSTR.rsplit('/', 1)
    basePath = '.'
    testListFile = split[-1]
    if len(split) == 2:
        basePath = split[0]

    allowWebGL1 = True
    allowWebGL2 = True
    alwaysFailEntry = TestEntry('always-fail.html', True, False)
    testList = [alwaysFailEntry]
    AccumTests(basePath, testListFile, allowWebGL1, allowWebGL2, testList)

    for x in testList:
        x.path = os.path.relpath(x.path, basePath).replace(os.sep, '/')
        continue

    return testList

##############################
# Internals

def IsVersionLess(a, b):
    aSplit = [int(x) for x in a.split('.')]
    bSplit = [int(x) for x in b.split('.')]

    while len(aSplit) < len(bSplit):
        aSplit.append(0)

    while len(aSplit) > len(bSplit):
        bSplit.append(0)

    for i in range(len(aSplit)):
        aVal = aSplit[i]
        bVal = bSplit[i]

        if aVal == bVal:
            continue

        return aVal < bVal

    return False

class TestEntry:
    def __init__(self, path, webgl1, webgl2):
        self.path = path
        self.webgl1 = webgl1
        self.webgl2 = webgl2
        return


def AccumTests(pathStr, listFile, allowWebGL1, allowWebGL2, out_testList):
    listPathStr = pathStr + '/' + listFile

    listPath = listPathStr.replace('/', os.sep)
    assert os.path.exists(listPath), 'Bad `listPath`: ' + listPath

    with open(listPath, 'rb') as fIn:
        lineNum = 0
        for line in fIn:
            lineNum += 1

            line = line.rstrip()
            if not line:
                continue

            curLine = line.lstrip()
            if curLine.startswith('//'):
                continue
            if curLine.startswith('#'):
                continue

            webgl1 = allowWebGL1
            webgl2 = allowWebGL2
            while curLine.startswith('--'): # '--min-version 1.0.2 foo.html'
                (flag, curLine) = curLine.split(' ', 1)
                if flag == '--min-version':
                    (minVersion, curLine) = curLine.split(' ', 1)
                    if not IsVersionLess(minVersion, "2.0.0"): # >= 2.0.0
                        webgl1 = False
                        break
                elif flag == '--max-version':
                    (maxVersion, curLine) = curLine.split(' ', 1)
                    if IsVersionLess(maxVersion, "2.0.0"):
                        webgl2 = False
                        break
                elif flag == '--slow':
                    continue # TODO
                else:
                    text = 'Unknown flag \'{}\': {}:{}: {}'.format(flag, listPath,
                                                                   lineNum, line)
                    assert False, text
                continue

            assert(webgl1 or webgl2)

            split = curLine.rsplit('.', 1)
            assert len(split) == 2, 'Bad split for `line`: ' + line
            (name, ext) = split

            if ext == 'html':
                newTestFilePathStr = pathStr + '/' + curLine
                entry = TestEntry(newTestFilePathStr, webgl1, webgl2)
                out_testList.append(entry)
                continue

            assert ext == 'txt', 'Bad `ext` on `line`: ' + line

            split = curLine.rsplit('/', 1)
            nextListFile = split[-1]
            nextPathStr = ''
            if len(split) != 1:
                nextPathStr = split[0]

            nextPathStr = pathStr + '/' + nextPathStr
            AccumTests(nextPathStr, nextListFile, webgl1, webgl2, out_testList)
            continue

    return

########################################################################
# Templates

def FillTemplate(inFilePath, templateDict, outFilePath):
    templateShell = ImportTemplate(inFilePath)
    OutputFilledTemplate(templateShell, templateDict, outFilePath)
    return


def ImportTemplate(inFilePath):
    with open(inFilePath, 'rb') as f:
        return TemplateShell(f)


def OutputFilledTemplate(templateShell, templateDict, outFilePath):
    spanStrList = templateShell.Fill(templateDict)

    with open(outFilePath, 'wb') as f:
        f.writelines(spanStrList)
    return

##############################
# Internals

def WrapWithIndent(lines, indentLen):
  split = lines.split('\n')
  if len(split) == 1:
      return lines

  ret = [split[0]]
  indentSpaces = ' ' * indentLen
  for line in split[1:]:
      ret.append(indentSpaces + line)

  return '\n'.join(ret)


templateRE = re.compile('(%%.*?%%)')
assert templateRE.split('  foo = %%BAR%%;') == ['  foo = ', '%%BAR%%', ';']


class TemplateShellSpan:
    def __init__(self, span):
        self.span = span

        self.isLiteralSpan = True
        if self.span.startswith('%%') and self.span.endswith('%%'):
            self.isLiteralSpan = False
            self.span = self.span[2:-2]

        return


    def Fill(self, templateDict, indentLen):
        if self.isLiteralSpan:
            return self.span

        assert self.span in templateDict, '\'' + self.span + '\' not in dict!'

        filling = templateDict[self.span]

        return WrapWithIndent(filling, indentLen)


class TemplateShell:
    def __init__(self, iterableLines):
        spanList = []
        curLiteralSpan = []
        for line in iterableLines:
            split = templateRE.split(line)

            for cur in split:
                isTemplateSpan = cur.startswith('%%') and cur.endswith('%%')
                if not isTemplateSpan:
                    curLiteralSpan.append(cur)
                    continue

                if curLiteralSpan:
                    span = ''.join(curLiteralSpan)
                    span = TemplateShellSpan(span)
                    spanList.append(span)
                    curLiteralSpan = []

                assert len(cur) >= 4

                span = TemplateShellSpan(cur)
                spanList.append(span)
                continue
            continue

        if curLiteralSpan:
            span = ''.join(curLiteralSpan)
            span = TemplateShellSpan(span)
            spanList.append(span)

        self.spanList = spanList
        return


    # Returns spanStrList.
    def Fill(self, templateDict):
        indentLen = 0
        ret = []
        for span in self.spanList:
            span = span.Fill(templateDict, indentLen)
            ret.append(span)

            # Get next `indentLen`.
            try:
                lineStartPos = span.rindex('\n') + 1

                # let span = 'foo\nbar'
                # len(span) is 7
                # lineStartPos is 4
                indentLen = len(span) - lineStartPos
            except ValueError:
                indentLen += len(span)
            continue

        return ret

########################################################################
# Output

def IsWrapperWebGL2(wrapperPath):
    return wrapperPath.startswith(GENERATED_PATHSTR + '/test_' + WEBGL2_TEST_MANGLE)


def WriteWrapper(entryPath, webgl2, templateShell, wrapperPathAccum):
    mangledPath = entryPath.replace('/', PATH_SEP_MANGLING)
    maybeWebGL2Mangle = ''
    if webgl2:
        maybeWebGL2Mangle = WEBGL2_TEST_MANGLE

    # Mochitests must start with 'test_' or similar, or the test
    # runner will ignore our tests.
    # The error text is "is not a valid test".
    wrapperFileName = 'test_' + maybeWebGL2Mangle + mangledPath

    wrapperPath = GENERATED_PATHSTR + '/' + wrapperFileName
    print('Adding wrapper: ' + wrapperPath)

    args = ''
    if webgl2:
        args = '?webglVersion=2'

    templateDict = {
        'TEST_PATH': entryPath,
        'ARGS': args,
    }

    OutputFilledTemplate(templateShell, templateDict, wrapperPath)

    if webgl2:
        assert IsWrapperWebGL2(wrapperPath)

    wrapperPathAccum.append(wrapperPath)
    return


def WriteWrappers(testEntryList):
    templateShell = ImportTemplate(WRAPPER_TEMPLATE_FILE)

    generatedDirPath = GENERATED_PATHSTR.replace('/', os.sep)
    if not os.path.exists(generatedDirPath):
        os.mkdir(generatedDirPath)
    assert os.path.isdir(generatedDirPath)

    wrapperPathList = []
    for entry in testEntryList:
        if entry.webgl1:
            WriteWrapper(entry.path, False, templateShell, wrapperPathList)
        if entry.webgl2:
            WriteWrapper(entry.path, True, templateShell, wrapperPathList)
        continue

    print('{} wrappers written.\n'.format(len(wrapperPathList)))
    return wrapperPathList


kManifestRelPathStr = os.path.relpath('.', os.path.dirname(DEST_MANIFEST_PATHSTR))
kManifestRelPathStr = kManifestRelPathStr.replace(os.sep, '/')

def ManifestPathStr(pathStr):
    pathStr = kManifestRelPathStr + '/' + pathStr
    return os.path.normpath(pathStr).replace(os.sep, '/')


def WriteManifest(wrapperPathStrList, supportPathStrList):
    destPathStr = DEST_MANIFEST_PATHSTR
    print 'Generating manifest: ' + destPathStr

    errataMap = LoadErrata()

    # DEFAULT_ERRATA
    defaultSectionName = 'DEFAULT'

    defaultSectionLines = []
    if defaultSectionName in errataMap:
        defaultSectionLines = errataMap[defaultSectionName]
        del errataMap[defaultSectionName]

    defaultSectionStr = '\n'.join(defaultSectionLines)

    # SUPPORT_FILES
    supportPathStrList = [ManifestPathStr(x) for x in supportPathStrList]
    supportPathStrList = sorted(supportPathStrList)
    supportFilesStr = '\n'.join(supportPathStrList)

    # MANIFEST_TESTS
    manifestTestLineList = []
    wrapperPathStrList = sorted(wrapperPathStrList)
    for wrapperPathStr in wrapperPathStrList:
        #print 'wrapperPathStr: ' + wrapperPathStr

        wrapperManifestPathStr = ManifestPathStr(wrapperPathStr)
        sectionName = '[' + wrapperManifestPathStr + ']'
        manifestTestLineList.append(sectionName)

        errataLines = []
        if wrapperPathStr in errataMap:
            errataLines = errataMap[wrapperPathStr]
            del errataMap[wrapperPathStr]

        if IsWrapperWebGL2(wrapperPathStr):
            needsSkip = True
            for i in range(len(errataLines)):
                if errataLines[i].startswith('skip-if'):
                    errataLines[i] += ' || ' + WEBGL2_SKIP_IF_CONDITION
                    needsSkip = False
                continue

            if needsSkip:
                errataLines.append('skip-if = ' + WEBGL2_SKIP_IF_CONDITION)

        manifestTestLineList += errataLines
        continue

    if errataMap:
        print 'Errata left in map:'
        for x in errataMap.keys():
            print ' '*4 + x
        assert False

    manifestTestsStr = '\n'.join(manifestTestLineList)

    # Fill the template.
    templateDict = {
        'DEFAULT_ERRATA': defaultSectionStr,
        'SUPPORT_FILES': supportFilesStr,
        'MANIFEST_TESTS': manifestTestsStr,
    }

    destPath = destPathStr.replace('/', os.sep)
    FillTemplate(MANIFEST_TEMPLATE_FILE, templateDict, destPath)
    return

##############################
# Internals

kManifestHeaderRegex = re.compile(r'[[]([^]]*)[]]')

def LoadINI(path):
    curSectionName = None
    curSectionMap = {}

    lineNum = 0

    ret = {}
    ret[curSectionName] = (lineNum, curSectionMap)

    with open(path, 'rb') as f:
        for line in f:
            lineNum += 1

            line = line.strip()
            if not line:
                continue

            if line[0] in [';', '#']:
                continue

            if line[0] == '[':
                assert line[-1] == ']', '{}:{}'.format(path, lineNum)

                curSectionName = line[1:-1]
                assert curSectionName not in ret, 'Line {}: Duplicate section: {}'.format(lineNum, line)

                curSectionMap = {}
                ret[curSectionName] = (lineNum, curSectionMap)
                continue

            split = line.split('=', 1)
            key = split[0].strip()
            val = ''
            if len(split) == 2:
                val = split[1].strip()

            curSectionMap[key] = (lineNum, val)
            continue

    return ret


def LoadErrata():
    iniMap = LoadINI(ERRATA_FILE)

    ret = {}

    for (sectionName, (sectionLineNum, sectionMap)) in iniMap.iteritems():
        curLines = []

        if sectionName == None:
            continue
        elif sectionName != 'DEFAULT':
            path = sectionName.replace('/', os.sep)
            assert os.path.exists(path), 'Errata line {}: Invalid file: {}'.format(sectionLineNum, sectionName)

        for (key, (lineNum, val)) in sectionMap.iteritems():
            assert key in ACCEPTABLE_ERRATA_KEYS, 'Line {}: {}'.format(lineNum, key)

            curLine = '{} = {}'.format(key, val)
            curLines.append(curLine)
            continue

        ret[sectionName] = curLines
        continue

    return ret

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

def GetSupportFileList():
    ret = EXTRA_SUPPORT_FILES[:]

    for pathStr in SUPPORT_DIRS:
        ret += GetFilePathListForDir(pathStr)
        continue

    for pathStr in ret:
        path = pathStr.replace('/', os.sep)
        assert os.path.exists(path), path + '\n\n\n' + 'pathStr: ' + str(pathStr)
        continue

    return ret


def GetFilePathListForDir(baseDir):
    ret = []
    for root, folders, files in os.walk(baseDir):
        for f in files:
            filePath = os.path.join(root, f)
            filePath = filePath.replace(os.sep, '/')
            ret.append(filePath)

    return ret


if __name__ == '__main__':
    fileDir = os.path.dirname(__file__)
    assert not fileDir, 'Run this file from its directory, not ' + fileDir

    testEntryList = GetTestList()
    wrapperPathStrList = WriteWrappers(testEntryList)

    supportPathStrList = GetSupportFileList()
    WriteManifest(wrapperPathStrList, supportPathStrList)

    print('Done!')
