

package dbmodel

  /** TSVModel, version 11.1
   *
   *  TSVModel offers three simple ways to define mapping between
   *  TSV/CSV files on disk and in-memory data types: 
   *  1/ A direct mapping of TSV/CSV as a collection of untyped 
   *     arrays of Int/Double/String, one array per row of the
         TSV/CSV file.
   *  2/ A mapping of TSV/CSV as a collection of Remy records,
   *     one record per row of the TSV/CSV file. A Remy record
   *     is basically an array coupled to a directory that maps
   *     field name/column heading to position in the array.
   *  3/ Flexible and more complex mapping based on encoder/decoder
   *     between an untyped array representing a row in the TSV/CSV
   *     file and a user-provided data type.
   *
   *  Wong Limsoon
   *  13 May 2023
   */


  object TSVModel:

    import dbmodel.OrderedCollection.{ OSeq, Key }
    import dbmodel.Synchronizable.CBI
    import dbmodel.DBFile.{ OFile, OTransient, PARSER, FORMATTER, HasOFile }
    import dbmodel.DBFile.{ Ord, transientOFile }
    import dbmodel.DBFile.TMP   // Default tmp folder to use.
    import scala.language.implicitConversions
    import scala.annotation.targetName
    import scala.reflect.ClassTag

    type Bool = Boolean


    /** The expected format of a tab-delimited file (TSV) is:
     *  1st line is header which is a tab-delimited sequence of
     *  the form f:t, where
     *    f is field name, 
     *    t is field type (Int, Double, String).
     *  Each subsequent line of the file is a tab-delimited 
     *  record having the corresponding number of field values.
     *
     *  When the tabs are all replaced by commas, we get the
     *  format for comma-delimited file (CSV).
     */

    type TSV = Array[Any]                        // A row of TSV/CSV file
    def tsv(e: Any*): Array[Any] = Array(e: _*)

    type ENCODER[B] = B => TSV                   // Encode/decode between
    type DECODER[B] = (TSV, Int) => B            // TSV/CSV and user's type



    /** [[SCHEMA]] is a type for defining the columns of a [[TSV]] file.
     *  [[FIELDNAME]] is the name of a column.
     *  [[FIELDTYPE]] is the type of column values.
     *  Assume [[TSV]] file is homogeneous; i.e., all rows are the same.
     */

    type SCHEMA = Vector[(FIELDNAME, FIELDTYPE)]
    type FIELDNAME = String
    enum FIELDTYPE { case INT, DOUBLE, STRING }



    /** Support functions to make it easy for users to convert
     *  between [[TSV]] and their own data types.
     */

    case class BadFieldTypeException(e: Any) extends Throwable


    extension (ty: FIELDTYPE)
      def str: String = ty match
        case FIELDTYPE.INT => "Int"
        case FIELDTYPE.DOUBLE => "Double"
        case FIELDTYPE.STRING => "String"

      def cvt(e: String): Any = ty match
        case FIELDTYPE.INT => e.toInt
        case FIELDTYPE.DOUBLE => e.toDouble
        case FIELDTYPE.STRING => e


    val fieldType = Map(
      "Int" -> FIELDTYPE.INT,
      "Double" -> FIELDTYPE.DOUBLE,
      "String" -> FIELDTYPE.STRING)


    extension (e: Any)
      def asInt = e.asInstanceOf[Int]
      def asDbl = e.asInstanceOf[Double]
      def asStr = e.asInstanceOf[String]
      def ty    = e match
        case _: Int    => FIELDTYPE.INT
        case _: Double => FIELDTYPE.DOUBLE
        case _: String => FIELDTYPE.STRING
        case _ => throw BadFieldTypeException(e)
      def toDbl = e match
        case i: Int    => i.toDouble
        case d: Double => d
        case s: String => s.toDouble
        case _ => throw BadFieldTypeException(e)
      def toInt = e match
        case i: Int    => i
        case d: Double => d.toInt
        case s: String => s.toInt
        case _ => throw BadFieldTypeException(e)
      def toStr = e match
        case i: Int    => i.toString
        case d: Double => d.toString
        case s: String => s
        case _ => throw BadFieldTypeException(e)


    extension (e: String)
      def asUntypedObj: Any =  
        try e.toInt catch { case _ => 
          try e.toDouble catch { case _ => 
            e 
          } 
        }

      def guessType: FIELDTYPE = e.asUntypedObj.ty


    extension (tsv: TSV)
      def int(n: Int, default: Int = 0): Int =
        val v = tsv(n)
        if v.ty == FIELDTYPE.INT then v.asInstanceOf[Int] else default 

      def dbl(n: Int, default: Double = 0.0): Double =
        val v = tsv(n)
        if v.ty == FIELDTYPE.DOUBLE then v.asInstanceOf[Double] else default 

      def str(n: Int, default: String = ""): String =
        val v = tsv(n)
        if v.ty == FIELDTYPE.STRING then v.asInstanceOf[String] else default 



    /** [[ TSVSchema[A] ]] is a trait describing how to encode an
     *  list of objects of type [[A]] into a tab-delimited file.
     */

    trait TSVSchema[A]:
      def schema: SCHEMA
      def encoder: ENCODER[A]
      def decoder: DECODER[A] = (tsv: TSV, p: Int) => make(tsv)
      def make(tsv: TSV): A
      def apply[R>:Array[Matchable]](tsv: R): A =
        make(tsv.asInstanceOf[Array[Any]])
        

    /** "Pass-thru" encoders/decoders that simply pass a TSV file thru.
     *  I.e., a row of a TSV file is simply an untyped array.
     */

    case class SimpleTSVSchema(schema: SCHEMA) extends TSVSchema[TSV]:
      val encoder = (tsv: TSV) => tsv
      def make(tsv: TSV) = tsv



    /** TSV database connectivity. 
     *
     *  [[ d: TSVdbc[B](formatter, parser) ]] constructs a reader/writer
     *  [[d]] for mapping a collection of type [[B]] objects to a TSV file.
     *  [[d.connect(key,filename,folder)]] reads the file to build the
     *  collection of [[B]] objects.
     *
    case class KDTree[B](left: KDTree[B], right: KDTree[B], items: List[B])
     *  A user defines [[formatter]] and [[parser]] to suit his custom-made
     *  mapping between his data type [[B]] and its TSV/CSV representation
     *  on disk.
     */

    case class TSVdbc[B](
            override val formatter: FORMATTER[B],
            override val parser: PARSER[B])
        extends HasOFile[B]:

      def connect[K](
              key: Key[B,K],
              filename: String, 
              folder: String = TMP)
           : OFile[B,K] =
        val fname = OFile.mkFileName(filename, folder).toString
        OFile[B,K](key, fname)(using this)


    object TSVdbc: 

      /** TSVdbc constructors for TSV files with and without
       *  a header row.
       */

      def apply[B](
              schema: SCHEMA, 
              encoder: ENCODER[B], 
              decoder: DECODER[B],
              delimiter: TSVSupport = DelimitByTab, 
              guard: (String, Int, Int) => Bool = null)
          : TSVdbc[B] =
        import delimiter.{ genFormatter, genParserSkipHeader, genHeader }
        val formatter = genFormatter(schema, encoder, genHeader)
        val parser = genParserSkipHeader(schema, decoder, guard)
        TSVdbc(formatter, parser)


      def noHeader[B](
              schema: SCHEMA, 
              encoder: ENCODER[B], 
              decoder: DECODER[B],
              delimiter: TSVSupport = DelimitByTab, 
              guard: (String, Int, Int) => Bool = null)
          : TSVdbc[B] =
        import delimiter.{ genFormatter, genParser, genHeader }
        val formatter = genFormatter(schema, encoder, genHeader)
        val parser = genParser(schema, decoder, guard)
        TSVdbc(formatter, parser)

    end TSVdbc



    /** [[ TSVFile[A](sch: TSVSchema[A]) ]] provides a mapping
     *  between a collection of [[A]] and a [[TSV]] file. I.e., it
     *  uses [[sch.encoder]] to encode each item in the collection
     *  into a TSV, and uses [[sch.decoder]] to decode each TSB into
     *  an item.
     *
     *  This is a higher-level mapping between user's data type [[A]]
     *  and the [[TSV]] file. Basically, we assume there is a more or
     *  less 1-to-1 correspondence between components of [[A]] and
     *  columns of the [[TSV]] file. This way, the [[formatter]]
     *  and [[parser]] for the underlying [[ TSVdbc[A] ]] can be
     *  constructed automatically. The automatic construction is done
     *  in the [[ HasTSVFile[A] ]] trait.
     */
    
    case class TSVFile[A](
          sch: TSVSchema[A],
          delimiter: TSVSupport,
          guard: (String, Int, Int) => Bool)
        extends HasTSVFile[A](sch, delimiter, guard):
  
      /** Default TSV db connectors, constructed via [[HasTSVFile]] trait.
       */
      val dbc = TSVdbc(formatter, parser)

      val dbcNoHeader = TSVdbc(formatter, parserNoHeader)


      /** Read a file that has header, using default TSV db connector
       */
      def apply(filename: String, folder: String = TMP): OFile[A,Unit]  =
        val key = Key((x: A) => (), Ordering[Unit])
        dbc.connect(key, filename, folder)


      /** Read a file that has no header, using either default
       *  TSV db connector or a user-customized one. Only simple
       *  customizations are supported (changing column [[delimiter]]
       *  and skipping some rows using a [[guard]] filter.)
       *
       *  @param delimiter  The delimiter to use. Two are provided,
       *                      [[DelimitByTab]] and [[DelimitByComma]],
       *                      but others can be defined via [[TSVSupport]].
       *  @param guard      The guard function to skip rows.
       *                      [[guard(s, l, p)]] tests the current line
       *                      [[s]], its line number [[l]], and item 
       *                      number (i.e., the output) that the parser
       *                      is currently trying to construct.  
       */
      def noHeader(
              filename: String, 
              folder: String = TMP,
              delimiter: TSVSupport = null,
              guard: (String, Int, Int) => Bool = null)
          : OFile[A,Unit]  =
        val key = Key((x: A) => (), Ordering[Unit])
        val dbc =
          if delimiter == null && guard == null then dbcNoHeader
          else TSVdbc.noHeader(
            sch.schema, sch.encoder, sch.decoder,
            delimiter, guard)
        dbc.connect(key, filename, folder)


      /** Map a transient collection to a TSV file.
       *  Not needed any more, since it can be done quite easily
       *  as [[es.transientOFile(key)(using tsvFile)]],
       *  where [[ es: IterableOnce[A] ]] and [[ tsvFile: TSVFile[A] ]].
       *
       {{{

      def transientTSVFile[K](
             es: IterableOnce[A], 
             key: Key[A,K],
             delimiter: TSVSupport = this.delimiter)
          : OFile[A,K] =
        import delimiter.{ genFormatter, genParserSkipHeader, genHeader }
        val formatter = genFormatter(sch.schema, sch.encoder, genHeader)
        val parser = genParserSkipHeader(sch.schema, sch.decoder)
        val cbi  = CBI(es)
        OTransient[A,K](key, cbi, parser, formatter, OFile.destructorOFile _)

       }}}
       *
       */


    object TSVFile:
      
      /** Constructors for TSV and CSV file readers
       */

      def apply[A](sch: TSVSchema[A]): TSVFile[A] =
        TSVFile(sch, DelimitByTab, null)

      def apply[A](
              sch: TSVSchema[A], 
              guard: (String, Int, Int) => Bool): TSVFile[A] =
        TSVFile(sch, DelimitByTab, guard)


      def CSVFile[A](
              sch: TSVSchema[A], 
              guard: (String, Int, Int) => Bool = null): TSVFile[A] =
        TSVFile(sch, DelimitByComma, guard)


      /** Map a transient collection to a TSV file.
       *  Not needed any more...
       *
       {{{

      def transientTSVFile[K](
             es: CBI[TSV], 
             key: Key[TSV,K],
             delimiter: TSVSupport = DelimitByTab)
          : OFile[TSV,K] =
        val schema: SCHEMA = 
          if es.isEmpty then Vector()
          else delimiter.guessSchema(es.head)
        val sch = SimpleTSVSchema(schema)
        TSVFile(sch, delimiter, null).transientTSVFile(es, key)

       }}}
       *
       */

    end TSVFile


    /** Connectors for plain untyped TSV & CSV files.
     *  [[PlainTSVFile]] is set as the preference over
     *  [[PlainCSVFile]].  
     */

    given PlainTSVFile: TSVFile[TSV] = TSVFile(SimpleTSVSchema(null))


    lazy val PlainCSVFile: TSVFile[TSV] = TSVFile.CSVFile(SimpleTSVSchema(null))



    /** Make it easier to convert between [[OSeq]] and [[TSVFile]].
     *  This is somewhat redundant in principle because [[.transientOFile]]
     *  does the same thing. But there are two reasons; 1/ this way,
     *  user does not need to import [[.transientOFile]] from
     *  [[dbmodel.DBFile]], and 2/ user gets a clearer method name.
     */

    extension [B,K](entries: OSeq[B,K])
      def transientTSVFile(using TSVFile[B]): OFile[B,K] = 
        entries.transientOFile
 

    extension [B](entries: IterableOnce[B])
      def transientTSVFile(using TSVFile[B]): OFile[B,Unit] = 
        entries.transientOFile
 
      def transientTSVFile[K](key: Key[B,K])(using TSVFile[B]): OFile[B,K] = 
        entries.transientOFile(key)

      def transientTSVFile[K:Ord](key: B => K)(using TSVFile[B]): OFile[B,K] = 
        entries.transientOFile(key)



    /** Once [[ HasTSVFile[B] ]] trait is enabled for a type [[B]],
     *  a number of extension functions can be used on [[OSeq[B,K]]]
     *  and [[IterableOnce[B]]] objects; see listed below. More importantly,
     *  this trait constructs the [[formatter]] and [[parser]] for
     *  TSV/CSV file automatically for type [[B]] objects.
     *
     {{{
          it.transientOFile: OFile[B,K]
          it.transientOFile(key: Key[B,K]): OFile[B,K]
          it.transientOFile(key: B => K): OFile[B,K]
     }}}
     *
     */

    trait HasTSVFile[B](
          sch: TSVSchema[B], 
          delimiter: TSVSupport,
          guard: (String, Int, Int) => Bool = null)
        extends HasOFile[B]:

      import delimiter.{ genFormatter, genParserSkipHeader, genParser, genHeader }
      import sch.{ schema, encoder, decoder }

      override val formatter = genFormatter(schema, encoder, genHeader)

      override val parser = genParserSkipHeader(schema, decoder, guard)
  
      val parserNoHeader = genParser(schema, decoder, guard)

    end HasTSVFile

      

    /** Support for parsing & formatting [[TSV]] file.
     *  Provides some flexibility in terms of delimiters.
     */

    val DelimitByTab: TSVSupport = TSVSupport("\t")

    val DelimitByComma: TSVSupport = TSVSupport(",")


    case class TSVSupport(separator: String = "\t"):

      /** Get schema/header from a file
       */

      def getHeader(
             filename: String, 
             folder: String = TMP)
          : Vector[(String,String)] =
        val fname  = OFile.mkFileName(filename, folder).toString
        val file   = scala.io.Source.fromFile(fname)
        val header = file.getLines().next().trim()
        file.close()
        (header split separator)
          .map { _.split(":") }
          .map { a => a(0) -> a(1) }
          .toVector


      def getSchema(filename: String, folder: String = TMP): SCHEMA =
        val header = getHeader(filename, folder)
        header.map { case (f, ty) => f -> fieldType(ty) }


      /** If there is no header line, or the header line is incompatible
       *  with our header format, make a guess of the schema by reading
       *  an actual data entry. 
       *
       *  @param n  # of lines occupied by incompatible header; need to skip.
       */

      def guessSchema(filename: String, folder: String=TMP, n: Int=0): SCHEMA =
        val fname  = OFile.mkFileName(filename, folder).toString
        val file   = scala.io.Source.fromFile(fname)
        val line   = file.getLines().drop(n).next().trim()
        file.close()
        guessSchema(line.split(separator).map(_.asUntypedObj))


      def guessSchema(tsv: TSV): SCHEMA =  
        { for i <- 0 to tsv.length - 1 yield i.toString -> tsv(i).ty }.toVector
        

      /** Generate a formatter given schema, header generator, & encoder
       */

      def genHeader(schema: SCHEMA): String =
        def genField(f: FIELDNAME, t: FIELDTYPE) = s"$f:${t.str}"
        (schema map genField).mkString(separator)

      def genHeader[B](schema: SCHEMA, tsv: TSV): String = 
        if schema != null then genHeader(schema)
        else genHeader(guessSchema(tsv))

      
      def genFormatter[B](schema: SCHEMA, encoder: ENCODER[B]): FORMATTER[B] =
        genFormatter(schema, encoder, null)

      def genFormatter[B](
              schema: SCHEMA, 
              encoder: ENCODER[B],
              genHeader: (SCHEMA, TSV) => String)
          : FORMATTER[B] = (options: String) =>
        def entry(b: B) = encoder(b).map(_.toString).mkString(separator)
        def format(b: B, position: Int): String =
          if position != 0 then entry(b)
          else if genHeader == null then entry(b)
          else s"${genHeader(schema, encoder(b))}\n${entry(b)}"
        format


      /** Generate a parser given schema, guard, & decoder
       *  The guard takes the current line, current line #, and
       *  current item #, and decide whether to skip the current line.
       *
       *  The parser relies on being given a schema (i.e., what the
       *  types of the columns of the TSV/CSV file are) to do parsing
       *  correctly and efficiently.  However, sometimes the schema
       *  is not given. When this happens, the parser uses the first
       *  row of data to guess the schema and assumes that the rest
       *  of the rows are same as the first row in terms of the type
       *  of values in the columns.
       */

      def genParser[B](
              schema: SCHEMA,
              decoder: DECODER[B],
              guard: (String, Int, Int) => Bool = null)
          : PARSER[B] = {  

        (options: String) => {

          var _schema = schema
          var getter = if _schema != null then _schema.map(_._2.cvt) else null
          var size = if _schema != null then _schema.length else 0
          var lno    = 0

          def parse(it: Iterator[String], position: Int): B =
            var line        = ""
            var parsed: TSV = null
            var continue    = it.hasNext

            while continue do
              line = it.next()
              lno  = lno + 1
              if guard == null || guard(line, lno, position) then
                val en = line split separator
                parsed = 
                  if _schema == null then
                    val tmp = en.map { _.asUntypedObj }
                    _schema = guessSchema(tmp)
                    getter = _schema.map(_._2.cvt)
                    size = _schema.length
                    tmp
                  else Array.tabulate(size) { n => getter(n)(en(n)) }
                continue = false
              else continue = it.hasNext 

            if parsed == null then throw new java.io.EOFException("")
            else return decoder(parsed, position)

          parse
        }
      }

 

      /** Generate a parser that ignores header line.
       */

      def genParserSkipHeader[B](
              schema: SCHEMA, 
              decoder: DECODER[B],
              guard: (String, Int, Int) => Bool = null)
          : PARSER[B] = {

        genParser(
          schema, 
          decoder, 
          (l, n, p) => n > 1 && (guard == null || guard(l, n, p)))
      }

    end TSVSupport 



    /** Remy records. Uses a Map to provide named access to TSV.
     */

    case class Remy(remy: Map[String,Int], tsv: TSV) extends Selectable:

      def apply[A](f: String): A = tsv(remy(f)).asInstanceOf[A]
      def apply[A](i: Int): A = tsv(i).asInstanceOf[A]
      def selectDynamic(f: String): Any = tsv(remy(f))
      def int(f: String): Int = tsv(remy(f)).asInt
      def dbl(f: String): Double = tsv(remy(f)).asDbl
      def str(f: String): String = tsv(remy(f)).asStr
      def idx(f: String): Int = remy(f)
      def get[A](i: Int): A = tsv(i).asInstanceOf[A] 

      def schema: SCHEMA =
        val inv = remy map { case (x, y) => (y, x) }
        val mx = tsv.length - 1
        (for (i <- 0 to mx) yield (inv(i) -> tsv(i).ty)).toVector
    

    object Remy: 

      /** Make Remy index
       */

      def remyIndex(sch: SCHEMA): Map[String,Int] =
        val mx = sch.length - 1
        val ix = for (i <- 0 to mx) yield (sch(i)._1 -> i)
        ix.toMap

      def remyIndex(fields: String*): Map[String,Int] =
        val mx = fields.length - 1
        val ix = for (i <- 0 to mx) yield (fields(i) -> i)
        ix.toMap
          

      /** Alternative constructor. Builds Remy directory from schema.
       */

      def apply(sch: SCHEMA, tsv: TSV): Remy = Remy(remyIndex(sch), tsv)

      def apply(sch: SCHEMA): TSV => Remy = 
        val remy = remyIndex(sch)
        (tsv: TSV) => Remy(remy, tsv)


      /** Not needed any more.
       *
       {{{ 

      @targetName("mkRemy1")
      def apply(sch: SCHEMA, tsv: Array[_]): Remy =
        Remy(sch, try tsv.asInstanceOf[Array[Any]] catch _ => tsv.toArray[Any])

      @targetName("mkRemy2")
      def apply(sch: Map[String,Int], tsv: Array[_]): Remy =
        Remy(sch, try tsv.asInstanceOf[Array[Any]] catch _ => tsv.toArray[Any])

       }}}
       *
       */

    end Remy



    /** [[ RemyTSVFile[R<:Remy](using sch:RemySchema[A]) ]] is a
     *  mapping between a collection of [[R]] and a [[TSV]] file.
     *  It uses [[sch.encoder]] to encode each item in the collection
     *  into a TSV, and uses [[sch.decoder]] to decode each TSV
     *  into an item.
     */

    case class RemySchema[R<:Remy](schema: SCHEMA) extends TSVSchema[R]:
       val remy =  
         val sch = for (i <- 0 to schema.length - 1) yield (schema(i)._1 -> i) 
         sch.toMap 
       def make(tsv: TSV): R = Remy(remy, tsv).asInstanceOf[R]
       def encoder: ENCODER[R] = (r: R) => r.tsv
      

    type RemyFile[R<:Remy,K] = OFile[R,K]   // User-level, file of Remy records
    type RemyTSVFile[R<:Remy] = TSVFile[R]  // Under the hood, a TSV file.

    /** The terminology [[ RemyTSVFile[R] ]] is recommended when
     *  defining/constructing database connector, corresponding
     *  to the "under the hood" perspective.
     */ 
    def RemyTSVFile[R<:Remy](using sch: RemySchema[R]) = TSVFile(sch)

    /** The terminology [[ RemyFile[R](filename) ]] is recommended
     *  when reading a file, corresponding to user-level perspective.
     */
    def RemyFile[R<:Remy](using sch: RemySchema[R]) = TSVFile(sch)


    /** Converting [[OSeq]] and [[IterableOnce]] to [[RemyFile]] 
     *  for writing to disk. These are a bit redundant, since
     *  [[transientOFile]] does the same thing. But for reasons
     *  similar to [[transientTSVFile]], we provide also
     *  [[transientRemyFile]] methods.
     */
    extension [R<:Remy,K](entries: OSeq[R,K])
      def transientRemyFile(using TSVFile[R]): OFile[R,K] =
        entries.transientOFile
 

    extension [R<:Remy](entries: IterableOnce[R])
      def transientRemyFile(using TSVFile[R]): OFile[R,Unit] =
        entries.transientOFile
 
      def transientRemyFile[K](key: Key[R,K])(using TSVFile[R]): OFile[R,K] = 
        entries.transientOFile(key)

      def transientRemyFile[K:Ord](key: R => K)(using TSVFile[R]): OFile[R,K] = 
        entries.transientOFile(key)

  end TSVModel



