/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

package org.apache.daffodil.grammar.primitives

import org.apache.daffodil.grammar._
import org.apache.daffodil.dsom._
import org.apache.daffodil.dpath._
import org.apache.daffodil.xml.XMLUtils
import org.apache.daffodil.xml.GlobalQName
import org.apache.daffodil.processors.parsers.{ Parser => DaffodilParser }
import org.apache.daffodil.processors.unparsers.{ Unparser => DaffodilUnparser }
import org.apache.daffodil.exceptions.Assert
import org.apache.daffodil.processors.parsers.NewVariableInstanceStartParser
import org.apache.daffodil.processors.parsers.AssertExpressionEvaluationParser
import org.apache.daffodil.dsom.ElementBase
import org.apache.daffodil.processors.parsers.NewVariableInstanceEndParser
import org.apache.daffodil.processors.parsers.SetVariableParser
import org.apache.daffodil.processors.parsers.IVCParser
import org.apache.daffodil.processors.unparsers.SetVariableUnparser
import org.apache.daffodil.processors.unparsers.NewVariableInstanceEndUnparser
import org.apache.daffodil.processors.unparsers.NewVariableInstanceStartUnparser
import org.apache.daffodil.compiler.ForParser
import org.apache.daffodil.schema.annotation.props.PropertyLookupResult
import org.apache.daffodil.schema.annotation.props.Found
import org.apache.daffodil.schema.annotation.props.gen.FailureType
import org.apache.daffodil.dsom.ExpressionCompilers
import org.apache.daffodil.dsom.DFDLSetVariable
import org.apache.daffodil.dsom.DFDLNewVariableInstance
import org.apache.daffodil.processors.parsers.AssertPatternParser
import org.apache.daffodil.processors.parsers.TypeValueCalcParser
import org.apache.daffodil.processors.unparsers.TypeValueCalcUnparser
import org.apache.daffodil.processors.parsers.InitiatedContentDiscrimOnIndexGreaterThanMinParser
import org.apache.daffodil.processors.parsers.InitiatedContentDiscrimChoiceParser
import org.apache.daffodil.processors.parsers.InitiatedContentDiscrimChoiceAndIndexGreaterThanMinParser
import org.apache.daffodil.processors.parsers.InitiatedContentDiscrimChoiceOnlyOnFirstIndexParser

abstract class AssertBase(
  decl: AnnotatedSchemaComponent,
  exprWithBraces: String,
  namespacesForNamespaceResolution: scala.xml.NamespaceBinding,
  scWherePropertyWasLocated: AnnotatedSchemaComponent,
  msgOpt: Option[String],
  discrim: Boolean, // are we a discriminator or not.
  assertKindName: String,
  failureType: FailureType)
  extends ExpressionEvaluatorBase(scWherePropertyWasLocated) {

  def this(
    decl: AnnotatedSchemaComponent,
    foundProp: Found,
    msgOpt: Option[String],
    discrim: Boolean, // are we a discriminator or not.
    assertKindName: String,
    failureType: FailureType) =
    this(decl, foundProp.value, foundProp.location.namespaces, decl, msgOpt, discrim, assertKindName, failureType)

  override val baseName = assertKindName
  override lazy val exprText = exprWithBraces
  override lazy val exprNamespaces = namespacesForNamespaceResolution
  override lazy val exprComponent = scWherePropertyWasLocated
  override def nodeKind = NodeInfo.Boolean

  override val forWhat = ForParser

  lazy val msgExpr = {
    if (msgOpt.isDefined) {
      ExpressionCompilers.String.compileExpression(
        qn,
        NodeInfo.String, msgOpt.get, exprNamespaces, exprComponent.dpathCompileInfo, false, this, exprComponent.dpathCompileInfo)
    } else {
      new ConstantExpression[String](qn, NodeInfo.String, exprWithBraces + " failed")
    }
  }

  lazy val parser: DaffodilParser = new AssertExpressionEvaluationParser(msgExpr, discrim, decl.runtimeData, expr, failureType)

  override def unparser: DaffodilUnparser = hasNoUnparser

}

abstract class AssertBooleanPrimBase(
  decl: AnnotatedSchemaComponent,
  stmt: DFDLAssertionBase,
  discrim: Boolean, // are we a discriminator or not.
  assertKindName: String) extends AssertBase(decl, Found(stmt.testTxt, stmt, "test", false), stmt.messageAttrib, discrim, assertKindName, stmt.failureType)

case class AssertBooleanPrim(
  decl: AnnotatedSchemaComponent,
  stmt: DFDLAssertionBase)
  extends AssertBooleanPrimBase(decl, stmt, false, "assert") {
}

