/*
 * 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.lib.util

import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.lang.reflect.Field
import java.net.URI
import java.net.URLClassLoader
import java.nio.ByteBuffer
import java.nio.channels.ReadableByteChannel
import java.nio.channels.WritableByteChannel
import java.nio.charset.Charset
import java.nio.charset.Charset as JavaCharset
import java.nio.charset.CodingErrorAction
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Paths
import scala.io.Source
import scala.jdk.CollectionConverters.*
import scala.util.Using

import org.apache.daffodil.lib.equality.*
import org.apache.daffodil.lib.exceptions.Assert

import passera.unsigned.UByte

/**
 * Various reusable utilities that I couldn't easily find a better place for.
 */
object Misc {

  def boolToOpt[T](test: Boolean, thing: => T): Option[T] = {
    if (test) Some(thing) else None
  }

  def getNameFromClass(obj: Any): String = getNameGivenAClassObject(obj.getClass)

  def getNameGivenAClassObject(clazz: Class[?]): String = {
    if (clazz == null) return "null"
    val nonPackageClassName = clazz.getName.split("""\.""").toList.reverse.head
    val nonDollarsParts = nonPackageClassName.split("""\$""").toList.reverse
    val nonNumericParts = nonDollarsParts.filterNot { _.matches("""\d*""") }
    val nameToken = nonNumericParts.head
    nameToken
  }

  /**
   * Removes a suffix string, if it is found.
   */
  def stripSuffix(s: String, suffix: String) = {
    if (s.endsWith(suffix))
      s.substring(0, s.length - suffix.length)
    else s
  }

  def stripQuotes(s: String) = {
    val stripFirst = if (s.startsWith("\"")) s.substring(1) else s
    val stripLast =
      if (stripFirst.endsWith("\"")) stripFirst.substring(0, stripFirst.length - 1)
      else stripFirst
    stripLast
  }

  def isNullOrBlank(s: String) = {
    val b = Option(s).isEmpty || s.trim.isEmpty
    b
  }

  def isFileURI(uri: URI) = {
    if (uri.isAbsolute()) {
      val protocol = uri.toURL.getProtocol()
      val result = protocol == "file"
      result
    } else
      false
  }

  /**
   * Takes care of using the resource built-in to the jar, or
   * if we're just running interactively in eclipse or Intellij, doesn't use the jar.
   * But those put the same things on the classpath.
   *
   * resourcePath argument is relative to the classpath root.
   */
  def getResourceOption(resourcePathRaw: String): (Option[URI], String) = {
    val resourcePath = resourcePathRaw.replaceAll("""\s""", "%20")

    // more time is wasted by people forgetting that the initial "/" is needed
    // to get classpath relative behavior... Let's make sure there is a leading "/"
    val resPath = if (resourcePath.startsWith("/")) resourcePath else "/" + resourcePath
    val res = Option(this.getClass().getResource(resPath))
    (res.map(_.toURI), resPath)
  }

  /**
   * Gets a resource on the classpath, or relative to another URI
   */
  private def getResourceAbsoluteOrRelativeOption(
    rawResName: String,
    optContextURI: Option[URI]
  ): Option[URI] = {
    val resName = rawResName.replaceAll("""\s""", "%20")
    val (maybeRes, _) = Misc.getResourceOption(resName)
    if (maybeRes.isDefined) {
      maybeRes // found directly on the classpath.
    } else {
      optContextURI.flatMap { contextURI =>
        getResourceRelativeOnlyOption(resName, contextURI)
      }
    }
  }

  /**
   * Get resource relative to the context URI.
   *
   * Does NOT try the string as an absolute location first
   * or anything like that.
   *
   * @param relPath
   * @param contextURI
   * @return Some uri if the relative resource exists.
   */
  def getResourceRelativeOnlyOption(relPath: String, contextURI: URI): Option[URI] = {
    Assert.usage(relPath ne null)
    Assert.usage(contextURI ne null)
    contextURI.getScheme match {
      case "jar" =>
        // We used to call new URL(jarURI, relativePathString)
        // but that is deprecated now (as of Java 20)
        optRelativeJarFileURI(contextURI, relPath)
      case "file" => optRelativeFileURI(contextURI, relPath)
      case "resource" => optRelativeResourceURI(contextURI, relPath)
      case _ =>
        // not a file nor an opaque resource URI. What is it?
        throw new IllegalArgumentException(s"Unrecognized URI scheme: $contextURI")
    }
  }