/** Examples **********************************************
 *
 *
{{{

    import dbmodel.TSVModel.FIELDTYPE.*
    import dbmodel.TSVModel.{ *, given }
    import dbmodel.OrderedCollection.Key
    import dbmodel.Synchronizable.CBI
    import scala.language.implicitConversions



  //========================================================
  // Here is an example to demo Remy records and
  // mapping them to TSV files
  //========================================================

    type Person = Remy {
      val name: String
      val age: Int
      val sex: String
    }


  // Naming the RemySchema below 'person' instead of 'PersonSchema'
  // has a purpose: This automatically also define a constructor
  // for the Person type, person: TSV => Person. If not for this,
  // it is more natural to declare this as:
  //  
  //    given PersonSchema: RemySchema[Person] = ...
  //
  // The RemySchema trait auto-constructs the encode/decode between
  // Remy records and TSV.

    given person: RemySchema[Person] = {
      val sch = Vector("name" -> STRING, "age" -> INT, "sex" -> STRING)
      RemySchema(sch)
    }  


  // This provides connector to Person files.
  // Use PersonFile(filename) to read Person files.

    given PersonFile: RemyTSVFile[Person] = RemyTSVFile[Person]

  

  // Define a list of TSV and turn them into Person objects.
  // See the use of the person(_) constructor. 
  // If earlier we had declared 'given PersonSchema: ...',
  // the constructor would become PersonSchema(_).

    val wls  = tsv("WLS", 50, "m")
    val lsq  = tsv("LSQ", 40, "f")
    val wms  = tsv("WMS", 10, "m")
    val family = List(wms, lsq, wls).map(person(_))


  // Turn that list of Person objects into a transient RemyFile.
  // This lets the system know how to map them to disk later.

    val db: RemyFile[Person, Int] = family.transientRemyFile { x => x.age }

 
  // Do a query on the file, e.g. get the age of the first Person
  // in the file. Then save the file and name it "xx".
  // Note the use of structural subtype, so that the function f
  // is defined on any Remy object that has the age field.

    def f[R<:Remy{val age:Int},K](r:RemyFile[R,K]) = r.head.age

    f(db)

    db.saveAs("xx")


  // These are some ways to read Person file
  //
  // val xx = RemyTSVFile[Person]("xx")
  // val xx = RemyFile[Person]("xx")
  // val xx = PersonFile("xx")

    val xx = PersonFile("xx")

    xx.length
    xx.map(x => x.name).toVector
    xx.protection(false).close()
  
  
  // "apersonfile" is an example headerless file of Persons
  // It is readable using the PersonFile connector like this...

    val yy = PersonFile.noHeader("apersonfile")

    yy.length
    yy.map(x => x.name).toVector
    (yy(1).name, yy(1).age, yy(1).sex)
    yy.close()


  //==========================================================
  // This is an example to show mapping between a more typical
  // case-class type to TSV on disk.
  //==========================================================

  // Let's define a People type using TSV directly instead of via
  // Remy records. Then, explicit encode/decode to/from TSV has to
  // be provided via methods of the TSVSchema trait...
  //
  //    schema: Vector[(FIELDNAME, FIELDTYPE)]
  //    make(TSV): People
  //    encoder: People => TSV

    case class People(name: String, age: Int, sex: String)


    val PeopleSchema: TSVSchema[People] = new TSVSchema[People] {
      val schema = Vector("name" -> STRING, "age" -> INT, "sex" -> STRING)
      def make(tsv: TSV) = People(tsv(0).asStr, tsv(1).asInt, tsv(2).asStr)
      def encoder = (r: People) => tsv(r.name, r.age, r.sex)
    }
  
    val PeopleFile = TSVFile[People](PeopleSchema)


  // "apersonfile" is an example headerless file of Person objects
  // As Person and People actually have the same underlying TSV,
  // we can read it using PeopleFile connector, instead of PersonFile.

    val yy = PeopleFile.noHeader("apersonfile")

    yy.length
    yy.map(x => x.name).toVector
    (yy(1).name, yy(1).age, yy(1).sex)
    yy.close()



  //==========================================================
  // Finally, here is an example of using plain TSV directly.
  // As TSV is an untyped array, no schema and mapping needs
  // to be provided.
  //==========================================================

  // Define list of arrays of Int and map to plain TSV
  // For plain TSV, schema does not need to be defined
  // in advance...

    def arrays = {
      val a1 = tsv(1, 2, 3, 4, 5, 6)
      val a2 = tsv(10, 20, 30, 40, 50, 60)
      val a3 = tsv(100, 200, 300, 400, 500, 600)
      List(a1, a2, a3)
    }


  // Create a sorting key for the file. 
  // Note that this is optional.

    val arraysKey: Key[TSV,Int] = Key.asc((a: TSV) => a(0).asInstanceOf[Int])


  // Use transientTSVFile to convert the list of TSVs to file,
  // and name the file "zzzz".

    arrays.transientTSVFile(arraysKey).saveAs("zzzz")

 
  // The default format is TSV. So, we can read the file back
  // using PlainTSVFile and do something to it.

    val zzzz = PlainTSVFile("zzzz")
    zzzz(1)
    zzzz.protection(false).close()


  // If we want to write it as CSV file, then need to explicitly
  // use PlainCSVFile as the connector. The short way is:
  //
  //     arrays.transientTSVFile(arraysKey)(using PlainCSVFile)
  //
  // The long way is like this...

    val tmp = {
      given ofile: TSVFile[TSV] = PlainCSVFile
      arrays.transientTSVFile(arraysKey)
    }

    val saved = tmp.saveAs("xxxx")

  
  // Read it back using PlainCSVFile and do something to do. 

    val xxxx = PlainCSVFile("xxxx")
    xxxx(1)
    xxxx.protection(false).close()
  

}}}
 *
 */



