/*
 * 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.dsom

import org.apache.daffodil.dpath._
import org.apache.daffodil.dpath.NodeInfo.PrimType
import scala.xml.NamespaceBinding
import org.apache.daffodil.xml.NamedQName
import java.lang.{ Long => JLong, Boolean => JBoolean }
import org.apache.daffodil.schema.annotation.props.Found
import org.apache.daffodil.util.DPathUtil
import org.apache.daffodil.infoset.DataValue.DataValuePrimitive
import org.apache.daffodil.BasicComponent

object ExpressionCompilers extends ExpressionCompilerClass {
  override val String = new ExpressionCompiler[String]
  override val JLong = new ExpressionCompiler[JLong]
  override val AnyRef = new ExpressionCompiler[AnyRef]
  override val JBoolean = new ExpressionCompiler[JBoolean]
}

class ExpressionCompiler[T <: AnyRef] extends ExpressionCompilerBase[T] {

  /**
   * Compiles an expression or a literl value. If the passed in string does not
   * start with an unescaped curly brace it is treated as a literal value.
   *
   * An expression could be evaluated as constant as in { 5 } or { "California" },
   * and in that case this should return a ConstantExpression object.
   * Non-constant expressions will return a RuntimeExpressionDPath.
   *
   * Literal values (i.e. those that do not start with a unescape curly brace),
   * will be converted to a privite value as defined by nodeInfoKind and will
   * return a ConstantExpression with that value.
   *
   * This method available at compilation and also at runtime for use by the debuggger.
   */
  def compileExpression(
    qn: NamedQName,
    nodeInfoKind: NodeInfo.Kind,
    exprOrLiteral: String,
    namespaces: NamespaceBinding,
    compileInfoWhereExpressionWasLocated: DPathCompileInfo,
    isEvaluatedAbove: Boolean,
    host: BasicComponent,
    compileInfo: DPathCompileInfo): CompiledExpression[T] = {

    val res =
      if (DPathUtil.isExpression(exprOrLiteral)) {
        compileRealExpression(
          qn,
          nodeInfoKind,
          exprOrLiteral,
          namespaces,
          compileInfoWhereExpressionWasLocated,
          isEvaluatedAbove,
          host,
          compileInfo)
      } else {
        convertLiteralToConstant(
          qn,
          nodeInfoKind,
          exprOrLiteral,
          namespaces,
          compileInfoWhereExpressionWasLocated,
          isEvaluatedAbove)
      }
    res
  }

  /**
   * Compile a potentially runtime-valued property.
   *
   * The property value can be an expression or a literal constant.
   *
   * This form for expressions that are the values of most DFDL properties
   *
   * The isEvaluatedAbove argument is used for properties like occursCount
   * which are evaluated before the element whose declaration carries it, exists.
   * That is, it is evaluated one Infoset node above where the expression is
   * written. This doesn't matter unless the expression contains relative
   * paths. In that case those relative paths will all have to be adjusted
   * so they work in the context of one node above.
   */
  def compileProperty(
    qn: NamedQName,
    nodeInfoKind: NodeInfo.Kind,
    property: Found,
    host: BasicComponent,
    compileInfo: DPathCompileInfo,
    isEvaluatedAbove: Boolean = false): CompiledExpression[T] = {

    compileExpression(
      qn,
      nodeInfoKind,
      property.value,
      property.location.namespaces,
      compileInfo,
      isEvaluatedAbove,
      host,
      compileInfo)
  }

  /**
   * Compile a potentially runtime-valued delimiter property
   *
   * The property value can be an expression or a literal constant.
   *
   * This form for delimiters and escapeEscapeCharacter since they
   * can have empty string if statically known, but if an evaluated expression,
   * it must be non-empty string.
   *
   * You can have an empty string, but only statically.
   * That turns off separators entirely.
   * If you have an expression (that is not trivially an empty string),
   * then it must be a non-empty string as the compiled parser will be
   * generated assuming there will be a concrete separator that is part of
   * the data syntax and serves as any delimiter to anchor the parse algorithm.
   *
   * We don't want to allow turning on/off whether a format is delimited or
   * not based on runtime expressions, only what the delimiters are.
   */
  def compileDelimiter(
    qn: NamedQName,
    staticNodeInfoKind: NodeInfo.Kind,
    runtimeNodeInfoKind: NodeInfo.Kind,
    property: Found,
    host: BasicComponent,
    compileInfo: DPathCompileInfo): CompiledExpression[T] = {

    val isEvaluatedAbove = false
    val exprOrLiteral = property.value
    val namespacesForNamespaceResolution = property.location.namespaces
    val compileInfoWhereExpressionWasLocated = propertyCompileInfo(property)
    val compiled1 = compileExpression(
      qn,
      staticNodeInfoKind,
      exprOrLiteral,
      namespacesForNamespaceResolution,
      compileInfoWhereExpressionWasLocated,
      isEvaluatedAbove,
      host,
      compileInfo)

    if (compiled1.isConstant || (staticNodeInfoKind == runtimeNodeInfoKind)) {
      compiled1
    } else {
      val compiled2 = compileExpression(
        qn,
        runtimeNodeInfoKind,
        exprOrLiteral,
        namespacesForNamespaceResolution,
        compileInfoWhereExpressionWasLocated,
        isEvaluatedAbove,
        host,
        compileInfo)
      compiled2
    }
  }

  /**
   * Returns compile info of property regardless of origin.
   *
   * Needed because before serialization of the runtime data
   * objects, the location of a property may be the associated schema
   * component. Once serialized, at runtime when we're compiling expressions
   * we have only the DPathCompileInfo.
   */
  private def propertyCompileInfo(property: Found) = {
    val compileInfoWherePropertyWasLocated = {
      property.location match {
        case sc: SchemaComponent => sc.dpathCompileInfo
        case di: DPathCompileInfo => di
      }
    }
    compileInfoWherePropertyWasLocated
  }

  private def compileRealExpression(
    qn: NamedQName,
    nodeInfoKind: NodeInfo.Kind,
    exprOrLiteral: String,
    namespaces: NamespaceBinding,
    compileInfoWhereExpressionWasLocated: DPathCompileInfo,
    isEvaluatedAbove: Boolean,
    host: BasicComponent,
    compileInfo: DPathCompileInfo): CompiledExpression[T] = {

    // Treat this as an expression--validate and compile it

    val expr = exprOrLiteral.trim
    if (!expr.endsWith("}")) {
      val msg = "'%s' is an unterminated expression. Add missing closing brace, or escape opening brace with another opening brace."
      compileInfoWhereExpressionWasLocated.SDE(msg, exprOrLiteral)
    }

    val compiler = new DFDLPathExpressionParser[T](
      qn,
      nodeInfoKind,
      namespaces,
      compileInfo,
      isEvaluatedAbove,
      host)
    val compiledDPath = compiler.compile(expr)
    compiledDPath
  }

  private def convertLiteralToConstant(
    qn: NamedQName,
    nodeInfoKind: NodeInfo.Kind,
    exprOrLiteral: String,
    namespaces: NamespaceBinding,
    compileInfoWhereExpressionWasLocated: DPathCompileInfo,
    isEvaluatedAbove: Boolean): CompiledExpression[T] = {

    // This string is not a real expression, we need to convert it to it's
    // logical type and set it as a constant expression. Not compilation
    // required. If something is passed in that does not convert to the
    // expected type it will result in error.

    // must be able to convert this to a primitive type
    val maybePrimType = PrimType.fromNodeInfo(nodeInfoKind)
    if (maybePrimType.isEmpty) {
      val msg = "No known primitive type to convert logical value to: %s"
      compileInfoWhereExpressionWasLocated.SDE(msg, nodeInfoKind)
    }

    // remove the leading escape curly brace if it exists
    val literal =
      if (exprOrLiteral.startsWith("{{")) exprOrLiteral.tail
      else exprOrLiteral

    val logical:DataValuePrimitive = try {
      maybePrimType.get.fromXMLString(literal)
    } catch {
      case e: Exception => {
        val msg = "Unable to convert logical value \"%s\" to %s: %s"
        compileInfoWhereExpressionWasLocated.SDE(msg, exprOrLiteral, nodeInfoKind, e.getMessage)
      }
    }

    new ConstantExpression[T](qn, nodeInfoKind, logical.getAnyRef.asInstanceOf[T])
  }
}
