
package gmql

/** Version 11.1
 *
 *  Wong Limsoon
 *  20 May 2023
 */


  object GenomeLocus:

    import scala.language.implicitConversions


    type Bool   = Boolean                  // Shorthand
    type PRED   = (Locus, Locus) => Bool   // Shorthand


    case class Locus(chrom: String, start: Int, end: Int):

      import Locus.dLocus

      /** These predicates are antimonotonic wrt isBefore.
       */

      def isBefore(u: Locus): Bool = dLocus.isBefore(this, u)
      def sameChrom(u: Locus): Bool = dLocus.sameChrom(this, u)
      def sameLocus(u: Locus): Bool = this == u
      def sameStart(u: Locus): Bool = dLocus.sameStart(this, u)
      def startBefore(u: Locus): Bool = dLocus.startBefore(this, u)
      def startAfter(u: Locus): Bool = dLocus.startAfter(this, u)
      def overlapStart(u: Locus): Bool = dLocus.overlapStart(this, u)
      def near(n: Int = 1000)(u: Locus): Bool = dLocus.near(n)(this, u)

      /** This predicate is antimonotonic wrt isBefore when n=1
       */

      def overlap(n: Int = 0)(u: Locus): Bool = dLocus.overlap(n)(this, u)

      /** These predicates are not antimonotonic wrt isBefore.
       */

      def sameEnd(u: Locus): Bool     = dLocus.sameEnd(this, u)
      def endBefore(u: Locus): Bool   = dLocus.endBefore(this, u)
      def endAfter(u: Locus): Bool    = dLocus.endAfter(this, u)
      def overlapEnd(u: Locus): Bool  = dLocus.overlapEnd(this, u)
      def inside(u: Locus): Bool      = dLocus.inside(this, u)
      def enclose(u: Locus): Bool     = dLocus.enclose(this, u)
      def outside(u: Locus): Bool     = dLocus.outside(this, u)
      def touch(u: Locus): Bool       = dLocus.touch(this, u)

      /** Other operations on loci.
       */

      def distFrom(u: Locus): Int     = dLocus.intdist(this, u)
      def +(n: Int): Locus            = Locus(chrom, start + n, end + n)
      def -(n: Int): Locus            = Locus(chrom, start - n, end - n)
      def chromStart: Locus           = Locus(chrom, start, start)
      def chromEnd: Locus             = Locus(chrom, end, end)

      def intersect(u: Locus): Option[Locus] = (this outside u) match
        case true  => None
        case false => 
          val st = start max u.start
          val en = end min u.end
          Some(Locus(chrom, st, en))

      def merge(u: Locus): Option[Locus] = (this outside u) match
        case true  => None
        case false => 
          val st = start min u.start
          val en = end max u.end
          Some(Locus(chrom, st, en))


    object Locus: 

      import dbmodel.Predicates.{ HasOrder, HasSize, LINETOPO, HasDistance } 
      import dbmodel.Predicates.Pred

      /** Conversion between [[Locus]] and [[(chrom, start, end)]].
       */

      def unapply(l: Locus): (String, Int, Int) = (l.chrom, l.start, l.end)

      def tupled(p: (String, Int, Int)): Locus =
        val (chrom, start, end) = p
        Locus(chrom, start, end)

    
      /* Predicate framework for [[Locus]]
       */

      trait HasLocus[K]:
        def sameChrom:    (K,K) => Bool
        def isBefore:     (K,K) => Bool
        def sameLocus:    (K,K) => Bool
        def sameStart:    (K,K) => Bool
        def startBefore:  (K,K) => Bool
        def startAfter:   (K,K) => Bool
        def endBefore:    (K,K) => Bool
        def endAfter:     (K,K) => Bool
        def sameEnd:      (K,K) => Bool
        def overlapStart: (K,K) => Bool
        def overlapEnd:   (K,K) => Bool
  
      /** These are provided via [[Predicates.LINETOPO]]
       *
       *    def overlap(n: Int): (K,K) => Bool
       *    def near(n: Int):    (K,K) => Bool
       *    def inside:      (K,K) => Bool
       *    def enclose:     (K,K) => Bool
       *    def outside:     (K,K) => Bool
       *    def touch:       (K,K) => Bool
       */

      def isBefore[K: HasLocus: HasOrder]: Pred[K] =
        val iHL = summon[HasLocus[K]]
        val iHO = summon[HasOrder[K]]
        Pred.mkWithOrder[K](iHL.isBefore)(using iHO)

      def sameChrom[K: HasLocus: HasOrder]: Pred[K] =
        val iHL = summon[HasLocus[K]]
        val iHO = summon[HasOrder[K]]
        Pred.mkWithOrder[K](iHL.sameChrom)(using iHO)

      def sameLocus[K: HasLocus: HasOrder]: Pred[K] =
        val iHL = summon[HasLocus[K]]
        val iHO = summon[HasOrder[K]]
        Pred.mkWithOrder[K](iHL.sameLocus)(using iHO)

      def sameStart[K: HasLocus: HasOrder]: Pred[K] =
        val iHL = summon[HasLocus[K]]
        val iHO = summon[HasOrder[K]]
        Pred.mkWithOrder[K](iHL.sameStart)(using iHO)

      def startBefore[K: HasLocus: HasOrder]: Pred[K] =
        val iHL = summon[HasLocus[K]]
        val iHO = summon[HasOrder[K]]
        Pred.mkWithOrder[K](iHL.startBefore)(using iHO)

      def startAfter[K: HasLocus: HasOrder]: Pred[K] =
        val iHL = summon[HasLocus[K]]
        val iHO = summon[HasOrder[K]]
        Pred.mkWithOrder[K](iHL.startAfter)(using iHO)

      def overlapStart[K: HasLocus: HasOrder]: Pred[K] =
        val iHL = summon[HasLocus[K]]
        val iHO = summon[HasOrder[K]]
        Pred.mkWithOrder[K](iHL.overlapStart)(using iHO)

      def endBefore[K: HasLocus: HasOrder]: Pred[K] =
        Pred.mkGen[K](summon[HasLocus[K]].endBefore)

      def endAfter[K: HasLocus: HasOrder]: Pred[K] =
        Pred.mkGen[K](summon[HasLocus[K]].endAfter)

      def sameEnd[K: HasLocus: HasOrder]: Pred[K] =
        Pred.mkGen[K](summon[HasLocus[K]].sameEnd)

      def overlapEnd[K: HasLocus: HasOrder]: Pred[K] = 
        Pred.mkGen[K](summon[HasLocus[K]].overlapEnd)


      /** Set up the implicits for the predicates above to be used
       *  with [[DBModel.DBSQL]].
       */

      trait DLOCUS extends 
        HasOrder[Locus], HasDistance[Locus], HasSize[Locus],
        LINETOPO[Locus], HasLocus[Locus]


      given dLocus: DLOCUS with
      
        /** The ordering on [[Locus]], which is the lexicographic
         *  ordering on [[(chrom, start, end)]].
         */

        val ord: Ordering[Locus]  = Ordering.by(Locus.unapply)

        /** Distance between two loci.
         */

        def intdist(v: Locus, u: Locus): Int = 
          if v.chrom != v.chrom then Int.MaxValue
          else if v.end < u.start then u.start - v.end
          else if u.end < v.start then v.start - u.end
          else 0

        /** The size of a locus is its length
         */

        def sz(v: Locus): Double  = v.end - v.start

        /** These predicates are antimonotonic wrt isBefore
         */

        @inline def cond(p: PRED): PRED =         // Ensure (v, u) are on
          (v, u) => v.chrom == u.chrom && p(v, u) // same chrom before testing
                                                  // for p(v, u).

        def sameChrom: PRED   = (v, u) => v.chrom == u.chrom
        def isBefore: PRED    = ord.lt
        def sameLocus: PRED   = (v, u) => v == u
        def sameStart: PRED   = cond { (v, u) => v.start == u.start }
        def startBefore: PRED = cond { (v, u) => v.start < u.start }
        def near(n: Int): PRED = cond { (v, u) => intdist(v, u) <= n }
        def overlapStart: PRED = cond { (v, u) => v.start <= u.start && u.start <= v.end }


        /** overlap(n) is antimonotonic wrt isBefore for n = 1. 
         *  But it is not antimonotonic for large n
         */

        def overlap(n: Int): PRED = cond { (v, u) => 
          v.end > u.start && u.end > v.start &&
          ((v.end min u.end) - (v.start max u.start) > n)
        }


        /** These predicates are not antimonotonic wrt isBefore.
         */

        def overlapEnd: PRED = cond { (v, u) => v.start <= u.end && u.end <= v.end }
        def startAfter: PRED = cond { (v, u) => v.start > u.end } 
        def endBefore: PRED = cond { (v, u) => v.end < u.start }
        def endAfter: PRED = cond { (v, u) => v.end > u.end }
        def sameEnd: PRED = cond { (v, u) => v.end == u.end }
        // def touch: PRED = cond { (v,u) => v.end == u.start || u.end == v.start }
        def far(n: Int): PRED = cond { (v, u) => intdist(v, u) >= n }
        // override def inside: PRED = cond { (v,u) => u.start > v.start && v.end < u.end }
        // override def enclose: PRED = cond { (v,u) => v.start > u.start && u.end < v.end }

        override def outside: PRED = (v, u) => 
          !sameChrom(v, u) || v.end < u.start || u.end < v.start
      
        def b2_after_a2: PRED = cond { (v,u) => v.end > u.end }
        def b2_after_a1: PRED = cond { (v,u) => v.end > u.start }
        def b2_before_a2: PRED = cond { (v,u) => v.end < u.end }
        def b2_before_a1: PRED = cond { (v,u) => v.end < u.start }
        def b1_after_a2: PRED = cond { (v,u) => v.start > u.end }
        def b1_after_a1: PRED = cond { (v,u) => v.start > u.start }
        def b1_before_a2: PRED = cond { (v,u) => v.start < u.end }
        def b1_before_a1: PRED = cond { (v,u) => v.start < u.start }
        def b2_at_a2: PRED = cond { (v,u) => v.end == u.end }
        def b2_at_a1: PRED = cond { (v,u) => v.end == u.start }
        def b1_at_a2: PRED = cond { (v,u) => v.start == u.end }
        def b1_at_a1: PRED = cond { (v,u) => v.start == u.start }

      given ordering: Ordering[Locus] = dLocus.ord

    end Locus

  end GenomeLocus