  /**
   * Split a jar URI into its two component parts: 1) the path to a jar file 2) the resource
   * path within that jar file. By definition, these two parts are delimited by the last
   * occurrence of an exclamation point. If the resource path part contains an exclamation point
   * it should have already been escaped using "%21", which is the URI escape code for an
   * exclamation point.
   *
   * Note that it is important that we look for the last index of an exclamation point, since it
   * is possible some tools have added support for nested jars, and could have multiple
   * exclamation points for each nesting.
   */
  def splitJarUri(uri: URI): (String, String) = {
    Assert.invariant(uri.getScheme == "jar")
    var uriStr = uri.toString
    val exclamIdx = uriStr.lastIndexOf("!")
    val jarPart = uriStr.substring(0, exclamIdx)
    val pathPart = uriStr.substring(exclamIdx + 1)
    (jarPart, pathPart)
  }

  /**
   * Java 20 deprecated the 2-arg URL constructor which worked to create relative URIs
   * within the same Jar file.
   *
   * This is a bit harder to achieve now. You are not allowed to resolve relative to a jar file URI.
   * That is URI.resolve(relPath) doesn't work if the URI is a jar file URI.
   *
   * Now we have to hack the jar:file: URI as a string because URI.resolve won't work
   *
   * jar file URIs look like this:
   *
   *    `jar:file:/..absolute path to jar file.jar!/absolute path from root inside jar to file``
   *
   * We split this URI into its component parts, make a relative path on just the resource path
   * inside the jar, then glue back together.
   *
   * @param contextURI
   * @param relPath
   * @return Some(uri) for an existing relative path within the same jar file, or None if it does not exist.
   */
  def optRelativeJarFileURI(contextURI: URI, relPath: String): Option[URI] = {
    val (jarPart, pathPart) = Misc.splitJarUri(contextURI)
    Assert.invariant(pathPart.startsWith("/"))
    val contextURIPathOnly = URI.create(pathPart)
    val resolvedURIPathOnly = contextURIPathOnly.resolve(relPath)
    val newJarPathURI = URI.create(jarPart + "!" + resolvedURIPathOnly.toString)
    try {
      newJarPathURI.toURL.openStream().close()
      // that worked, so we can open it so it exists.
      Some(newJarPathURI)
    } catch {
      case io: IOException =>
        // failed. So that jar file doesn't exist
        None
    }
  }

  def optRelativeFileURI(contextURI: URI, relPath: String): Option[URI] = {
    val resourceURI = contextURI.resolve(relPath)
    if (Paths.get(resourceURI).toFile.exists())
      Some(resourceURI)
    else None
  }

  def optRelativeResourceURI(contextURI: URI, relPath: String): Option[URI] = {
    val resolvedURI = contextURI.resolve(relPath)
    if (this.getClass.getResource(resolvedURI.getPath) != null)
      Some(resolvedURI)
    else None
  }

  /**
   * Search for a resource name, trying a handful of heuristics.
   *
   * This is useful in cases where it's okay to be a little lax when searching
   * for a resource name (e.g. testing). This is often needed in situations
   * like directory structures not exactly match paths and we just want to try
   * our best to find a file. Heuristics include things like ignoring leading
   * directories in a resource, looking for a resource as a file, etc. This
   * does mean that it's possible that this could find the wrong file if there
   * are ambiguities, so be careful when using.
   *
   * If ambiguities would be a serious problem, use getResourceOption or
   * getResourceRelativeOption.
   */
  def searchResourceOption(resName: String, relativeTo: Option[URI]): Option[URI] = {
    val resAsURI = new URI(resName)
    val resPath =
      if (resAsURI.getScheme != null) Paths.get(resAsURI) else Paths.get(resName)
    val resolvedURI =
      if (Files.exists(resPath)) Some(resPath.toFile().toURI())
      else Misc.getResourceAbsoluteOrRelativeOption(resName, relativeTo)
    val res = resolvedURI.orElse {
      // try ignoring the directory part
      val parts = resName.split("/")
      if (parts.length > 1) { // if there is one
        val filePart = parts.last
        val secondTry = searchResourceOption(filePart, relativeTo) // recursively
        secondTry
      } else {
        None
      }
    }
    res
  }

  lazy val classPath = {
    val cl = this.getClass().getClassLoader()
    val urls = cl match {
      case url: URLClassLoader => url.getURLs().toSeq
      case _ => Seq.empty
    }
    urls
  }

