# Copyright (C) 2014 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.

from collections                      import namedtuple
from common.immutables                import ImmutableDict
from common.logger                    import Logger
from file_format.c1visualizer.struct  import C1visualizerFile, C1visualizerPass
from file_format.checker.struct       import CheckerFile, TestCase, TestAssertion
from match.line                       import MatchLines, EvaluateLine

MatchScope = namedtuple("MatchScope", ["start", "end"])
MatchInfo = namedtuple("MatchInfo", ["scope", "variables"])

class MatchFailedException(Exception):
  def __init__(self, assertion, lineNo, variables):
    self.assertion = assertion
    self.lineNo = lineNo
    self.variables = variables

def splitIntoGroups(assertions):
  """ Breaks up a list of assertions, grouping instructions which should be
      tested in the same scope (consecutive DAG and NOT instructions).
   """
  splitAssertions = []
  lastVariant = None
  for assertion in assertions:
    if (assertion.variant == lastVariant and
        assertion.variant in [TestAssertion.Variant.DAG, TestAssertion.Variant.Not]):
      splitAssertions[-1].append(assertion)
    else:
      splitAssertions.append([assertion])
      lastVariant = assertion.variant
  return splitAssertions

def findMatchingLine(assertion, c1Pass, scope, variables, excludeLines=[]):
  """ Finds the first line in `c1Pass` which matches `assertion`.

  Scan only lines numbered between `scope.start` and `scope.end` and not on the
  `excludeLines` list.

  Returns the index of the `c1Pass` line matching the assertion and variables
  values after the match.

  Raises MatchFailedException if no such `c1Pass` line can be found.
  """
  for i in range(scope.start, scope.end):
    if i in excludeLines: continue
    newVariables = MatchLines(assertion, c1Pass.body[i], variables)
    if newVariables is not None:
      return MatchInfo(MatchScope(i, i), newVariables)
  raise MatchFailedException(assertion, scope.start, variables)

def matchDagGroup(assertions, c1Pass, scope, variables):
  """ Attempts to find matching `c1Pass` lines for a group of DAG assertions.

  Assertions are matched in the list order and variable values propagated. Only
  lines in `scope` are scanned and each line can only match one assertion.

  Returns the range of `c1Pass` lines covered by this group (min/max of matching
  line numbers) and the variable values after the match of the last assertion.

  Raises MatchFailedException when an assertion cannot be satisfied.
  """
  matchedLines = []
  for assertion in assertions:
    assert assertion.variant == TestAssertion.Variant.DAG
    match = findMatchingLine(assertion, c1Pass, scope, variables, matchedLines)
    variables = match.variables
    assert match.scope.start == match.scope.end
    assert match.scope.start not in matchedLines
    matchedLines.append(match.scope.start)
  return MatchInfo(MatchScope(min(matchedLines), max(matchedLines)), variables)

def testNotGroup(assertions, c1Pass, scope, variables):
  """ Verifies that none of the given NOT assertions matches a line inside
      the given `scope` of `c1Pass` lines.

  Raises MatchFailedException if an assertion matches a line in the scope.
  """
  for i in range(scope.start, scope.end):
    line = c1Pass.body[i]
    for assertion in assertions:
      assert assertion.variant == TestAssertion.Variant.Not
      if MatchLines(assertion, line, variables) is not None:
        raise MatchFailedException(assertion, i, variables)

def testEvalGroup(assertions, scope, variables):
  for assertion in assertions:
    if not EvaluateLine(assertion, variables):
      raise MatchFailedException(assertion, scope.start, variables)

def MatchTestCase(testCase, c1Pass):
  """ Runs a test case against a C1visualizer graph dump.

  Raises MatchFailedException when an assertion cannot be satisfied.
  """
  assert testCase.name == c1Pass.name

  matchFrom = 0
  variables = ImmutableDict()
  c1Length = len(c1Pass.body)

  # NOT assertions are verified retrospectively, once the scope is known.
  pendingNotAssertions = None

  # Prepare assertions by grouping those that are verified in the same scope.
  # We also add None as an EOF assertion that will set scope for NOTs.
  assertionGroups = splitIntoGroups(testCase.assertions)
  assertionGroups.append(None)

  for assertionGroup in assertionGroups:
    if assertionGroup is None:
      # EOF marker always matches the last+1 line of c1Pass.
      match = MatchInfo(MatchScope(c1Length, c1Length), None)
    elif assertionGroup[0].variant == TestAssertion.Variant.Not:
      # NOT assertions will be tested together with the next group.
      assert not pendingNotAssertions
      pendingNotAssertions = assertionGroup
      continue
    elif assertionGroup[0].variant == TestAssertion.Variant.InOrder:
      # Single in-order assertion. Find the first line that matches.
      assert len(assertionGroup) == 1
      scope = MatchScope(matchFrom, c1Length)
      match = findMatchingLine(assertionGroup[0], c1Pass, scope, variables)
    elif assertionGroup[0].variant == TestAssertion.Variant.NextLine:
      # Single next-line assertion. Test if the current line matches.
      assert len(assertionGroup) == 1
      scope = MatchScope(matchFrom, matchFrom + 1)
      match = findMatchingLine(assertionGroup[0], c1Pass, scope, variables)
    elif assertionGroup[0].variant == TestAssertion.Variant.DAG:
      # A group of DAG assertions. Match them all starting from the same point.
      scope = MatchScope(matchFrom, c1Length)
      match = matchDagGroup(assertionGroup, c1Pass, scope, variables)
    else:
      assert assertionGroup[0].variant == TestAssertion.Variant.Eval
      scope = MatchScope(matchFrom, c1Length)
      testEvalGroup(assertionGroup, scope, variables)
      continue

    if pendingNotAssertions:
      # Previous group were NOT assertions. Make sure they don't match any lines
      # in the [matchFrom, match.start) scope.
      scope = MatchScope(matchFrom, match.scope.start)
      testNotGroup(pendingNotAssertions, c1Pass, scope, variables)
      pendingNotAssertions = None

    # Update state.
    assert matchFrom <= match.scope.end
    matchFrom = match.scope.end + 1
    variables = match.variables

def MatchFiles(checkerFile, c1File, targetArch, debuggableMode):
  for testCase in checkerFile.testCases:
    if testCase.testArch not in [None, targetArch]:
      continue
    if testCase.forDebuggable != debuggableMode:
      continue

    # TODO: Currently does not handle multiple occurrences of the same group
    # name, e.g. when a pass is run multiple times. It will always try to
    # match a check group against the first output group of the same name.
    c1Pass = c1File.findPass(testCase.name)
    if c1Pass is None:
      Logger.fail("Test case not found in the CFG file",
                  testCase.fileName, testCase.startLineNo, testCase.name)

    Logger.startTest(testCase.name)
    try:
      MatchTestCase(testCase, c1Pass)
      Logger.testPassed()
    except MatchFailedException as e:
      lineNo = c1Pass.startLineNo + e.lineNo
      if e.assertion.variant == TestAssertion.Variant.Not:
        msg = "NOT assertion matched line {}"
      else:
        msg = "Assertion could not be matched starting from line {}"
      msg = msg.format(lineNo)
      Logger.testFailed(msg, e.assertion, e.variables)