/*
 * 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.runtime1.layers

import org.apache.daffodil.io.processors.charset.BitsCharsetJava
import org.apache.daffodil.io.processors.charset.BitsCharsetNonByteSize
import org.apache.daffodil.lib.api.WarnID
import org.apache.daffodil.lib.schema.annotation.props.gen.LayerLengthKind
import org.apache.daffodil.lib.schema.annotation.props.gen.LayerLengthUnits
import org.apache.daffodil.lib.xml.NS
import org.apache.daffodil.lib.xml.RefQName
import org.apache.daffodil.runtime1.dpath.NodeInfo.PrimType
import org.apache.daffodil.runtime1.dsom.ImplementsThrowsOrSavesSDE
import org.apache.daffodil.runtime1.processors.SequenceRuntimeData
import org.apache.daffodil.runtime1.processors.VariableRuntimeData

/**
 * Must be implemented by all layers.
 *
 * These are the classes which must be dynamically loaded in order to add a layer implementation
 * to Daffodil.
 *
 * These instances are NOT serialized as part of a saved processor. The compileLayer method
 * is called and the resulting LayerTransformerFactory is the serialized object.
 */
abstract class LayerCompiler(nom: String) {

  def name() = nom

  /**
   * Performs all schema-compile-time checking for the layer parameters, and constructs
   * a LayerTransformerFactory which is the serialized runtime object that becomes part of the
   * processor.
   * @param layerCompileInfo Schema-compilation time information about the layer properties.
   * @return
   */
  def compileLayer(layerCompileInfo: LayerCompileInfo): LayerTransformerFactory

}

/**
 * Provides access to DFDL schema compile-time information about the layer properties.
 *
 * Allows reporting of schema definition errors and warnings at schema compile time.
 */
final class LayerCompileInfo(
  sequence: ImplementsThrowsOrSavesSDE,
  val layerRuntimeInfo: LayerRuntimeInfo,
) {

  private def lri = layerRuntimeInfo
  private def srd: SequenceRuntimeData = lri.runtimeData

  def getVariableRuntimeData(
    prefix: String,
    namespace: String,
    localName: String,
    primType: PrimType,
  ): VariableRuntimeData = {
    val varNamespace = NS(namespace)
    val qName = RefQName(Some(prefix), localName, varNamespace).toGlobalQName
    val vrd = srd.variableMap.getVariableRuntimeData(qName).getOrElse {
      srd.SDE("Variable '%s' is not defined.", qName.toExtendedSyntax)
    }
    srd.schemaDefinitionUnless(
      vrd.primType == primType,
      "Variable '%s' is not of type '%s'.",
      qName.toExtendedSyntax,
      primType,
    )
    vrd
  }

  /**
   * If defined, and the value is a compile-time constant then this will be Some(Some(Charset))
   * If defined, and the value is non-constant or not a regular JVM charset, then this will be Some(None)
   * If undefined, the value is None
   */
  def optLayerJavaCharsetOptConstantValue: Option[Option[java.nio.charset.Charset]] = {
    if (lri.maybeLayerCharsetEv.isEmpty) None
    else
      lri.maybeLayerCharsetEv.get.optConstant.map {
        case java: BitsCharsetJava => Some(java.javaCharset)
        case _: BitsCharsetNonByteSize => None
      }
  }

  def optLayerLengthKind: Option[LayerLengthKind] = {
    lri.maybeLayerLengthKind.toScalaOption
  }

  def optLayerLengthOptConstantValue: Option[Option[Long]] = {
    if (lri.maybeLayerLengthEv.isEmpty) None
    else
      Some(lri.maybeLayerLengthEv.get.optConstant.map {
        _.toLong
      })
  }

  def optLayerLengthUnits: Option[LayerLengthUnits] = {
    lri.maybeLayerLengthUnits.toScalaOption
  }

  def optLayerBoundaryMarkOptConstantValue: Option[Option[String]] = {
    if (lri.maybeLayerBoundaryMarkEv.isEmpty) None
    else Some(lri.maybeLayerBoundaryMarkEv.get.optConstant)
  }

  def schemaDefinitionError(message: String, args: Any*): Nothing = {
    sequence.schemaDefinitionError(message, args: _*)
  }

  def schemaDefinitionWarning(message: String, args: Any*): Unit = {
    sequence.SDW(WarnID.LayerCompileWarning, message, args: _*)
  }

  def SDEUnless(test: Boolean, message: String, args: Any*): Unit =
    if (!test) SDE(message, args: _*)

  def SDE(message: String, args: Any*): Nothing = schemaDefinitionError(message, args: _*)
}