  def getRequiredResource(resourcePath: String): URI = {
    getResourceOption(resourcePath) match {
      case (None, resPath) => {
        val msg = "Required resource " + resPath + " was not found.\nClasspath is " +
          (if (classPath.length == 0) "unknown."
           else ": " + classPath.mkString("\n"))
        throw new java.io.FileNotFoundException(msg)
      }
      case (Some(res), _) => res
    }
  }

  // TODO: no test coverage
  def initialUpperCase(s: String): String = s.head.toUpper +: s.substring(1)
  def initialLowerCase(s: String): String = s.head.toLower +: s.substring(1)

  /**
   * Convert FooBar to fooBar, but leave FOOBAR as FOOBAR.
   */
  def toInitialLowerCaseUnlessAllUpperCase(s: String): String = {
    if (s(0).isLower) return s
    //
    // At this point we know the first letter is uppercase
    //
    if (isAllUpper(s, 1)) s
    else s(0).toLower +: s.substring(1)
  }

  def isAllUpper(s: String, start: Int): Boolean = {
    var i = start
    val l = s.length
    while (i < l) {
      if (s(i).isLower) return false
      i += 1
    }
    true
  }

  /**
   * Returns true if a StringBuilder contains all whitespace
   */
  def isAllWhitespace(sb: StringBuilder): Boolean = {
    if (sb.isEmpty) false
    else {
      var in = 0
      val sbLen = sb.length
      while (in < sbLen) {
        if (!sb.charAt(in).isWhitespace) return false
        in += 1
      }
      true
    }
  }

  /**
   * Returns the primary version of daffodil from the jar
   */
  def getDaffodilVersion: String = {
    val uri = getRequiredResource("org/apache/daffodil/lib/VERSION")
    val version = Source.fromInputStream(uri.toURL.openStream, "UTF-8").mkString
    version
  }

  /**
   * Requires an even number of nibbles.
   */
  def hex2Bytes(hex: String): Array[Byte] = {
    // This function originally just looked like this:
    //
    //   hex.sliding(2, 2).toArray.map { Integer.parseInt(_, 16).toByte }
    //
    // Although that is very clear and simple, it is actually very slow. The
    // code below, which accomplishes the same thing, is about 2 orders of
    // magnitude faster. We're leaving the above comment in place to more
    // clearly show what this function is doing, but keeping this below code
    // due to its speed, which is very important for schemas containing
    // xs:hexBinary types.

    def hexCharToValue(c: Char): Int = {
      val i = c.toInt
      val v =
        if (i >= 48 && i <= 57) i - 48 // number 0-9
        else if (i >= 65 && i <= 70) (i - 65) + 10 // capital A-F
        else if (i >= 97 && i <= 102) (i - 97) + 10 // lowercase a-f
        else
          throw new NumberFormatException(
            "Hex character must be 0-9, a-f, or A-F, but was '" + c + "'"
          )
      v
    }

    val len = hex.length
    if (len % 2 != 0) {
      throw new NumberFormatException(
        "Hex string must have an even number of characters, but was " + len + " for " + hex
      )
    }
    val numBytes: Int = len / 2
    val arr = new Array[Byte](numBytes)
    var arrIdx: Int = 0
    var hexIdx: Int = 0
    while (arrIdx < numBytes) {
      val l = hexCharToValue(hex(hexIdx))
      val r = hexCharToValue(hex(hexIdx + 1))
      arr(arrIdx) = ((l << 4) | r).toByte
      arrIdx += 1
      hexIdx += 2
    }
    arr
  }

  def hex2Bits(hex: String): String = {
    val nums = hex.map { ch => Integer.parseInt(ch.toString, 16) }
    val bits = nums.map { java.lang.Long.toString(_, 2) }
    val paddedBits = bits.map { "%4s".format(_).replaceAll(" ", "0") }
    val res = paddedBits.mkString
    res
  }

  private val hexLookup = "0123456789ABCDEF".toArray

  def bytes2Hex(bytes: Array[Byte]): String = {
    Assert.invariant(bytes.length <= Int.MaxValue / 2)
    val hexArr = new Array[Char](bytes.length * 2)
    var bytIdx = 0
    var hexIdx = 0
    while (bytIdx < bytes.length) {
      val b = bytes(bytIdx) & 0xff
      hexArr(hexIdx) = hexLookup(b >>> 4)
      hexArr(hexIdx + 1) = hexLookup(b & 0x0f)
      bytIdx += 1
      hexIdx += 2
    }
    new String(hexArr)
  }

