//  Print examples used in "Regular Expressions in Swift"
//
//  main.swift
//  RegexInSwift
//
//  Created by Guy Lapalme on 2024-04-25.
//
//
import Foundation
import RegexBuilder
print("** 1. What is a regex")
let è = "e\u{300}"
print(è.wholeMatch(of:/./) != nil) // true because this is a single unicode char
let expr = /a(bc|de)f|ghi/
let quote = /".*?"/
let hello = #""hello" my "friends""#
showMatches(of: "abc|def|ghi",   in: "labc ghk ghi def ccc")
showMatches(of:"a(bc|de)f|ghi",  in: #"abc abcf ghi def adef"#)
showMatches(of:#"a(bc|de)f|ghi"#,in: #"abc "abcf" "gh"i"#)
showMatches(of: "\".*\"",        in: hello)
showMatches(of:#"".*?""#,        in: hello)
showMatches(of:#"".*+""#,        in: hello)
showMatches(of:#""[^"]*+""#,     in: hello)
print("** 2. Regex in Swift")
print("*** 2.1 Swift Regex Definition")
let exprRB = Regex {
    ChoiceOf {
      Regex {
        "a"
        ChoiceOf {
          "bc" ; "de"
         }
        "f"
      }
      "ghi"
    }
  }
let quoteRB = Regex {
    /"/
    ZeroOrMore(.reluctant) {
      /./
    }
    /"/
  }
let quoteEL = #/
   "      # open quote
   .*?    # up to next quote
   "      # end quote
/#
showMatches(of: quoteEL,         in: hello)
print("*** 2.2 Swift Regex Operations")
let identifier = /[A-Za-z]\w*/
let IDENTIFIER = /[A-Z]\w*/.ignoresCase()
let subject = "Here are 10 tokens to be (matched) !"
print("*** 2.2.1 Checking for an Occurrence of a Pattern")
print(subject.contains(identifier))
print("123 + 456".contains(identifier))
print(subject.starts(with:identifier))
print("123 + 456".starts(with:identifier))
print("*** 2.2.2 Looking in a Subject for Matches of a Pattern")
print("*** 2.2.1 Checking for an Occurrence of a Pattern")
print("firstMatch.output:",subject.firstMatch(of: identifier)?.output ?? "no match!")
print("firstMatch.count:",subject.firstMatch(of: identifier)?.count ?? "no match!")
print("wholeMatch:",subject.wholeMatch(of: identifier)?.output ?? "no match!")
print("prefixMatch:",subject.prefixMatch(of: identifier)?.output ?? "no match!")
print("**** 2.2.2.2 Finding all matches")
print("matches:",subject.matches(of: identifier).map{m in m.output.uppercased()})
showMatches(of: identifier, in: subject)
print("matches", "12 + (a_bc*435)- 78".matches(of:/\w+|\d+|[-+*\/()]/).map{String($0.output)})
print("** 2.2.3 Given a pattern, finding matches in a subject")
print(try identifier.firstMatch(in: subject)?.output ?? "no match!")
print(try identifier.firstMatch(in: subject)?.count ?? "no match!")
print(try identifier.wholeMatch(in: subject)?.output ?? "no match!")
print(try identifier.prefixMatch(in: subject)?.output ?? "no match!")
print("*** 2.2.4 Replacing the match in the subject")
print(subject.replacing(identifier, with: "*id*"))
print(subject.replacing(identifier, with: {m in return "[\(m.output)]"}))
print(subject.replacing(identifier){"{\($0.output)}"})
var subjectV = subject
print(subjectV.replace(identifier){"#\($0.output)#"})
print(subjectV)
print("*** 2.2.5 Determining the Ranges of Matches")
print(subject.ranges(of: identifier).map{rng in subject[rng]})
print("*** 2.2.6 Splitting a String According to a Pattern")
print(subject.split(separator: /\W+/))
print(subject.split(separator: /(?=\W+)/))
//   To keep separators one solution would be
//       /(?=\W+)|(?<=\W+)/
//   But Lookbehind (?<=...) is not yet implemented
//   so we define the following extension
extension String {
    func splitKeeping(separator:Regex)->[Substring]{
        // adapté de https://stackoverflow.com/questions/41587416/split-string-by-components-and-keep-components-in-place
        var result=[Substring]()
        var pos = self.startIndex
        for rng in self.ranges(of:separator) {
            if rng.lowerBound != pos { // add substring before the separator
                result.append(self[pos..\p{alpha}\w*)(\s*=\s*(?\d+))?/
print(type(of: kvPatN_))
let kvPatN = /(?\p{alpha}\w*)(?:\s*=\s*(?\d+))?/
print(type(of: kvPatN))
print(keyValues.matches(of: kvPatN).map{Int($0.value ?? "1")!})
print(keyValues.matches(of: kvPatN).map{Int($0.2 ?? "1")!})
// *** Matching Capture Groups
print(" *** 2.3.2 Matching Capture Groups")
let test = /(?[a-z]+)\k/
print("wxabcdabcd34".contains(test))
print("abaaba".contains(test))
let xml_string = "<_a>xx yy  content"
let xml_tag = /<([a-zA-Z_][-a-zA-Z_0-9.]*)\s*(?:.*?)(?:\/>|>(.*?)<\/\1>)/   // remove tags,  keep only content
print("xml_tag")
print(xml_string.replacing(xml_tag){$0.2 ?? ""})
let nc_nameS = "[a-zA-Z_][-a-zA-Z_0-9.]*"
let xml_tagS:Regex = try! Regex<(Substring,Substring,Substring?)> (#"<(\#(nc_nameS))\s*(?:.*?)(?:\/>|>(.*?)\1>)"#)
print("xml_tagS")
print(xml_string.replacing(xml_tagS){$0.2 ?? ""})
let xml_tagX = #/
   <                          # begin of start-tag
    (?[a-zA-Z_][-a-zA-Z_0-9.]*) # save name
    \s*(?:.*?)                 # skip attributes
    (?:/>                      # empty tag
     |
       >                       # end of start-tag
       (?.*?)         # content
       \k>)            # end-tag
/#
print("xml_tagX")
print(xml_string.replacing(xml_tagX){$0.content ?? ""})
print("xml_tagX - repeated")
print((xml_string+" ab").replacing(xml_tagX){$0.content ?? ""})
print("** 3 RegexBuilder")
print("*** 3.1 Capturing Information")
let identifierRB = Regex {
    CharacterClass(("A"..."Z"),("a"..."z"))
    ZeroOrMore(.word)
}
print(subject.firstMatch(of: identifierRB)?.output ?? "no match!")
let key = Reference(Substring.self)
let kPatRB = Regex {
    Capture(as: key) {
        identifierRB
//          /[A-Za-z]\w*/
    }
}
print(type(of: kPatRB))
let value = Reference(Int.self)
let vPatRB = Regex {
    Capture(as: value) {
        OneOrMore(.digit)
    } transform: {Int($0)!}
}
print(type(of: vPatRB))
let kvPatRB = Regex {
    kPatRB
    Optionally {
        ZeroOrMore(.whitespace); /=/; ZeroOrMore(.whitespace)
        vPatRB
    }
}
print(type(of: kvPatRB))
print(keyValues.matches(of: kvPatRB).map{$0.2 != nil ? $0[value] : 1})
let xml_tag_named = /<(?[a-zA-Z_][-a-zA-Z_0-9.]*)\s*(?.*?)(\/>|>(?.*?)<\/\1>)/
print(xml_string.replacing(xml_tag_named){$0.content ?? ""})
// letter or underscore followed by letter, digit, underscore, hyphen or period
let nc_name = /[a-zA-Z_][-a-zA-Z_0-9.]*/
let tag_name = Reference(Substring.self)
let content = Reference(Substring?.self)
let xml_tag_RB = Regex {
    "<"     // begin of start-tag
    Capture (as: tag_name) {nc_name}
    /.*?/   // skip attributes
    ChoiceOf {
        "/>"    // empty-element tag
        Regex {
            ">"   // end of start-tag
            /.*?/
            Capture (as: content) { /.*?/ } transform: {$0}
            Regex{"" ; tag_name ; ">"}  // end-tag
        }
    }
}
print(xml_string.replacing(xml_tag_RB){$0[content] ?? ""})
// RegexComponent
print("** 3.2 Foundation Parsers")
print("**** Matching a Date")
let date = Reference(Date.self)
let dateRB = Regex{
    Capture (as:date){
        // Date.ParseStrategy.date
        // Date.FormatStyle.Symbol.Day.defaultDigits
        .date(format:"\(day:.defaultDigits)/\(month:.defaultDigits)/\(year:.defaultDigits)",
              locale: Locale(identifier: "fr_CA"),
              timeZone:.current)
    }
}
print("**** Matching a currency")
let price = Reference(Decimal.self)
let priceRB = Regex {
    Capture (as:price){
        // Decimal.FormatStyle.Currency.localizedCurrency
        .localizedCurrency(code: Locale.Currency("CAD"),
                           locale: Locale(identifier: "fr_CA"))
    }
}
print("**** Matching a URL")
let link = Reference(URL.self)
let linkRB = Regex {Capture (as:link){
    // URL.ParseStrategy.url
    .url()
}}
print("*** 3.2.2 Using Foundation Parsers")
let order = "19/2/2024 : Computer Screen : 258,92 $ : https://www.azamon.ca/gp/aw/d/B0CJVK87Y7/?ref_=sbx_be_s_sparkle_mcd_asin_1_img&pd_rd_w=dPFhO&content-id=amzn1.sym"
print("**** Separating fields")
let sep = Regex{
    Local {ZeroOrMore(.horizontalWhitespace); ":"; ZeroOrMore(.horizontalWhitespace)}
}
print("**** Matching the Order")
let item = Reference(Substring.self)
let itemRB = Regex {Capture (as:item){OneOrMore(.any)}}
let orderRB = Regex{
    dateRB ; sep ; itemRB ; sep ; priceRB ; sep ; linkRB
}
print("**** Creating an Invoice")
let en_CA_date = Date.FormatStyle()
    .year()
    .day(.defaultDigits)
    .month(.defaultDigits)
    .locale(Locale(identifier: "en_CA"))
let decimalFormat = Decimal.FormatStyle(locale: Locale(identifier: "fr_CA"))
    .precision(.integerAndFractionLength(integer: 6, fraction: 2))
func fmt(_ val:Decimal)->String{
    val.formatted(decimalFormat).replacing(/^[\s0]*/){String(repeating:" ",count:$0.count)}+" $"
}
let m = order.firstMatch(of: orderRB)!
let m_price = m[price]
let taxes = m_price * Decimal(14.975) / 100 // rate of Québec sales tax
let total = m_price + taxes
print("""
On \(m[date].formatted(en_CA_date)),
you ordered a \(m[item])
   Price: \(fmt(m_price))
   Taxes: \(fmt(taxes))
   Total: \(fmt(total))
Thank you
\(m[link].host!)
""")
print("** 3.3 Custom RegexComponent")
print("*** 3.3.1 Matching Balanced Parentheses")
// example adapted from
//       https://developer.apple.com/videos/play/wwdc2022/110358 (18:50)
// match a balanced parentheses string (e.g. BAL in SNOBOL4)
struct BalancedParentheses: CustomConsumingRegexComponent {
    typealias RegexOutput = Substring
    func consuming(_ input: String,
                   startingAt index: String.Index,
                   in bounds: Range)
    throws -> (upperBound: String.Index, output: Substring)? {
        guard index < input.endIndex && input[index]=="(" else {return nil}
        var level=1
        var pos = input.index(after: index)
        while pos != input.endIndex {
            if input[pos] == ")" {
                level -= 1
                if level == 0 {
                    return (input.index(after:pos),input[index ... pos])
                }
            } else if input[pos] == "(" {
                level += 1
            }
            pos = input.index(after:pos)
        }
        return nil
    }
}
let bal = BalancedParentheses()
print("(2+(3+4)) )((())".matches(of: bal).map{"\($0.output)"})
print(" (2+(3+4)())+( abc (1+2) ".matches(of: bal).map{"\($0.output)"})
//  for matching well parenthetised substrings
//  https://www.regular-expressions.info/recurse.html
//  suggests this regex "\((?>[^()]|(?R))*\)" that could be translated as the following
//  but the recursive call to balRB is not allowed (commented compiler error message)
//  so this regex only matches one level balanced parentheses
let balRB = Regex {
    "("
    ZeroOrMore(.eager) {
        Local {
            ChoiceOf {
                /[^()]/
                // balRB  // Cannot reference invalid declaration 'balRB'
            }
        }
    }
    ")"
}
print(" (2+(3+4)())+( abc (1+2) ".matches(of: balRB).map{"\($0.output)"})
print("*** 3.3.2 Matching Nested XML Tags")
let xml_attr = Regex {
    let quotesym = Reference(Substring.self)
    /\s+/
    nc_name
    /\s*=\s*/
    Capture (as:quotesym) {/["']/}
    /.*?/
    quotesym
}
let xml_tagN = Regex {
    Capture (as:tag_name){nc_name}
    Regex {ZeroOrMore {xml_attr}
        /\s*/
    }
}
let xml_start_tag = Regex {"<"  ; xml_tagN ; ">" }
let xml_empty_tag = Regex {"<"  ; xml_tagN ; "/>"}
let xml_end_tag   = Regex {"" ; xml_tagN ; ">" }
print("Within:",xml_string)
print("start-tags:",xml_string.matches(of: xml_start_tag).map{$0[tag_name]})
print("empty-tags:",xml_string.matches(of: xml_empty_tag).map{$0[tag_name]})
print("end-tags:",xml_string.matches(of: xml_end_tag).map{$0[tag_name]})
let doc = """
 hello 
info
  nothing test
 
 a value spanning
two lines
goodfriends
"""
var tags = [(Substring,String.Index)]()   // stack of (start-tag-name, string index of start)
struct NestedXML: CustomConsumingRegexComponent {
    typealias RegexOutput = Substring
    func consuming(_ input: String,
                   startingAt index: String.Index,
                   in bounds: Range)
    throws -> (upperBound: String.Index, output: Substring)? {
        guard index < input.endIndex else {return nil}
        var pos = index                           // current position
        while let m = input[pos...].firstMatch(of: /(?><)/) { // skip to before the next <
            let start = m.range.lowerBound
            if let m = input[start...].prefixMatch(of: xml_end_tag){ // end-tag encountered
                if m[tag_name] == tags.last!.0 { // check if the tag-name is the same as the top of the stack
                    let (_,tag_start) = tags.popLast()! // remove it
                    return (m.range.upperBound,input[tag_start ..< m.range.upperBound])
                } else {                 // should never happen (bad nesting of tags)
                    fatalError("Bad XML: \(m[tag_name]) should match \(tags.last?.0 ?? "strange tag")")
                }
            } else if let m = input[start...].prefixMatch(of: xml_empty_tag){ // empty tag
                return (input.index(after: start),input[start ..< m.range.upperBound]) // return it
            } else if let m = input[start...].prefixMatch(of: xml_start_tag){ // start tag
                tags.append((m[tag_name],start))                // add it to the stack
                pos = input.index(start, offsetBy: m.0.count)   // update position
            } else {      // should never happen: < not followed by a tag
                fatalError("no match of tag: \(start) : \(input[start...])")
            }
        }
        return nil
    }
}
doc.matches(of: NestedXML()).forEach{print("\($0.output)")}
print("** 4 Run-time Regular Expressions")
// without type specification
let subj1 = "labcf ghk ghi def ccc"
let exprS  = try! Regex("a(bc|de)f|ghi")
let quoteES = try! Regex(#"".*?""#)
print(subj1.contains(exprS))
print(subj1.firstMatch(of: exprS) ?? "no match") // printing hard to decipher! should use getGroups(…) defined below
print(subj1.firstMatch(of: exprS)?.range ?? "no range")
print(subj1.firstMatch(of: exprS)?.count ?? "no count")
print(subj1.firstMatch(of: exprS)?.output[0].substring ?? "no output substring")
print(subj1.firstMatch(of: exprS)?.output[0].range ?? "no output range")
print(subj1.firstMatch(of: exprS)?.output[0].name ?? "no output name")
print("*** 4.1 Specifying the Expected Type")
let quoteESt = try! Regex(#"".*?""#)
let exprSt = try! Regex<(Substring,Substring?)>("a(bc|de)f|ghi")
let exprSt1 = try! Regex("a(?:bc|de)f|ghi")
let quoteESta = try! Regex(#"".*?""#,as:Substring.self)
let exprSta = try! Regex("a(bc|de)f|ghi",as:(Substring,Substring?).self)
let exprSt1a = try! Regex("a(?:bc|de)f|ghi",as: Substring.self)
print(subj1.firstMatch(of: exprSt1)!.output)
print(subj1.matches(of: exprSt).map{"\($0.0)"})
//  ** 4.2 Some Uses of Run-Time Regexes
struct WordsReplace{
    let dict:[String:String]
    let dictRE:Regex
    
    init(_ dict:[String:String]){
        self.dict = dict
        let reS = dict.keys.joined(separator: "|")
        dictRE = try! Regex(#"\b(?:\#(reS))\b"#)
    }
    
     func replacing(in str: String)->String {
        return str.replacing(dictRE, with: {dict[String($0.output)]!})
    }
}
let fr2en = WordsReplace(["jour":"day", "monde":"world", "joyeux":"happy", "triste":"sad"])
print(fr2en.replacing(in:"bonjour le monde joyeux et rarement triste"))
print("** 5. Use Cases")
print("*** 5.1 Parse a Roman Numeral")
runRoman()
print("*** 5.2 Developing a Tokenizer")
runTokenizer()
print("*** 5.3 Eliza-Like Chatbot")
runScript(lines: landauerScript)
print("=======================")
runScript(lines: ["Often,I remember returning to my house.",
                  "I need some help."])
print("=======================")
//chat()
print("** Showing Matches")
// ===============================================================================
//  function to highlight parts of the subject that are matched
func showMatches(of patternS:String,in subject:String){
    let pattern = try! Regex(patternS)
    let rngs = subject.ranges(of: pattern)
    let nb = rngs.count
    print("Matching /\(patternS)/ \(nb == 1 ? "once" : nb == 2 ? "twice" : "\(nb) times")")
    showMatches(of: pattern,in: subject,rngs: rngs)
}
func showMatches(of pattern:any RegexComponent,in subject:String,rngs:[Range]? = nil){
    print("> \(subject)")
    let rngs = rngs ?? subject.ranges(of: pattern)
    var positions = ""
    if rngs.count == 0 {
        positions = "** no match **"
    } else {
        var lastPos = subject.startIndex
        for rng in rngs {
            if rng.lowerBound != lastPos {
                positions += String(repeating: " ", count: subject.distance(from: lastPos, to: rng.lowerBound))
            }
            positions += String(repeating: "↑", count: subject.distance(from: rng.lowerBound, to: rng.upperBound))
            lastPos = rng.upperBound
        }
    }
    print("> \(positions)")
}
//print("*** 7.5 Showing Capture Groups")
// ===============================================================================
// Function to return a list of Strings corresponding to the capture groups
//    in the Match of a run-time regex
// can also be used for a typed regex with a call of the form
//    getGroups(Regex.Match(...)) which creates type erased Match object
func getGroups(_ output:Regex.Match)->[String]{
    return( 0 ..< output.count).map{"\(output[$0].substring!)"}
}
print(#"** getGroups of first match of run-time regex "a(bc|de)f|ghi" in "\#(subj1)""#)
print(getGroups(subj1.firstMatch(of: exprS)!))
print(#"** getGroups of first match of regex literal /([A-Za-z]\w*)(\s*=\s*(\d+))?/ in "\#(keyValues)""#)
print(getGroups(Regex.Match(keyValues.firstMatch(of: kvPat)!)))