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

import org.apache.daffodil.exceptions.ThrowsSDE
import java.text.ParsePosition
import com.ibm.icu.util.Calendar
import com.ibm.icu.text.SimpleDateFormat
import java.text.ParseException
import org.apache.daffodil.calendar.DFDLDateTime
import org.apache.daffodil.calendar.DFDLTime
import org.apache.daffodil.calendar.DFDLDate

/*
 * Casting chart taken from http://www.w3.org/TR/xpath-functions/#casting, with
 * types DFDL does not allow removed.
 *
 *  S\T   bool dat dbl dec dT  flt hxB int str tim
 *  bool   Y    N   Y   Y   N   Y   N   Y   Y   N
 *  dat    N    Y   N   N   Y   N   N   N   Y   N
 *  dbl    Y    N   Y   M   N   Y   N   M   Y   N
 *  dec    Y    N   Y   Y   N   Y   N   Y   Y   N
 *  dT     N    Y   N   N   Y   N   N   N   Y   Y
 *  flt    Y    N   Y   M   N   Y   N   M   Y   N
 *  hxB    N    N   N   N   N   N   Y   N   Y   N
 *  int    Y    N   Y   Y   N   Y   N   Y   Y   N
 *  str    M    M   M   M   M   M   M   M   Y   M
 *  tim    N    N   N   N   N   N   N   N   Y   Y
 */

object Conversion {