  def bits2Bytes(bits: String): Array[Byte] =
    if (bits.isEmpty()) Nil.toArray
    else bits2Bytes(Seq(bits))

  def bits2Bytes(bits: Seq[String]): Array[Byte] = {
    // split at any character not a 0 or 1, then concatenate all
    val allBitsOnly = bits.flatMap { _.split("[^01]") }.mkString
    val byteSizedBits = allBitsOnly.sliding(8, 8)
    byteSizedBits.map(Integer.parseInt(_, 2).toByte).toArray
  }

  def bytes2Bits(bytes: Array[Byte]): String = {
    bytes.map { b => (b & 0xff).toBinaryString.reverse.padTo(8, '0').reverse }.mkString
  }

  // Moved here from Compiler object.

  def stringToReadableByteChannel(s: String): ReadableByteChannel = {
    val bytes = s.getBytes(StandardCharsets.UTF_8) // never use default charset. NEVER.
    byteArrayToReadableByteChannel(bytes)
  }

  def stringToWritableByteChannel(s: String): WritableByteChannel = {
    val size = s.length() // TODO: get byte count by encoding
    byteArrayToWritableByteChannel(size)
  }

  def byteArrayToReadableByteChannel(bytes: Array[Byte]): ReadableByteChannel = {
    val inputStream = new ByteArrayInputStream(bytes);
    val rbc = java.nio.channels.Channels.newChannel(inputStream);
    rbc
  }

  def byteArrayToWritableByteChannel(size: Int): WritableByteChannel = {
    val outputStream = new ByteArrayOutputStream(size);
    val wbc = java.nio.channels.Channels.newChannel(outputStream);
    wbc
  }

  def fileToReadableByteChannel(file: java.io.File): ReadableByteChannel = {
    val inputStream = new java.io.FileInputStream(file)
    val rbc = java.nio.channels.Channels.newChannel(inputStream);
    rbc
  }

  /**
   * This function creates a representation of data which doesn't
   * contain any control or whitespace characters that jump around the screen.
   * It replaces those with characters that have a simple glyph.
   *
   * The point of this is when you display the stream of data for
   * debugging, or for a diagnostic message,
   * the characters which control position like CR, LF, FF,
   * VT, HT, BS, etc. all make it hard to figure out what is going on.
   * Replacing these with the picture characters (designed for this purpose)
   * in the unicode x2400 block helps.
   */
  def remapStringToVisibleGlyphs(s: String) =
    nonGlyphToVisibleGlyphsRemapper.remap(s)

  object nonGlyphToVisibleGlyphsRemapper extends CharacterSetRemapper {
    override protected def remap(prev: Char, curr: Char, next: Char): Int =
      remapControlOrLineEndingToVisibleGlyphs(curr)
  }

