/*
 * 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.processors.unparsers

import java.lang.{ Number => JNumber }
import java.math.{ BigInteger => JBigInteger, BigDecimal => JBigDecimal }

import org.apache.daffodil.dpath.NodeInfo
import org.apache.daffodil.exceptions.Assert
import org.apache.daffodil.io.DataOutputStream
import org.apache.daffodil.io.FormatInfo
import org.apache.daffodil.processors.ElementRuntimeData
import org.apache.daffodil.processors.Evaluatable
import org.apache.daffodil.processors.ParseOrUnparseState
import org.apache.daffodil.util.Maybe._
import org.apache.daffodil.infoset.DataValue.DataValuePrimitiveNullable

trait PackedBinaryConversion {
  def fromBigInteger(bigInt: JBigInteger, nBits: Int): Array[Byte]
}

abstract class PackedBinaryBaseUnparser(
  override val context: ElementRuntimeData)
  extends PrimUnparser
  with PackedBinaryConversion {

  override lazy val runtimeDependencies: Vector[Evaluatable[AnyRef]] = Vector()

  protected def getBitLength(s: ParseOrUnparseState): Int

  def putNumber(dos: DataOutputStream, number: JNumber, nBits: Int, finfo: FormatInfo): Boolean

  def getNumberToPut(state: UState): JNumber = {
    val node = state.currentInfosetNode.asSimple

    // Packed decimal calendars use the convert combinator which sets the string value of the calendar
    //   - using dataValue would give the Calendar value rather than that string. Since the Calendar value
    //   cannot be cast as a JNumber we need to use dataValueAsString and convert it to a JBigInteger.
    //   With packed numbers, dataValue is already a number so just use that.
    val nodeValue: DataValuePrimitiveNullable =
      node.erd.optPrimType.get match {
        case NodeInfo.Date | NodeInfo.DateTime | NodeInfo.Time => new JBigInteger(node.dataValueAsString)
        case _ => node.dataValue
      }

    val value = nodeValue.getNumber
    value
  }

  override def unparse(state: UState): Unit = {
    val nBits = getBitLength(state)
    val value = getNumberToPut(state)
    val dos = state.dataOutputStream
    val res = putNumber(dos, value, nBits, state)

    if (!res) {
      Assert.invariant(dos.maybeRelBitLimit0b.isDefined)
      UnparseError(One(state.schemaFileLocation), One(state.currentLocation), "Insufficient space to unparse element %s, required %s bits, but only %s were available.",
        context.dpathElementCompileInfo.namedQName.toPrettyString, nBits, dos.maybeRelBitLimit0b.get)
    }
  }

}

abstract class PackedBinaryDecimalBaseUnparser(
  e: ElementRuntimeData,
  binaryDecimalVirtualPoint: Int)
  extends PackedBinaryBaseUnparser(e) {

  override def getNumberToPut(ustate: UState): JNumber = {
    val number = super.getNumberToPut(ustate)
    val bigDec = number.asInstanceOf[JBigDecimal]
    if (bigDec.movePointRight(binaryDecimalVirtualPoint).scale != 0) {
      e.schemaDefinitionError("Decimal point of number '%s' does not match the binaryVirtualDecmialPoint: %d", bigDec, binaryDecimalVirtualPoint)
    }
    bigDec.unscaledValue
  }

  override def putNumber(dos: DataOutputStream, number: JNumber, nBits: Int, finfo: FormatInfo): Boolean = {
    val packedNum = fromBigInteger(number.asInstanceOf[JBigInteger], nBits)
    dos.putByteArray(packedNum, packedNum.length * 8, finfo)
  }
}

abstract class PackedBinaryIntegerBaseUnparser(
  e: ElementRuntimeData)
  extends PackedBinaryBaseUnparser(e) {

  override def getNumberToPut(ustate: UState): JNumber = {
    val number = super.getNumberToPut(ustate)
    val bigInt = number.isInstanceOf[JBigInteger] match {
      case true => number.asInstanceOf[JBigInteger]
      case false => new JBigInteger(number.toString)
    }
    bigInt
  }

  override def putNumber(dos: DataOutputStream, number: JNumber, nBits: Int, finfo: FormatInfo): Boolean = {
    val packedNum = fromBigInteger(number.asInstanceOf[JBigInteger], nBits)
    dos.putByteArray(packedNum, packedNum.length * 8, finfo)
  }
}