  /**
   * Computes additional recipe ops to be added to the end of an argument Recipe
   * to convert the type. ST is the start type, the type the recipe has as its
   * output type. TT is the target type desired.
   */
  def conversionOps(st: NodeInfo.Kind, tt: NodeInfo.Kind, context: ThrowsSDE): List[RecipeOp] = {
    import NodeInfo._
    val ops: List[RecipeOp] = (st, tt) match {
      case (_, Array) => Nil
      case (x, y) if (x == y) => Nil
      case (x, Nillable) => {
        // we don't have enough information here to check whether the
        // item under scrutiny is nillable or not.
        Nil
      }
      case (Nothing, x) => Nil
      case (x, AnyType) => Nil
      case (x: AnySimpleType.Kind, AnySimpleExists) => Nil
      case (x: AnyAtomic.Kind, AnyAtomic) => Nil
      case (x: Numeric.Kind, String) => List(NumericToString)

      case (Numeric, _) => NumericToString +: conversionOps(String, tt, context)
      //
      // Because of round-half-to-even, we need to know what the type of the original
      // argument is. Doing these auto-conversions to Decimal hide this.
      //
      // Valid Numeric types are xs:float, xs:double, xs:integer and any sub-types.
      //
      //      case (Float, n: Numeric.Kind) => FloatToDouble +: conversionOps(Double, tt, context)
      //      case (Integer, n: Numeric.Kind) => IntegerToDecimal +: conversionOps(Decimal, tt, context)
      //      case (Double, Numeric) => List(DoubleToDecimal)
      case (n: NodeInfo.Numeric.Kind, Numeric) => Nil

      case (x: AnyDateTime.Kind, String) => List(DateTimeToString)
      case (HexBinary, String) => List(HexBinaryToString)
      case (String, NonEmptyString) => List(StringToNonEmptyString)
      case (x, NonEmptyString) => conversionOps(st, String, context) :+ StringToNonEmptyString
      case (x, ArrayIndex) => conversionOps(st, Long, context) ++ List(LongToArrayIndex)

      case (Integer, Boolean) => IntegerToDecimal +: conversionOps(Decimal, tt, context)
      case (Decimal, Boolean) => List(DecimalToBoolean)
      case (Double, Boolean) => List(DoubleToBoolean)
      case (Float, Boolean) => FloatToDouble +: conversionOps(Double, tt, context)
      case (n: Numeric.Kind, Boolean) => conversionOps(n, Long, context) ++ List(LongToBoolean)

      case (Boolean, Double) => BooleanToLong +: conversionOps(Long, tt, context)
      case (Boolean, Decimal) => BooleanToLong +: conversionOps(Long, tt, context)
      case (Boolean, Float) => BooleanToLong +: conversionOps(Long, tt, context)
      case (Boolean, Integer) => BooleanToLong +: conversionOps(Long, tt, context)
      case (Boolean, NonNegativeInteger) => BooleanToLong +: conversionOps(Long, tt, context)
      case (Boolean, Long) => List(BooleanToLong)
      case (Boolean, UnsignedLong) => List(BooleanToLong) // no need to range check.
      case (Boolean, Int) => BooleanToLong +: conversionOps(Long, tt, context)
      case (Boolean, UnsignedInt) => BooleanToLong +: conversionOps(Long, tt, context)
      case (Boolean, Short) => BooleanToLong +: conversionOps(Long, tt, context)
      case (Boolean, UnsignedShort) => BooleanToLong +: conversionOps(Long, tt, context)
      case (Boolean, Byte) => BooleanToLong +: conversionOps(Long, tt, context)
      case (Boolean, UnsignedByte) => BooleanToLong +: conversionOps(Long, tt, context)
      case (Boolean, String) => List(BooleanToString)
      case (Date, DateTime) => List(DateToDateTime)
      case (Double, Decimal) => List(DoubleToDecimal)
      case (Double, Float) => List(DoubleToFloat)
      case (Double, Integer) => DoubleToDecimal +: conversionOps(Decimal, tt, context)
      case (Double, NonNegativeInteger) => DoubleToDecimal +: conversionOps(Decimal, tt, context)
      case (Double, Long) => List(DoubleToLong)
      case (Double, UnsignedLong) => List(DoubleToUnsignedLong)
      case (Double, i: Int.Kind) => DoubleToLong +: conversionOps(Long, tt, context)
      case (Double, ui: UnsignedInt.Kind) => DoubleToUnsignedLong +: UnsignedLongToLong +: conversionOps(Long, tt, context)

      case (Float, n: Numeric.Kind) => FloatToDouble +: conversionOps(Double, tt, context)

      case (Decimal, Integer) => List(DecimalToInteger)
      case (Decimal, NonNegativeInteger) => List(DecimalToNonNegativeInteger)
      case (Decimal, Double) => List(DecimalToDouble)
      case (Decimal, Float) => DecimalToDouble +: conversionOps(Double, tt, context)
      case (Decimal, Long) => List(DecimalToLong)
      case (Decimal, UnsignedLong) => List(DecimalToUnsignedLong)
      case (Decimal, Int) => List(DecimalToLong, LongToInt)
      case (Decimal, UnsignedInt) => List(DecimalToLong, LongToUnsignedInt)
      case (Decimal, Short) => List(DecimalToLong, LongToShort)
      case (Decimal, UnsignedShort) => List(DecimalToLong, LongToUnsignedShort)
      case (Decimal, Byte) => List(DecimalToLong, LongToByte)
      case (Decimal, UnsignedByte) => List(DecimalToLong, LongToUnsignedByte)

      case (Integer, n: NodeInfo.Numeric.Kind) => IntegerToDecimal +: conversionOps(Decimal, tt, context)

      case (NonNegativeInteger, n: Numeric.Kind) => IntegerToDecimal +: conversionOps(Decimal, tt, context)
      // Commented these out because these are illegal conversions for XPath functions.
      // I cannot pass a DateTime as a Date argument to an XPath function.
      //
      //case (DateTime, Date) => List(DateTimeToDate)
      //case (DateTime, Time) => List(DateTimeToTime)
      //case (Time, DateTime) => List(TimeToDateTime)
      case (String, Double) => List(StringToDouble)
      case (String, Decimal) => List(StringToDecimal)
      case (String, Float) => List(StringToDouble, DoubleToFloat)
      case (String, Integer) => List(StringToDecimal, DecimalToInteger)
      case (String, NonNegativeInteger) => List(StringToDecimal, DecimalToNonNegativeInteger)
      case (String, Long) => List(StringToLong)
      case (String, UnsignedLong) => List(StringToUnsignedLong)
      case (String, Int) => List(StringToLong, LongToInt)
      case (String, UnsignedInt) => List(StringToLong, LongToUnsignedInt)
      case (String, Short) => List(StringToLong, LongToShort)
      case (String, UnsignedShort) => List(StringToLong, LongToUnsignedShort)
      case (String, Byte) => List(StringToLong, LongToByte)
      case (String, UnsignedByte) => List(StringToLong, LongToUnsignedByte)
      case (String, HexBinary) => List(XSHexBinary.asInstanceOf[RecipeOp]) // TODO: figure out why I had to put this cast in place.
      case (String, Date) => List(StringToDate)
      case (String, Time) => List(StringToTime)
      case (String, DateTime) => List(StringToDateTime)
      case (String, Boolean) => List(StringToBoolean)
      case (Byte, Long) => List(ByteToLong)

      case (Long, Integer) => List(LongToInteger)
      case (Long, NonNegativeInteger) => List(LongToNonNegativeInteger)
      case (Long, Decimal) => List(LongToDecimal)
      case (Long, UnsignedLong) => List(LongToUnsignedLong)
      case (Long, Int) => List(LongToInt)
      case (Long, UnsignedInt) => List(LongToUnsignedInt)
      case (Long, Short) => List(LongToShort)
      case (Long, UnsignedShort) => List(LongToUnsignedShort)
      case (Long, Byte) => List(LongToByte)
      case (Long, UnsignedByte) => List(LongToUnsignedByte)
      case (Long, Double) => List(LongToDouble)
      case (Long, Float) => LongToDouble +: conversionOps(Double, tt, context)

      case (Int, Double) => IntToLong +: conversionOps(Long, tt, context)
      case (Int, Decimal) => IntToLong +: conversionOps(Long, tt, context)
      case (Int, Float) => IntToLong +: conversionOps(Long, tt, context)
      case (Int, Integer) => IntToLong +: conversionOps(Long, tt, context)
      case (Int, NonNegativeInteger) => IntToLong +: conversionOps(Long, tt, context)
      case (Int, Long) => List(IntToLong)
      case (Int, UnsignedLong) => IntToLong +: conversionOps(Long, tt, context)
      case (Int, UnsignedInt) => IntToLong +: conversionOps(Long, tt, context)
      case (Int, Short) => IntToLong +: conversionOps(Long, tt, context)
      case (Int, UnsignedShort) => IntToLong +: conversionOps(Long, tt, context)
      case (Int, Byte) => IntToLong +: conversionOps(Long, tt, context)
      case (Int, UnsignedByte) => IntToLong +: conversionOps(Long, tt, context)

      case (UnsignedInt, Double) => UnsignedIntToLong +: conversionOps(Long, tt, context)
      case (UnsignedInt, Decimal) => UnsignedIntToLong +: conversionOps(Long, tt, context)
      case (UnsignedInt, Float) => UnsignedIntToLong +: conversionOps(Long, tt, context)
      case (UnsignedInt, Integer) => UnsignedIntToLong +: conversionOps(Long, tt, context)
      case (UnsignedInt, NonNegativeInteger) => UnsignedIntToLong +: conversionOps(Long, tt, context)
      case (UnsignedInt, Long) => List(UnsignedIntToLong)
      case (UnsignedInt, UnsignedLong) => List(UnsignedIntToLong) // no need to range check.
      case (UnsignedInt, Int) => UnsignedIntToLong +: conversionOps(Long, tt, context)
      case (UnsignedInt, Short) => UnsignedIntToLong +: conversionOps(Long, tt, context)
      case (UnsignedInt, UnsignedShort) => UnsignedIntToLong +: conversionOps(Long, tt, context)
      case (UnsignedInt, Byte) => UnsignedIntToLong +: conversionOps(Long, tt, context)
      case (UnsignedInt, UnsignedByte) => UnsignedIntToLong +: conversionOps(Long, tt, context)

      case (UnsignedLong, Double) => UnsignedLongToLong +: conversionOps(Long, tt, context)
      case (UnsignedLong, Decimal) => UnsignedLongToLong +: conversionOps(Long, tt, context)
      case (UnsignedLong, Float) => UnsignedLongToLong +: conversionOps(Long, tt, context)
      case (UnsignedLong, Integer) => UnsignedLongToLong +: conversionOps(Long, tt, context)
      case (UnsignedLong, NonNegativeInteger) => UnsignedLongToLong +: conversionOps(Long, tt, context)
      case (UnsignedLong, Long) => List(UnsignedLongToLong)
      case (UnsignedLong, Int) => UnsignedLongToLong +: conversionOps(Long, tt, context)
      case (UnsignedLong, Short) => UnsignedLongToLong +: conversionOps(Long, tt, context)
      case (UnsignedLong, UnsignedShort) => UnsignedLongToLong +: conversionOps(Long, tt, context)
      case (UnsignedLong, Byte) => UnsignedLongToLong +: conversionOps(Long, tt, context)
      case (UnsignedLong, UnsignedByte) => UnsignedLongToLong +: conversionOps(Long, tt, context)

      case (ArrayIndex, Double) => ArrayIndexToLong +: conversionOps(Long, tt, context)
      case (ArrayIndex, Decimal) => ArrayIndexToLong +: conversionOps(Long, tt, context)
      case (ArrayIndex, Float) => ArrayIndexToLong +: conversionOps(Long, tt, context)
      case (ArrayIndex, Integer) => ArrayIndexToLong +: conversionOps(Long, tt, context)
      case (ArrayIndex, NonNegativeInteger) => ArrayIndexToLong +: conversionOps(Long, tt, context)
      case (ArrayIndex, Long) => List(ArrayIndexToLong)
      case (ArrayIndex, UnsignedLong) => List(ArrayIndexToLong) // no need to range check.
      case (ArrayIndex, Int) => ArrayIndexToLong +: conversionOps(Long, tt, context)
      case (ArrayIndex, UnsignedInt) => ArrayIndexToLong +: conversionOps(Long, tt, context)
      case (ArrayIndex, Short) => ArrayIndexToLong +: conversionOps(Long, tt, context)
      case (ArrayIndex, UnsignedShort) => ArrayIndexToLong +: conversionOps(Long, tt, context)
      case (ArrayIndex, Byte) => ArrayIndexToLong +: conversionOps(Long, tt, context)
      case (ArrayIndex, UnsignedByte) => ArrayIndexToLong +: conversionOps(Long, tt, context)

      case (Short, Double) => ShortToLong +: conversionOps(Long, tt, context)
      case (Short, Decimal) => ShortToLong +: conversionOps(Long, tt, context)
      case (Short, Float) => ShortToLong +: conversionOps(Long, tt, context)
      case (Short, Integer) => ShortToLong +: conversionOps(Long, tt, context)
      case (Short, NonNegativeInteger) => ShortToLong +: conversionOps(Long, tt, context)
      case (Short, Long) => List(ShortToLong)
      case (Short, UnsignedLong) => List(ShortToLong) // no need to range check.
      case (Short, Int) => ShortToLong +: conversionOps(Long, tt, context)
      case (Short, UnsignedInt) => ShortToLong +: conversionOps(Long, tt, context)
      case (Short, UnsignedShort) => ShortToLong +: conversionOps(Long, tt, context)
      case (Short, Byte) => ShortToLong +: conversionOps(Long, tt, context)
      case (Short, UnsignedByte) => ShortToLong +: conversionOps(Long, tt, context)

      case (UnsignedShort, Double) => UnsignedShortToLong +: conversionOps(Long, tt, context)
      case (UnsignedShort, Decimal) => UnsignedShortToLong +: conversionOps(Long, tt, context)
      case (UnsignedShort, Float) => UnsignedShortToLong +: conversionOps(Long, tt, context)
      case (UnsignedShort, Integer) => UnsignedShortToLong +: conversionOps(Long, tt, context)
      case (UnsignedShort, NonNegativeInteger) => UnsignedShortToLong +: conversionOps(Long, tt, context)
      case (UnsignedShort, Long) => List(UnsignedShortToLong)
      case (UnsignedShort, UnsignedLong) => List(UnsignedShortToLong) // no need to range check.
      case (UnsignedShort, Int) => UnsignedShortToLong +: conversionOps(Long, tt, context)
      case (UnsignedShort, UnsignedInt) => UnsignedShortToLong +: conversionOps(Long, tt, context)
      case (UnsignedShort, Short) => UnsignedShortToLong +: conversionOps(Long, tt, context)
      case (UnsignedShort, Byte) => UnsignedShortToLong +: conversionOps(Long, tt, context)
      case (UnsignedShort, UnsignedByte) => UnsignedShortToLong +: conversionOps(Long, tt, context)

      case (Byte, Double) => ByteToLong +: conversionOps(Long, tt, context)
      case (Byte, Decimal) => ByteToLong +: conversionOps(Long, tt, context)
      case (Byte, Float) => ByteToLong +: conversionOps(Long, tt, context)
      case (Byte, Integer) => ByteToLong +: conversionOps(Long, tt, context)
      case (Byte, NonNegativeInteger) => ByteToLong +: conversionOps(Long, tt, context)
      case (Byte, UnsignedLong) => List(ByteToLong) // no need to range check.
      case (Byte, Int) => ByteToLong +: conversionOps(Long, tt, context)
      case (Byte, UnsignedInt) => ByteToLong +: conversionOps(Long, tt, context)
      case (Byte, Short) => ByteToLong +: conversionOps(Long, tt, context)
      case (Byte, UnsignedShort) => ByteToLong +: conversionOps(Long, tt, context)
      case (Byte, Byte) => ByteToLong +: conversionOps(Long, tt, context)
      case (Byte, UnsignedByte) => ByteToLong +: conversionOps(Long, tt, context)

      case (UnsignedByte, Double) => UnsignedByteToLong +: conversionOps(Long, tt, context)
      case (UnsignedByte, Decimal) => UnsignedByteToLong +: conversionOps(Long, tt, context)
      case (UnsignedByte, Float) => UnsignedByteToLong +: conversionOps(Long, tt, context)
      case (UnsignedByte, Integer) => UnsignedByteToLong +: conversionOps(Long, tt, context)
      case (UnsignedByte, NonNegativeInteger) => UnsignedByteToLong +: conversionOps(Long, tt, context)
      case (UnsignedByte, Long) => List(UnsignedByteToLong)
      case (UnsignedByte, UnsignedLong) => List(UnsignedByteToLong) // no need to range check.
      case (UnsignedByte, Int) => UnsignedByteToLong +: conversionOps(Long, tt, context)
      case (UnsignedByte, UnsignedInt) => UnsignedByteToLong +: conversionOps(Long, tt, context)
      case (UnsignedByte, Short) => UnsignedByteToLong +: conversionOps(Long, tt, context)
      case (UnsignedByte, UnsignedShort) => UnsignedByteToLong +: conversionOps(Long, tt, context)
      case (UnsignedByte, Byte) => UnsignedByteToLong +: conversionOps(Long, tt, context)

      // Note: These are explicitly not allowed
      //      case (AnyAtomic, Time) => AnyAtomicToString +: conversionOps(String, tt, context)
      //      case (AnyAtomic, Date) => AnyAtomicToString +: conversionOps(String, tt, context)
      //      case (AnyAtomic, DateTime) => AnyAtomicToString +: conversionOps(String, tt, context)

      // TODO: Is this a valid solution for DFDL-1074 and the like?
      // Essentially, all of the 'types' should fall under AnyAtomic and there
      // is no need to conver to an AnyAtomic.  So anything converted to AnyAtomic
      // should be itself.
      //
      case (_, AnyAtomic) => Nil // is this correct?

      case (_, Exists) => Nil
      case (_, other) =>
        context.SDE("The type %s cannot be converted to %s.", st.name, tt.name)
    }
    ops
  }