  /**
   * For debugger displays, data dumps, etc.
   *
   * Control characters, line-endings, spaces, and various others do not have a glyph that is displayed.
   * They also can cause text to be split across lines, bells to ring, characters to be overwritten, etc.
   *
   * Convert to a character that has a glyph. Unicode provides some control-picture
   * characters for this purpose. Note that this by-definition loses information, and many characters
   * will map to the unicode replacement character. It is expected this display would be complemented
   * by a hex dump or other means of understanding the actual representation of these remapped
   * characters.
   *
   * Keep in mind this is a Unicode to Unicode transformation. It is not starting from byte values.
   * See `byteToChar(b)` for how to got from raw byte values to unicode chars.
   * @param c a unicode character that may or may not have a glyph.
   * @param replaceControlPictures when true means the Unicode control pictures characters are replaced by
   *                               the Unicode replacement character. When false these characters are preserved.
   *                               Defaults to false.
   * @return a unicode character that definitely has a glyph.
   */
  def remapControlOrLineEndingToVisibleGlyphs(
    c: Char,
    replaceControlPictures: Boolean = false
  ): Char = {
    val URC =
      0x2426 // Unicode control picture character for substitution (also looks like arabic q-mark)
    val code: Int = c.toInt match {
      //
      // C0 Control pictures
      case n if (n <= 0x1f) => n + 0x2400
      case 0x20 => 0x2423 // For space we use the SP we use the ␣ (Unicode OPEN BOX)
      case 0x7f => 0x2421 // DEL pic isn't at 0x247F, it's at 0x2421
      //
      // We remap these into the Unicode Latin Extended B codepoints by
      // adding 0x100 to their basic value.
      //
      case n if (n >= 0x80 && n <= 0x9f) =>
        n + 0x100
      case 0xa0 => 0x2422 // non-break space => ␢ (blank symbol or little b with stroke)
      case 0xad => 0x002d // soft hyphen => hyphen
      //
      // Unicode separators & joiners
      case 0x200b => URC // zero width space
      case 0x2028 => URC // line separator
      case 0x2029 => URC // paragraph separator
      case 0x200c => URC // zero width non-joiner
      case 0x200d => URC // zero width joiner
      case 0x2060 => URC // word joiner
      // bi-di controls
      case 0x200e | 0x200f => URC
      case b if (b >= 0x202a && b <= 0x202e) => URC
      // byte order mark
      case 0xfffe => URC // ZWNBS aka Byte Order Mark
      case 0xffff => URC // non-character FFFF
      // we assume surrogate codepoints all have a glyph (depends on font used of course)
      //
      // TODO: this could go on and on. There's a flock of 'space' characters (EM SPACE)
      // all over the place in Unicode.
      //
      // TODO: combining characters,
      // all whitespace, zero-width, and combining/joining characters would be
      // represented by a separate glyph-character.
      //
      // Probably could be done by checking the character against some
      // unicode regex character classes like \p{M} which is the class
      // of combining mark characters
      //
      //
      // Special case - if incoming character is one of the glyph
      // characters we're remapping onto, then we could issue
      // a substitution character, but there are things that depend
      // on these being preserved. So we have a flag to control this.
      //
      case n if (n > 0x2400 && n < 0x2423 && replaceControlPictures) => URC
      case x => x
    }
    code.toChar
  }

  private lazy val byteToCharTable = {
    val cs = Charset.forName("windows-1252")
    val dec = cs
      .newDecoder()
      .onUnmappableCharacter(CodingErrorAction.REPLACE)
      .onMalformedInput(CodingErrorAction.REPORT)
    val bb = ByteBuffer.wrap((0 to 255).map { i => i.toByte }.toArray)
    val cb = dec.decode(bb)
    assert(cb.position() == 0)
    assert(cb.limit() == 256)
    // These 5 are unmapped by Windows-1252 but we want to turn any
    // byte into a legit character. So these We add 0x100
    // to get unicode codepoints.
    cb.put(0x81, 0x181.toChar)
    cb.put(0x8d, 0x18d.toChar)
    cb.put(0x8f, 0x18f.toChar)
    cb.put(0x90, 0x190.toChar)
    cb.put(0x9d, 0x19d.toChar)
    val res = cb.toString
    res
  }

  /**
   * Convert a byte to a unicode character assuming the byte is iso-8859-1
   * (or really, windows-1252 which has a few more glyph chars but is otherwise
   * the same as iso-8859-1)
   *
   * This is a super pain to do using Java charsets because they
   * don't provide an API to convert one character, only byte buffers
   * into char buffers.
   *
   * So we just use a lookup table.
   * @param b a byte containing a code point of windows-1252 encoding
   * @return a unicode equivalent character
   */
  def byteToChar(b: Byte): Char = {
    byteToCharTable(UByte(b).toInt)
  }

  /**
   * This function creates a representation of data which doesn't
   * contain any control or whitespace characters that jump around the screen.
   * It replaces those with characters that have a simple glyph.
   *
   * The point of this is when you display the stream of data for
   * debugging, or for a diagnostic message, and it is mostly single-byte text
   * characters, then the characters which control position like CR, LF, FF,
   * VT, HT, BS, etc. all make it hard to figure out what is going on.
   * Replacing these with the picture characters (designed for this purpose)
   * in the unicode x2400 block helps.
   */
  def remapBytesToStringOfVisibleGlyphs(ba: Array[Byte]): String = {
    val len = ba.length
    if (len == 0) ""
    else {
      val sb = new StringBuilder(ba.length)
      var i: Int = 0
      while (i < ba.length) {
        sb.append(remapControlOrLineEndingToVisibleGlyphs(byteToChar(ba(i))))
        i += 1
      }
      sb.toString()
    }
  }

  def remapOneByteToVisibleGlyph(b: Byte) =
    remapControlOrLineEndingToVisibleGlyphs(byteToChar(b))

  /**
   * True if this charset encoding is suitable for display using the
   * all-visible-glyph stuff above.
   */
  def isAsciiBased(csName: String): Boolean = isAsciiBased(JavaCharset.forName(csName))

