/*
 * 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.schema.annotation.props

import org.apache.daffodil.exceptions._
import org.apache.daffodil.util.Misc._
import org.apache.daffodil.util._
import org.apache.daffodil.cookers.Converter
import scala.collection.mutable

/**
 * Enum class as basis for our DFDL properties
 *
 * This is the best Enum idiom I could find on StackOverflow and other Scala web sites.
 * (That was a somewhat dated comment. This Enum idiom is used in the DFDL properties
 * and is laid down by the code generator for properties. Hence, not changing it eventhough
 * in other places in our code we have Enum.scala which defines a slightly improved
 * enum idiom. Perhaps fix and make consistent both of these once Scala actually puts in
 * a decent not so clumsy enum idiom into the language.)
 *
 * An enumeration for a DFDL property is defined like this:
 * @example {{{
 * ////////////////////////////////////////////////////////////////////////
 * // <xsd:simpleType name="BinaryNumberRepEnum" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:dfdl="http://www.ogf.org/dfdl/dfdl-1.0/">
 * //     <xsd:restriction base="xsd:string">
 * //       <xsd:enumeration value="packed"></xsd:enumeration>
 * //       <xsd:enumeration value="bcd"></xsd:enumeration>
 * //       <xsd:enumeration value="binary"></xsd:enumeration>
 * //     </xsd:restriction>
 * //   </xsd:simpleType>
 *
 * sealed trait BinaryNumberRep extends BinaryNumberRep.Value
 * object BinaryNumberRep extends Enum[BinaryNumberRep] {
 *   case object Packed extends BinaryNumberRep ; forceConstruction(Packed)
 *   case object Bcd extends BinaryNumberRep ; forceConstruction(Bcd)
 *   case object Binary extends BinaryNumberRep ; forceConstruction(Binary)
 *
 *   def apply(name: String) : BinaryNumberRep = stringToEnum("binaryNumberRep", name)
 * }
 *
 * trait BinaryNumberRepMixin extends PropertyMixin {
 *   //
 *   // lazy so if we don't need the property we don't access it. If we do access it, and it
 *   // is not found, it is a schema definition error.
 *   //
 *   lazy val binaryNumberRep = BinaryNumberRep(getProperty("binaryNumberRep"))
 *   ...other stuff...
 * }
 * }}}
 * The first bunch of // comments is the fragment of the DFDL Annotations schema which
 * defines the enumeration. The scala code to realize this follows (this is the output
 * from a code generator). This is just in here to serve as documentation.
 *
 * For the most as a programmer you don't have to deal with the above code much. To
 * use a property you mix in the trait BinaryNumberRepMixin into the class which is
 * to have access to the property. This is actually done with traits that group these individual enum
 * traits into the proper sets for the various schema components.
 *
 * Then within that class's code, to access the property value you just use the lazy val,
 * in this case binaryNumberRep. The type of which will be the enum, with values given by the
 * case objects BinaryNumberRep.Packed, BinaryNumberRep.Bcd, and BinaryNumberRep.Binary.
 *
 * Code using properties never deals with the strings for these enumerations,
 * and never deals with where/how to retrieve properties for any properties, enum or otherwise.
 * Also, retrieving properties is unconditional.
 * If you ask for a property value and it's not defined, it's a schema-definition error.
 * (This is part of the DFDL spec. Implementations are not to provide default values for properties.)
 */
abstract class EnumBase
abstract class EnumValueBase extends Serializable
abstract class Enum[A] extends EnumBase with Converter[String, A] {
  class Value extends EnumValueBase { self: A => {
      val theVal = this
      val cn = getNameFromClass(this)
      val en = cn match {
        //
        // Special case for CalendarFirstDayOfWeek
        //
        case "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" => cn
        case _ => Misc.toInitialLowerCaseUnlessAllUpperCase(cn)
      }
      _values += (en -> theVal)
      _values
    }
    override lazy val toString = {
      val propName = Misc.toInitialLowerCaseUnlessAllUpperCase(getNameFromClass(this))
      propName
    }
  }

  def toPropName(prop: A) = prop.toString

  private val _values = mutable.ArrayBuffer.empty[Tuple2[String, A]]

  /**
   * This is invoked at runtime to compare expression results to see if they
   * match the strings for the enum values. So this has to be fast.
   */
  def stringToEnum(enumTypeName: String, str: String, context: ThrowsSDE): A = {
    val opt = optionStringToEnum(enumTypeName, str)
    if (opt.isDefined) opt.get
    else {
      context.SDE("Unknown value for %s property: %s", enumTypeName, str)
    }
  }

  def optionStringToEnum(enumTypeName: String, str: String): Option[A] = {
    var i: Int = 0
    while (i < _values.size) {
      if (_values(i)._1.equals(str)) { // was equals ignore case - that's just going to allow errors if used at runtime
        return Some(_values(i)._2)
      }
      i += 1
    }
    None
  }

  /**
   * Useful for diagnostic messages where you want to say "must be one of ...." and list the possibilities.
   */
  def allValues = _values.map { case (k, v) => v }

  /**
   * Scala delays construction of case objects (presumably because many programs don't use them at all)
   * We need to force creation of our inner property case objects because constructing them also has
   * the side-effect of registering them in the _values list.
   */
  def forceConstruction(obj: Any): Unit = {
    //Assert.invariant(obj.toString() != "") // TODO: is forceConstruction needed at all?
  }

  override protected def convert(b: String, context: ThrowsSDE, forUnparse: Boolean): A = apply(b, context)

  def apply(name: String, context: ThrowsSDE): A
} // end class

/**
 * base mixin for traits representing collections of DFDL properties
 *
 */
trait PropertyMixin
  extends FindPropertyMixin
  with ThrowsSDE
  with SavesErrorsAndWarnings {

  //  /**
  //   * Only for testing purposes
  //   */
  //  def properties: PropMap

  /**
   * Properties will push their toString function onto this list
   *
   * Each object that has any properties mixed in must implement this
   * value.
   */
  var toStringFunctionList: List[() => String] = Nil

  /**
   * prints all the properties on the object.
   */
  def verboseToString = {
    val className = getNameFromClass(this)
    val props = toStringFunctionList.map { f => f.apply() }.foldLeft(className +
      "(")(_ + _)
    val suffix = ")"
    val str = props + suffix
    str
  }

  def registerToStringFunction(f: (() => String)): Unit = {
    toStringFunctionList +:= f
  }

  /**
   * Convert a property string value to a Boolean
   *
   * Note: no error checking is required, as we assume the schema has already been
   * validated, so we only have to deal with the valid formats for Boolean.
   */
  def convertToBoolean(pv: String) = {
    pv match {
      case "true" => true
      case "false" => false
      case "TRUE" => true
      case "FALSE" => false
    }
  }

  /**
   * Convert a property string value to a Int
   *
   * Note: no error checking is required, as we assume the schema has already been
   * validated, so we only have to deal with the valid formats for Int.
   */
  def convertToInt(pv: String) = {
    pv.toInt
  }

  def convertToDouble(pv: String) = {
    pv.toDouble
  }

  def convertToFloat(pv: String) = {
    pv.toFloat
  }

  /**
   * There's no conversion to do here, but to eliminate a special case in the code generator
   * we always generate a call to a convertToTYPE function.
   */
  def convertToString(pv: String) = {
    // TODO DFDL String Literal processing to deal with
    // entities and raw bytes
    pv
  }

  def convertToQName(pv: String, pl: LookupLocation) = {
    pl.resolveQName(pv)
  }

  def convertToNCName(pv: String): String = {
    // remember: schema validation already checked format of NCName for us.
    pv
  }

} // end trait