  protected def constructCalendar(value: String, inFormat: SimpleDateFormat, fncName: String, toType: String): Calendar = {
    val isLenient = inFormat.isLenient()

    try {
      inFormat.setLenient(false)
      inFormat.parse(value)
      true
    } catch {
      case e: ParseException => {
        inFormat.setLenient(isLenient)
        throw new java.lang.IllegalArgumentException(String.format("Conversion Error: %s failed to convert \"%s\" to %s. Due to %s",
          fncName, value.toString, toType, "Failed to match format."))
      }
    }

    val calendar = inFormat.getCalendar()
    val pos = new ParsePosition(0)

    inFormat.parse(value.toString, calendar, pos)

    try {
      calendar.getTime()
    } catch {
      case ex: java.lang.IllegalArgumentException => {
        inFormat.setLenient(isLenient)
        throw new java.lang.IllegalArgumentException(String.format("Conversion Error: %s failed to convert \"%s\" to %s. Due to %s",
          fncName, value.toString, toType, ex.getMessage()))
      }
    }

    inFormat.setLenient(isLenient)

    if (pos.getIndex() == 0 || pos.getErrorIndex() != -1) {
      throw new java.lang.IllegalArgumentException(String.format("Conversion Error: %s failed to convert \"%s\" to %s due to a parse error.",
        fncName, value.toString, toType))
    }
    if (pos.getIndex() != (value.length())) {
      throw new java.lang.IllegalArgumentException(String.format("Conversion Error: %s failed to convert \"%s\" to %s. Failed to use up all characters in the parse.  Stopped at %s.",
        fncName, value.toString, toType, pos.getIndex().toString))
    }
    calendar
  }

  def stringToDFDLDateTime(value: String, inFormat: SimpleDateFormat, expectsTZ: Boolean, fncName: String, toType: String): DFDLDateTime = {
    val calendar = constructCalendar(value, inFormat, fncName, toType)
    DFDLDateTime(calendar, expectsTZ)
  }

  def stringToDFDLDate(value: String, inFormat: SimpleDateFormat, expectsTZ: Boolean, fncName: String, toType: String): DFDLDate = {
    val calendar = constructCalendar(value, inFormat, fncName, toType)
    DFDLDate(calendar, expectsTZ)
  }

  def stringToDFDLTime(value: String, inFormat: SimpleDateFormat, expectsTZ: Boolean, fncName: String, toType: String): DFDLTime = {
    val calendar = constructCalendar(value, inFormat, fncName, toType)
    DFDLTime(calendar, expectsTZ)
  }
}