  def isAsciiBased(cs: JavaCharset): Boolean = {
    val aliases = cs.aliases().asScala.map { _.toUpperCase }
    val byName =
      aliases.exists { s =>
        !(s.contains("7-BIT")) &&
        !(s.contains("EBCDIC")) && (s.startsWith("ASCII") ||
          s.startsWith("US-ASCII") ||
          s.startsWith("ISO-8859") ||
          s.startsWith("UTF"))
      }
    if (byName) byName
    else {
      val decoder = cs.newDecoder()
      decoder.onMalformedInput(CodingErrorAction.REPLACE)
      decoder.onUnmappableCharacter(CodingErrorAction.REPLACE)
      val abcBytes = "abc".getBytes("ascii")
      val bb = ByteBuffer.wrap(abcBytes)
      val cb = decoder.decode(bb)
      val abc = cb.toString()
      if (abc =:= "abc") true
      else false
    }
  }

  /**
   * convenience method
   */
  def writeToFile(fileName: String)(body: java.io.Writer => Unit): Unit = {
    val writer = new java.io.FileWriter(fileName)
    Using.resource(writer) { body(_) }
  }

  /**
   * Detects the encoding of the File for us.
   *
   * This was needed for the ConstructingParser
   * in order to read in xml from a file.
   */
  def determineEncoding(uri: URI): String = {
    val encH = scala.xml.include.sax.EncodingHeuristics
    val is = uri.toURL.openStream()
    val bis = new java.io.BufferedInputStream(is)
    val enc = encH.readEncodingFromStream(bis)
    is.close()
    enc
  }

  def determineEncoding(file: File): String = determineEncoding(file.toURI)

  /**
   * CharSequence to String
   */
  def csToString(cs: CharSequence): String = {
    cs match {
      case s: String => s
      case _ => {
        val sb = new StringBuilder(cs.length())
        sb.append(cs)
        sb.mkString
      }
    }
  }

  /**
   * Java throwable/exception objects may or may not have a message. They are supposed to have a cause if they
   * don't have a message of their own, but might have neither, or might have both.
   *
   * This is too painful to deal with in code when you want to be generic about converting throws/exceptions
   * into diagnostic information.
   *
   * So we have a more uniform behavior. Never returns null. Always gets a message.
   * If the argument has none, but has a cause object, then it
   * gets the message from that, if that has no message, it chases further.
   * Ultimately, if there's no message, it just uses the innermost cause object's class name.
   */
  def getAMessage(th: Throwable): String = {
    val m = th.getMessage()
    val c = th.getCause()
    val res = (m, c) match {
      case (null, null) => Misc.getNameFromClass(th)
      case ("", null) => Misc.getNameFromClass(th)
      case (m, null) => m
      case (null, c) => getAMessage(c)
      case (m, c) => {
        val cmsg = getAMessage(c)
        cmsg + " (within " + th.getClass.getSimpleName + " " + m + ")"
      }
    }
    Assert.invariant(res != null)
    res
  }

  def getSomeMessage(th: Throwable): Some[String] = Some(getAMessage(th))

  def getACause(th: Throwable): Throwable = {
    val c = th.getCause()
    val res = c match {
      case null => th
      case _ => getACause(c)
    }
    res
  }

  def getSomeCause(th: Throwable): Some[Throwable] = Some(getACause(th))

  /**
   * Get the diagnosticFilepath from a uri
   */
  def uriToDiagnosticFile(uri: URI): File = {
    uri.getScheme match {
      case "jar" => {
        val (_, pathPart) = Misc.splitJarUri(uri)
        Paths.get(pathPart).toFile
      }
      case "file" => Paths.get(uri).toFile
      case "resource" => {
        val resourceFilePart = uri.getPath
        Paths.get(resourceFilePart).toFile
      }
      case _ => Paths.get(uri.getPath).toFile
    }
  }

  /**
   * scala 3 changes the way lazy val are deserialized so it looks like fieldName$lzy1
   * This is a helper function for lazy and non-lazy fields and to keep from 
   * cluttering the code.
   */
  def lookupDeclaredField(clazz: Class[?], fieldName: String): Field = {
    try {
      clazz.getDeclaredField(fieldName)
    } catch {
      case _: NoSuchFieldException =>
        // scala 3 changes the way lazy val are deserialized so it looks like fieldName$lzy1
        clazz.getDeclaredField(fieldName + "$lzy1")
    }
  }

}