case class DiscriminatorBooleanPrim(
  decl: AnnotatedSchemaComponent,
  stmt: DFDLAssertionBase)
  extends AssertBooleanPrimBase(decl, stmt, true, "discriminator")

// TODO: performance wise, initiated content is supposed to be faster
// than evaluating an expression. There should be a better way to say
// "resolve this point of uncertainty" without having to introduce
// an XPath evaluator that runs fn:true() expression.
case class InitiatedContent(
  mg: ModelGroup,
  t: Term)
  extends Terminal(t, true) {

  override val forWhat = ForParser

  override def parser = {
    // Each of the parsers that this could create always appear immediately
    // after an initiator parser. So they can all assume that the previous
    // initiator was successfully parsed and thus resolve points of uncertainty
    // appropriately
 
    t match {
      case eb: ElementBase if eb.isArray => {
        (mg, eb.optPoUMinOccurs) match {
          case (sq: SequenceTermBase, Some(min)) => {
            // This is an array of elements inside a sequence with initiated
            // content. There is no PoU if occursIndex <= min. All elements
            // after that have a PoU, which are resolved by this parser. Note
            // that min here is the value of optPoUMinOccurs, which is slightly
            // different than value of minOccurs, e.g. when occursCountKind="parsed"
            // this value is zero rather than the value of minOccurs.
            new InitiatedContentDiscrimOnIndexGreaterThanMinParser(min, eb.erd)
          }
          case (ch: ChoiceTermBase, None) => {
            // This branch of a choice is a fixed number of array elements.
            // Because they are fixed, there is no PoU associated with the
            // elements, only the choice. This must resolve the choice PoU only
            // the first time so as not to resolve any other PoUs.
            new InitiatedContentDiscrimChoiceOnlyOnFirstIndexParser(eb.erd)
          }
          case (ch: ChoiceTermBase, Some(min)) => {
            // This branch of a choice is for an array of elements with a
            // minimum number of occurrences. When occursIndex <= min, there
            // are no PoU's for the array elements, so this parser only
            // resolves PoU's once that index has been reached. However, there
            // is still a PoU for the choice, which must only be resolved once
            // when we parsed the first array element. Note that min here is
            // the value of optPoUMinOccurs, which is slightly different than
            // the value of minOccurs, e.g. when occursCountKind="parsed" this
            // value is zero rather than the value of minOccurs.
            new InitiatedContentDiscrimChoiceAndIndexGreaterThanMinParser(min, eb.erd)
          }
          case x => Assert.invariantFailed("Guard should exclude this case: " + x)
        }
      }
      case _ => new InitiatedContentDiscrimChoiceParser(t.termRuntimeData)
    }
  }

  override def unparser = hasNoUnparser
}

case class SetVariable(stmt: DFDLSetVariable)
  extends ExpressionEvaluatorBase(stmt.annotatedSC) {

  val baseName = "SetVariable[" + stmt.varQName.local + "]"

  override lazy val exprText = stmt.value
  override lazy val exprNamespaces = stmt.xml.scope
  override lazy val exprComponent = stmt

  override lazy val nodeKind = stmt.defv.primType

  lazy val parser: DaffodilParser = new SetVariableParser(expr, stmt.defv.runtimeData)
  override lazy val unparser: DaffodilUnparser = new SetVariableUnparser(expr, stmt.defv.runtimeData, stmt.nonTermRuntimeData)
}

abstract class NewVariableInstanceBase(decl: AnnotatedSchemaComponent, stmt: DFDLNewVariableInstance)
  extends Terminal(decl, true) {
}

case class NewVariableInstanceStart(decl: AnnotatedSchemaComponent, stmt: DFDLNewVariableInstance)
  extends NewVariableInstanceBase(decl, stmt) {

  lazy val parser: DaffodilParser = new NewVariableInstanceStartParser(stmt.variableRuntimeData)
  override lazy val unparser: DaffodilUnparser = new NewVariableInstanceStartUnparser(stmt.variableRuntimeData)
}

case class NewVariableInstanceEnd(decl: AnnotatedSchemaComponent, stmt: DFDLNewVariableInstance)
  extends NewVariableInstanceBase(decl, stmt) {

  lazy val parser: DaffodilParser = new NewVariableInstanceEndParser(stmt.variableRuntimeData)
  override lazy val unparser: DaffodilUnparser = new NewVariableInstanceEndUnparser(stmt.variableRuntimeData)
}

