/*
   Copyright 2010 Aaron J. Radke

   Licensed 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 cc.drx

import java.nio.charset.StandardCharsets.UTF_8

final class DrxRegex(val regex:Regex) extends AnyVal{
  def existsMatch(string:String):Boolean = regex.findFirstMatchIn(string).isDefined
  def matchesFully(string:String):Boolean = regex.unapplySeq(string).isDefined
  def ignoreCase = new Regex("(?i)" + regex.regex)
}
object DrxRegex{
  val whitespace = """\s+""".r
}
trait Opposite[A,B]{
  def oppositeOf(orig:A):Option[B]
}
object Opposite{
  trait OppositeWords extends Opposite[String,String]{
    protected def words:Iterable[Array[String]]
    private lazy val wordMap:Map[String,String] = {
      val pairs = words.filter{_.size == 2}
      def alt(f:String=>String) = pairs.map(_ map f)
      val alts = alt(_.toLowerCase) ++ alt(_.toUpperCase) ++ alt(_.toLowerCase.capitalize)
      (alts ++ alts.map(_.reverse)).map{case Array(a,b) => a -> b}.toMap
    }
    private lazy val sortedKeys = wordMap.keys.toList.sortBy(-_.size)
    def oppositeOf(orig:String):Option[String] = {
      // Log(orig,wordMap)
      wordMap.get(orig) orElse
      sortedKeys.find{orig contains _}.map{k => orig.replace(k, wordMap(k) ) }
    }
  }
  implicit object OppositeCommonWords extends OppositeWords{
    protected lazy val words:Iterable[Array[String]] = """
      |true false|on off|yes no 
      |start end|min max|begin finish|head tail|first last
      |some none|many few
      |good bad|great poor|awesome terrible|easy difficult|simple complex
      |fast slow|hot cold|warm cool|large small|big tiny|deep shallow|heavy light|soft hard
      |left right|top bottom|north south|east west|high low|hi low|in out|inside outside
      |light dark|brighter darker|new old|young old|strong weak|loud quiet
      |white black|green red
      """.split("""\s*\|\s*""").map{_ split ' ' map (_.trim)}
  }
}
trait Phonetic{
  def phoneticTerm(str:String):String

  final def phonetic(str:String):String = str.splitTerms.map{phoneticTerm}.mkString(" ") //spaces are not required for soundex but others may
}
object Phonetic{
  implicit val defaultPhonetic:Phonetic = Soundex
}
// https://www.wikiwand.com/en/Soundex
object Soundex extends Phonetic{
  def apply(str:String) = phonetic(str)
  private val codes = "BFPV CGJKQSXZ DT L MN R".split(' ') //char codes for: 1 2 3 4 5 6  vowels and HW intentionally undefined
  private def code(c:Char):Option[Int] = codes.someIndexWhere(_ contains c).map{_+1}
  final def phoneticTerm(term:String):String = {
    val uTerm = term.toUpperCase
    uTerm.toList.drop(1).flatMap(code).filterDuplicates.mkString(uTerm.take(1),"","0000").take(4)
  }
}

final class SymbolStringContext(val sc:StringContext) extends AnyVal{
  def symbol(args:Any*):Symbol = Symbol(sc.s(args:_*)) //alternate way to construct a symbol since Symbols are deprecated in dotty
}

final class DrxString(val string:String) extends AnyVal{

  //MOVED def ~(vec:Vec):Text = Text(string,vec) //moved as an implicit brought in through style because style has some of these already

  def words = (string.trim split """\s+""").toIndexedSeq  //make it return an immutable structure
  def wordMap[B](f:String=>B) = (words.iterator zipWith f).toMap
  def removeVowels = string filterNot {Data.Vowels contains _}
  def levenshtein(that:String):Int = Alg.levenshtein(this.string, that)
  def lcp(that:String):String = (string.toIterable lcp that.toIterable).mkString
  def longestCommonSuffix(that:String):String = (string.toSeq longestCommonSuffix that.toSeq).mkString
  def singularize(str:String):String = if((str.size > 1) && (str endsWith "s")) str.dropRight(1) else str
  //def decapitalize = java.beans.Introspector.decapitalize(string)
  def decapitalize = string.head.toString.toLowerCase + string.tail

  def overlay(that:String):String = this.string zip that map {case (a,b) => if(a == ' ') b else a} mkString ""

  def base(radix:Int):Long =
    (radix <= 36).getOrElse(string.toLowerCase, string).base(Data.Radix36 take radix)
  def base(charset:IndexedSeq[Char]):Long = string.reverse.zipWithIndex.foldLeft(0L){ case (x,(c,i)) =>
     x + charset.indexOf(c)*(charset.size ** i).toLong
  }

  def toOption:Option[String] = if(string.isEmpty) None else Some(string)

  /**assuming utf8 now that this is nearly standard*/
  def toBytes:Array[Byte] = string.getBytes(UTF_8)
  /**assuming utf8 now that this is nearly standard*/
  def toByteBuffer:java.nio.ByteBuffer = java.nio.ByteBuffer.wrap(string.getBytes(UTF_8))

  def camelCase = {
     string.head +:
     (string zip string.tail).flatMap{
       case ('_','_') => None
       case ('_',c) => Some(c.toString.toUpperCase)
       case (_,'_') => None
       case (_,c) => Some(c)
     }
  }.mkString

  //regex pat from stackoverflow https://stackoverflow.com/a/7594052/622016
  //TODO don't use a regex on this use a some stream process, it is a simple problem and can be made more general
  def splitTerms:Array[String] = string.split("""[^a-zA-Z0-9]+""").flatMap{_ split """(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[0-9])(?=[A-Z][a-z])|(?<=[a-zA-Z])(?=[0-9])"""}.filter(_.nonEmpty)
    //string split """(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])"""

  //def toInputStream:java.io.InputStream = File.Normal.is(string)

  /**replace spaces with underscores*/
  def underscore:String = DrxRegex.whitespace.replaceAllIn(string.trim, "_")

  def truncate(len:Int=70,postfix:String="...") = if(string.size <= len) string else {
     string.take(len-postfix.size) + postfix
  }
  def indent:String = indent("  ") //default indent is two spaces
  def indent(margin:String):String = string.split("\n").map{margin + _}.mkString("\n")
  def undent(margin:String):String = string.split("\n").map{s => if(s startsWith margin) s.drop(margin.size) else s}.mkString("\n")
  def undent:String = string.split("\n").map{_.unpadHead}.mkString("\n")

  def takeRightWhile(p:Char => Boolean):String = string.reverse.takeWhile(p).reverse
  def dropRightWhile(p:Char => Boolean):String = string.reverse.dropWhile(p).reverse

  def unpadHead = string.dropWhile(_.isWhitespace)
  /**a better `trim`, the java.lang.St strips out to much (including ansicodes)*/
  def unpad = unpadHead.reverse.unpadHead.reverse
  import Style.{Left,Right,Center,AlignHorizontal}
  def fit(n:Int,align:AlignHorizontal=Left,padChar:String=" "):String = {
    val trimmed = unpad
    val displayedWidth = Ansi.strip(trimmed).size  //don't count (zero print width) ansi codes
    val delta = n - displayedWidth
    if(delta < 0) align match {
        case Left   => trimmed.take(n)
        case Right  => trimmed.takeRight(n)
        case Center => val j = -delta/2; trimmed.drop(j).dropRight(j + delta.isOdd.getOrElse(1,0))
    } else align match {
        case Left   => trimmed + padChar*delta
        case Right  => padChar*delta + trimmed
        case Center => padChar*(delta/2) + trimmed + padChar*(delta/2)  + (if(delta.isOdd) padChar else "")
    }
  }

  //--wrap help
  def quote:String = "\""+ string + "\"" //because escaping is so onerous
  def unQuote:String = string.dropWhile(_ == '"').dropRightWhile(_ == '"')
  def wrap(pre:String):String = wrap(pre, (pre map Data.Brackets.apply _).reverse)
  def wrap(pre:String,post:String):String = pre + string + post
  def unWrap(pre:String,post:String):String = ???

  //--common html tag help
  def tag(c:Color):String = string.tag("span",  "style"-> s"color:${c.cssHex}" )
  def tag(tag:String,attrs:(String,String)*):String = {
     val tags = tag.split("""\.""")
     val classes = tags.drop(1)
     val baseTag = if(tags.head == "") "div" else tags.head
     val attrMap = if(classes.isEmpty) attrs.toMap else attrs.toMap + ("class" -> classes.mkString(" "))
     if(attrMap.isEmpty) s"<$baseTag>$string</$baseTag>" else{
        val attrString = attrMap.map{case (k,v) => s"""$k="$v""""}.mkString(" ")
        s"<$baseTag $attrString>$string</$baseTag>"
     }
  }
  def href(path:String):String = string.tag("a",  "href" -> path)

  def escapeXML:String = string.replace("&","&amp;")
                               .replace("<","&lt;")
                               .replace(">","&gt;")

  //--containment tests
  def doesNotContain(x:String):Boolean = !string.contains(x)
  def doesNotStartWith(x:String):Boolean = !string.startsWith(x)
  def doesNotEndWith(x:String):Boolean = !string.endsWith(x)

  def containsSubsequence(x:String) = string.toSeq containsSubsequence x.toSeq

  //--replace
  def replaceAllWith(regex:Regex,replacement:String=""):String = regex.replaceAllIn(string, replacement)
  def replaceAllWith(regex:Regex)(replacer:String=>String):String = regex.replaceAllIn(string, m => Regex.quoteReplacement(replacer(m.matched)))
  def replaceIf(cond:String => Boolean, replacement: => String):String = if(cond(string)) replacement else string
  def strip(regex:Regex):String = regex.replaceAllIn(string,"")
  def strip(strings:Iterable[String]):String = strings.foldLeft(string){case (a, pat) =>  a.replace(pat,"")}

  // private def twoGroups(m:Regex.Match):(String,String) = (m group m.groupCount-1, m group m.groupCount)
  // private def allGroups(m:Regex.Match):List[String] = m.subgroups
  /**easy way to grab all matches of a string, an alternative name would be pluckAllFirstMatchGroup*/
  def pluck[A](p:Pluck[String,A]):Option[A] = p.pluck(string)
  def pluckAll[A](p:Pluck[String,A]):Iterator[A] = p.pluckAll(string)

  def dos2unix:String = string.replaceAll("\r\n", "\n")

  //FIXME why are these ansi's deprecated?  They seem nice.???
  2017-07-30("use Ansi.strip","v0.2.15")
  def stripAnsi:String = Ansi.strip(string)
  // 2017-07-30("use Ansi.color(color,string) or color.ansi(string)","v0.2.15")
  // def ansi(c:Color):String = c.ansi(string)

  def ansi(c:Color):String = c.ansi(string)

  2017-07-30("use part of the Pluck typeclass transformer now", "0.2.13")
  private def firstGroup(m:Regex.Match):String = m group m.groupCount
  2017-07-30("use pluck(Pluck(regex)) istead", "0.2.13")
  def pluck(regex:Regex):Option[String] = regex findFirstMatchIn string map firstGroup
  2017-07-30("use pluckAll(Pluck(regex)) istead", "0.2.13")
  def pluckAll(regex:Regex):Iterator[String] = regex findAllMatchIn string map firstGroup
  /**an left to right version of regex findAllMatchIn string*/
  2017-07-30("use the Pluck typeclass instead which better encodes the Regex -> Match -> Extrator -> Type pattern in a typesafe inference way", "0.2.13")
  def findAllMatches(regex:Regex) = regex findAllMatchIn string

  /**fix "bug" of intent for string concat with a +++ for flatConcat or ++ for a concat of seq of chars*/
  def flatConcat(xs:Iterable[String]):String = string + xs.mkString("")
  def flatConcat(xs:Option[String]):String = flatConcat(xs.toList)

  /**alias for flatConcat is needed so left to right operator precidence works when named opperators are higher than operators*/
  def +++(opt:Option[String]):String = flatConcat(opt)
  def +++(xs:Iterable[String]):String = flatConcat(xs)

  def soundsLike(str:String)(implicit p:Phonetic):Boolean = p.phonetic(string) == p.phonetic(str)
  def containsSound(str:String)(implicit p:Phonetic):Boolean = p.phonetic(string) contains p.phonetic(str)

  def opposite(implicit p:Opposite[String,String]):Option[String] = p.oppositeOf(string)

  def println():Unit = System.out.println(string)
  // def println:String = {println(string); string}
}