/**
 * Refactored primitives that use expressions to put expression evaluation in one place.
 * On this base (for the primitive), and a corresponding parser base class for the
 * actual evaluation.
 *
 * That fixed a bug where a SDE wasn't being reported until the parser was run that
 * could have been reported at compilation time.
 *
 * Anything being computed that involves the dsom or grammar objects or attributes of them,
 * should be done in the grammar primitives, and NOT in the parser.
 * This is important to insure errors are captured at compilation time and
 * reported on relevant objects.
 */
abstract class ExpressionEvaluatorBase(e: AnnotatedSchemaComponent) extends Terminal(e, true) {
  override def toString = baseName + "(" + exprText + ")"

  def baseName: String
  def exprNamespaces: scala.xml.NamespaceBinding
  def exprComponent: SchemaComponent
  def exprText: String

  def nodeKind: NodeInfo.Kind

  protected def qn = GlobalQName(Some("daf"), baseName, XMLUtils.dafintURI)

  lazy val expr = LV('expr) {
    ExpressionCompilers.AnyRef.compileExpression(
      qn,
      nodeKind, exprText, exprNamespaces, exprComponent.dpathCompileInfo, false, this, exprComponent.dpathCompileInfo)
  }.value
}

abstract class ValueCalcBase(
  e: ElementBase,
  property: PropertyLookupResult)
  extends ExpressionEvaluatorBase(e) {

  override lazy val exprText = exprProp.value
  override lazy val exprNamespaces = exprProp.location.namespaces
  override lazy val exprComponent = exprProp.location.asInstanceOf[SchemaComponent]

  lazy val pt = e.primType //.typeRuntimeData
  override lazy val nodeKind = pt
  lazy val ptn = pt.name

  lazy val exprProp = property.asInstanceOf[Found]

}

case class InputValueCalc(
  e: ElementBase,
  property: PropertyLookupResult)
  extends ValueCalcBase(e, property) {

  override def baseName = "inputValueCalc"

  override lazy val parser: DaffodilParser = {
    new IVCParser(expr, e.elementRuntimeData)
  }

  override lazy val unparser = Assert.usageError("Not to be called on InputValueCalc class.")
}

case class TypeValueCalc(e: ElementBase)
  extends Terminal(e, e.hasRepType) {

  private lazy val simpleTypeDefBase = e.simpleType.asInstanceOf[SimpleTypeDefBase]
  private lazy val typeCalculator = simpleTypeDefBase.optTypeCalculator.get
  private lazy val repTypeRuntimeData = simpleTypeDefBase.optRepTypeElement.get.elementRuntimeData
  private lazy val repTypeParser = simpleTypeDefBase.optRepTypeElement.get.enclosedElement.parser
  private lazy val repTypeUnparser = simpleTypeDefBase.optRepTypeElement.get.enclosedElement.unparser

  override lazy val parser: DaffodilParser = {
    if (!typeCalculator.supportsParse) {
      SDE("Parsing not defined by typeValueCalc")
    }
    new TypeValueCalcParser(typeCalculator, repTypeParser, e.elementRuntimeData, repTypeRuntimeData)
  }
  override lazy val unparser: DaffodilUnparser = {
    if (!typeCalculator.supportsUnparse) {
      SDE("Unparsing not defined by typeValueCalc")
    }
    new TypeValueCalcUnparser(typeCalculator, repTypeUnparser, e.elementRuntimeData, repTypeRuntimeData)
  }

}

abstract class AssertPatternPrimBase(decl: Term, stmt: DFDLAssertionBase, discrim: Boolean)
  extends ExpressionEvaluatorBase(decl) {

  override val baseName = if (discrim) "Discriminator" else "Assert"
  override lazy val exprText = stmt.messageAttrib.get
  override lazy val exprNamespaces = decl.namespaces
  override lazy val exprComponent = decl

  override def nodeKind = NodeInfo.String

  lazy val testPattern = {
    PatternChecker.checkPattern(stmt.testTxt, decl)
    stmt.testTxt
  }

  lazy val msgExpr =
    if (stmt.messageAttrib.isDefined) {
      expr
    } else {
      new ConstantExpression[String](qn, NodeInfo.String, testPattern + " failed")
    }

  override val forWhat = ForParser

  lazy val parser: DaffodilParser = new AssertPatternParser(decl.termRuntimeData, discrim, testPattern, msgExpr, stmt.failureType)

  override def unparser: DaffodilUnparser = Assert.invariantFailed("should not request unparser for asserts/discriminators")
}

case class AssertPatternPrim(override val term: Term, stmt: DFDLAssert)
  extends AssertPatternPrimBase(term, stmt, false)

case class DiscriminatorPatternPrim(override val term: Term, stmt: DFDLDiscriminator)
  extends AssertPatternPrimBase(term, stmt, true)
