import Foundation


//class RGBArrays : RGBArraysProtocol {


// Essential for file exists

fileprivate let dfm = FileManager.default


//MARK: Parsing String tables (from string or URL, delimited or columnar)


/// Return [[String]]? from columnar file passsed as path

func stringTable(contentsOfFile filePath: String, columns cols: [Int]) -> [[String]]? {

  guard dfm.fileExists(atPath: filePath) else { return nil }

  let expectedTotalLength = cols.reduce(0, { $0 + $1} )

  //

  // Try to read file as lines, then keep only the ones that have the proper length

  let lines:[String]

  

  do {

    // Get all lines, keeping only non-empty ones

    let allLines = try String(contentsOfFile: filePath,

                              encoding: String.Encoding.utf8)

      .components(separatedBy: "\n")

      .filter { !$0.isEmpty }

    

    // Get lines with correct length

    lines = allLines.filter({ $0.count == expectedTotalLength })

    

    //If lines were discarded, warn user

    if allLines.count != lines.count {

      print("WARNING: \(allLines.count - lines.count) had incorrect lengths and were discarded")

    } 

  } catch {

    print(error)

    return nil

  }

  // Now split lines into tokens, trim them and add them to the final result

  var result:[[String]]?

  

  for numRow in 0..<lines.count {

    let row = lines[numRow]

    var firstIndex = row.startIndex

    var lastIndex = row.index(firstIndex, offsetBy: cols[0])

    var range = firstIndex..<lastIndex

    var frags = [String]()

    frags.append(row[range].trimmingCharacters(in: CharacterSet.whitespaces))

    for i in 1..<cols.count {

      firstIndex = lastIndex

      lastIndex = row.index(firstIndex, offsetBy: cols[i])

      range = firstIndex..<lastIndex

      frags.append(row[range].trimmingCharacters(in: CharacterSet.whitespaces))

    }

    result?.append(frags)

  }

  return result

} // End of stringTable(contentsOfFile:columns:)


/// /// Return [[String]]? from CSV file passsed as path

func stringTable(contentsOfFile filePath: String, delimiter del: String) -> [[String]]? {

  // If file does not exist, bail out

  guard dfm.fileExists(atPath: filePath) else { return nil }

  

  // Transform the contents of the file into a [[String]] if possible or return nil

  let result:[[String]]?

  do {

    result = try String(contentsOfFile: filePath,

                        encoding: String.Encoding.utf8)

      .components(separatedBy: "\n")

      .map { return $0.components(separatedBy: del) }

  } catch {

    print(error)

    result = nil

  }

  return result

}


/// /// Return [[String]]? from columnar file passsed as URL

func stringTable(contentsOf fileURL: URL, columns cols: [Int]) -> [[String]]? {

  let filePath = fileURL.path

  return stringTable(contentsOfFile: filePath, columns: cols)

}


/// /// Return [[String]]? from CSV file passsed as URL

func stringTable(contentsOf fileURL: URL, delimiter del: String) -> [[String]]? {

  let filePath = fileURL.path

  return stringTable(contentsOfFile: filePath, delimiter: del)

}


//MARK: Parsing Double tables (from string or URL, delimited or columnar)

/// Return [[Double]]? from columnar file passsed as path 

func doubleTable(contentsOfFile filePath: String, columns cols: [Int]) -> [[Double]]? {

  

  var result:[[Double]]? = nil

  if let table = stringTable(contentsOfFile: filePath, columns: cols) {

    var tmp = [[Double]]()

    for stringRow in table {

      var numberRow = [Double]()

      for s in stringRow {

        if let x = Double(s) {

          numberRow.append(x)

        } else {

          print("ERROR: item \(s) is not a valid Double")

          return result

        }

      } // End of for s in stringRow

      tmp.append(numberRow)

    } // End of for stringRow in table

    result = tmp

  } // End of if let table = stringTable(contentsOfFile:columns:)

  else {

    print("\n\nERROR in doubleTable(contentsOfFile:columns:) --> Could not read \(filePath)")

  }

  return result

} // End of doubleTable(contentsOfFile:columns:)


/// Return [[Double]]? from CSV file passsed as path

func doubleTable(contentsOfFile filePath: String, delimiter del: String) -> [[Double]]? {

  var  result:[[Double]]? = nil

  if let table = stringTable(contentsOfFile: filePath, delimiter: del) {

    var tmp = [[Double]]()

    for stringRow in table {

      var numberRow = [Double]()

      for s in stringRow {

        if let x = Double(s) {

          numberRow.append(x)

        } else {

          print("ERROR: item \(s) is not a valid Double")

          return result

        }

      } // End of for s in stringRow

      tmp.append(numberRow)

    } // End of for stringRow in table

    result = tmp

  } // End of if let table = stringTable(contentsOfFile:columns:)

  else {

    print("\n\nERROR in doubleTable(contentsOfFile:delimiter:) --> Could not read \(filePath)")

  }

  return result

}


/// Return [[Double]]? from columnar file passsed as URL

func doubleTable(contentsOf fileURL: URL, columns cols: [Int]) -> [[Double]]? {

  let filePath = fileURL.path

  return doubleTable(contentsOfFile: filePath, columns: cols)

}


/// Return [[Double]]? from CSV file passsed as path

func doubleTable(contentsOf fileURL: URL, delimiter del: String) -> [[Double]]? {

  let filePath = fileURL.path

  return doubleTable(contentsOfFile: filePath, delimiter: del)

}


//MARK: Parsing Float tables (from string or URL, delimited or columnar)

/// Return [[Float]]? from columnar file passsed as path

func floatTable(contentsOfFile filePath: String, columns cols: [Int]) -> [[Float]]? {

  var result:[[Float]]? = nil

  if let table = stringTable(contentsOfFile: filePath, columns: cols) {

    var tmp = [[Float]]()

    for stringRow in table {

      var numberRow = [Float]()

      for s in stringRow {

        if let x = Float(s) {

          numberRow.append(x)

        } else {

          print("ERROR: item \(s) is not a valid Float")

          return result

        }

      } // End of for s in stringRow

      tmp.append(numberRow)

    } // End of for stringRow in table

    result = tmp

  } // End of if let table = stringTable(contentsOfFile:columns:)

  else {

    print("\n\nERROR in floatTable(contentsOfFile:columns:) --> Could not read \(filePath)")

  }

  return result

  

}


/// Return [[Float]]? from CSV file passsed as path

func floatTable(contentsOfFile filePath: String, delimiter del: String) -> [[Float]]? {

  var result:[[Float]]? = nil

  if let table = stringTable(contentsOfFile: filePath, delimiter: del) {

    var tmp = [[Float]]()

    for stringRow in table {

      var numberRow = [Float]()

      for s in stringRow {

        if let x = Float(s) {

          numberRow.append(x)

        } else {

          print("ERROR: item \(s) is not a valid Float")

          return result

        }

      } // End of for s in stringRow

      tmp.append(numberRow)

    } // End of for stringRow in table

    result = tmp

  } // End of if let table = stringTable(contentsOfFile:columns:)

  else {

    print("\n\nERROR in floatTable(contentsOfFile:delimiter:) --> Could not read \(filePath)")

  }

  return result

  

}


/// Return [[Float]]? from columnar file passsed as URL

func floatTable(contentsOf fileURL: URL, columns cols: [Int]) -> [[Float]]? {

  let filePath = fileURL.path

  return floatTable(contentsOfFile: filePath, columns: cols)

}


/// Return [[String]]? from CSV file passsed as URL

func floatTable(contentsOf fileURL: URL, delimiter del: String) -> [[Float]]? {

  let filePath = fileURL.path

  return floatTable(contentsOfFile: filePath, delimiter: del)

}



//MARK: Parsing Int tables (from string or URL, delimited or columnar)

/// Return [[Int]]? from columnar file passsed as path

func intTable(contentsOfFile filePath: String, columns cols: [Int]) -> [[Int]]? {

  var  result:[[Int]]? = nil

  if let table = stringTable(contentsOfFile: filePath, columns: cols) {

    var tmp = [[Int]]()

    for stringRow in table {

      var numberRow = [Int]()

      for s in stringRow {

        if let x = Int(s) {

          numberRow.append(x)

        } else {

          print("ERROR: item \(s) is not a valid Int")

          return result

        }

      } // End of for s in stringRow

      tmp.append(numberRow)

    } // End of for stringRow in table

    result = tmp

  } // End of if let table = stringTable(contentsOfFile:columns:)

  else {

    print("\n\nERROR in intTable(contentsOfFile:columns:) --> Could not read \(filePath)")

  }

  return result

  

}


/// Return [[Int]]? from CSV file passsed as path

func intTable(contentsOfFile filePath: String, delimiter del: String) -> [[Int]]? {

  var  result:[[Int]]? = nil

  if let table = stringTable(contentsOfFile: filePath, delimiter: del) {

    var tmp = [[Int]]()

    for stringRow in table {

      var numberRow = [Int]()

      for s in stringRow {

        if let x = Int(s) {

          numberRow.append(x)

        } else {

          print("ERROR: item \(s) is not a valid Int")

          return result

        }

      } // End of for s in stringRow

      tmp.append(numberRow)

    } // End of for stringRow in table

    result = tmp

  } // End of if let table = stringTable(contentsOfFile:delimiter:)

  else {

    print("\n\nERROR in intTable(contentsOfFile:delimiter:) --> Could not read \(filePath)")

  }

  return result

  

}


/// Return [[Int]]? from columnar file passsed as URL 

func intTable(contentsOf fileURL: URL, columns cols: [Int]) -> [[Int]]? {

  let filePath = fileURL.path

  return intTable(contentsOfFile: filePath, columns: cols)

  

}


/// Return [[Int]]? from CSV file passsed as path

func intTable(contentsOf fileURL: URL, delimiter del: String) -> [[Int]]? {

  let filePath = fileURL.path

  return intTable(contentsOfFile: filePath, delimiter: del)

}



//MARK: Writing String tables (to string or URL, delimited or columnar)


/// Write [[String]] to columnar file passsed as path

func writeStringTable(_ t: [[String]], toFile filePath: String, columns cols: [Int]) -> Bool {

  var tmp = ""

  for row in t {

    

    for n in 0..<row.count {

      tmp.append(row[n].padding(toLength: cols[n],

                                withPad: " ",

                                startingAt: 0))

    }

    tmp.append("\n")

  } // for row in t

  var didWrite = true

  do {

    try tmp.write(toFile: filePath,

                  atomically: true,

                  encoding: String.Encoding.utf8)

  } catch {

    print("ERROR: writeStringTable(_:contentsOf:columns:) could not write to \(filePath)")

    didWrite = false

  }

  return didWrite

}


/// Write [[String]] to columnar file passsed as URL

func writeStringTable(_ t: [[String]], toURL fileURL: URL, columns cols: [Int]) -> Bool {

  let filePath = fileURL.path

  var tmp = ""

  for row in t {

    

    for n in 0..<row.count {

      tmp.append(row[n].padding(toLength: cols[n],

                                withPad: " ",

                                startingAt: 0))

    }

    tmp.append("\n")

  } // for row in t

  var didWrite = true

  do {

    try tmp.write(toFile: filePath,

                  atomically: true,

                  encoding: String.Encoding.utf8)

  } catch {

    print("ERROR: writeStringTable(_:contentsOf:columns:) could not write to \(filePath)")

    didWrite = false

  }

  return didWrite

}


/// Write [[String]] to CSV file passsed as URL

func writeStringTable(_ t: [[String]], toURL fileURL: URL, delimiter del: String) -> Bool {

  let filePath = fileURL.path

  var tmp = ""

  t.forEach { tmp.append( $0.joined(separator: del));  tmp.append("\n")   }

  var didWrite = true

  do {

    try tmp.write(toFile: filePath,

                  atomically: true,

                  encoding: String.Encoding.utf8)

  } catch {

    print("ERROR: writeStringTable(_:contentsOf:delimiter:) could not write to \(filePath)")

    didWrite = false

  }

  return didWrite

}


/// Write [[String]] to CSV file passsed as path

func writeStringTable(_ t: [[String]], toFile filePath: String, delimiter del: String) -> Bool {

  var tmp = ""

  t.forEach { tmp.append( $0.joined(separator: del));  tmp.append("\n")   }

  var didWrite = true

  do {

    try tmp.write(toFile: filePath,

                  atomically: true,

                  encoding: String.Encoding.utf8)

  } catch {

    print("ERROR: writeStringTable(_:contentsOf:delimiter:) could not write to \(filePath)")

    didWrite = false

  }

  return didWrite

}



// MARK: Printing String, Int, Float or Double tables


/// Print [[String]] as columns with automatic width calculation 

func printStringTable(_ t:[[String]]) {

  guard 

    let maxColWidth = t.flatMap( { $0 } ).map( { $0.count }).max()

    else  { print("\n\nERROR: could not print string table\n\n"); return }

  //  print("Maxcolwidth is: \(maxColWidth)")

  

  for row in t {

    for column in row {

      print("| \(column.padding(toLength: maxColWidth, withPad: " ", startingAt: 0)) ", terminator: "")

    }

    print("|")

  }

}


/// Print [String] as columns with automatic width calculation 

func printStringList(_ t:[String]) {

  guard 

    let maxColWidth = t.compactMap( { $0 } ).map( { $0.count }).max()

    else  { print("\n\nERROR: could not print string table\n\n"); return }

  //  print("Maxcolwidth is: \(maxColWidth)")

  

  for row in t {

    print("| \(row.padding(toLength: maxColWidth, withPad: " ", startingAt: 0)) ", terminator: "")

  }

  print("|")

  

} // End of printStringList(_:)


/// Print [[Double]] as columns with automatic width calculation 

func printDoubleTable(_ t:[[Double]]) {

  guard 

    let maxColWidth = t.flatMap( { $0 }).map({ $0.description.count }).max()

    else  { print("\n\nERROR: could not print Double table\n\n"); return }

  //  print("Maxcolwidth is: \(maxColWidth)")

  

  for row in t {

    for column in row {

      print("| \(column.description.padding(toLength: maxColWidth, withPad: " ", startingAt: 0)) ", terminator: "")

    }

    print("|")

  }

}


/// Print [[Float]] as columns with automatic width calculation 

func printFloatTable(_ t:[[Float]]) {

  guard 

    let maxColWidth = t.flatMap( { $0 }).map({ $0.description.count }).max()

    else  { print("\n\nERROR: could not print Float table\n\n"); return }

  //  print("Maxcolwidth is: \(maxColWidth)")

  

  for row in t {

    for column in row {

      print("| \(column.description.padding(toLength: maxColWidth, withPad: " ", startingAt: 0)) ", terminator: "")

    }

    print("|")

  }

}


/// Print [[Int]] as columns with automatic width calculation 

func printIntTable(_ t:[[Int]]) {

  guard 

    let maxColWidth = t.flatMap( { $0 }).map({ $0.description.count }).max()

    else  { print("\n\nERROR: could not print Int table\n\n"); return }

  //  print("Maxcolwidth is: \(maxColWidth)")

  

  for row in t {

    for column in row {

      print("| \(column.description.padding(toLength: maxColWidth, withPad: " ", startingAt: 0)) ", terminator: "")

    }

    print("|")

  }

}


//MARK: Writing Double tables (to string or URL, delimited or columnar)


/// Write [[Double]] to columnar file passed as path

func writeDoubleTable(_ t: [[Double]], toFile filePath: String, columns cols: [Int]) -> Bool {

  var tmp = ""

  

  t.forEach {

    

    for numCol in 0..<$0.count {

      tmp.append($0[numCol].description.padding(toLength: cols[numCol], withPad: " ", startingAt: 0))

    }

    

    tmp.append("\n")

  } // t.forEach

  var didwrite = true

  do {

    try tmp.write(toFile: filePath, atomically: true, encoding: String.Encoding.utf8)

  } catch {

    print(error)

    print("ERROR: writeDoubleTable(_:toFile:columns:) could not write to \(filePath)")

    didwrite = false

  }

  return didwrite

} // writeDoubleTable(_:toFile:columns:)


/// Write [[Double]] to CSV file passed as path

func writeDoubleTable(_ t: [[Double]], toFile filePath: String, delimiter del: String) -> Bool {

  

  var total = ""

  t.forEach {

    var tmp = [String]()

    for numCol in 0..<$0.count {

      tmp.append($0[numCol].description)

    }

    total.append(contentsOf: tmp.joined(separator: del))

    total.append("\n")

  } // t.forEach

  var didwrite = true

  do {

    try total.write(toFile: filePath, atomically: true, encoding: String.Encoding.utf8)

  } catch {

    print(error)

    print("ERROR: writeDoubleTable(_:toFile:delimiter:) could not write to \(filePath)")

    didwrite = false

  }

  return didwrite

  

}


/// Write [[Double]] to columnar file passed as URL

func writeDoubleTable(_ t: [[Double]], toURL fileURL: URL, columns cols: [Int]) -> Bool {

  let filePath = fileURL.path

  return writeDoubleTable(t, toFile: filePath, columns: cols)

}


/// Write [[Double]] to CSV file passed as path

func writeDoubleTable(_ t: [[Double]], toURL fileURL: URL, delimiter del: String) -> Bool {

  let filePath = fileURL.path

  return writeDoubleTable(t, toFile: filePath, delimiter: del)

}


//MARK: Writing Float tables (to string or URL, delimited or columnar)


/// Write [[Float]] to columnar file passed as path

func writeFloatTable(_ t: [[Float]], toFile filePath: String, columns cols: [Int]) -> Bool {

  var tmp = ""

  

  t.forEach {

    

    for numCol in 0..<$0.count {

      tmp.append($0[numCol].description.padding(toLength: cols[numCol], withPad: " ", startingAt: 0))

    }

    

    tmp.append("\n")

  } // t.forEach

  var didwrite = true

  do {

    try tmp.write(toFile: filePath, atomically: true, encoding: String.Encoding.utf8)

  } catch {

    print(error)

    print("ERROR: writeDoubleTable(_:toFile:columns:) could not write to \(filePath)")

    didwrite = false

  }

  return didwrite

  

}


/// Write [[Double]] to CSV file passed as path

func writeFloatTable(_ t: [[Float]], toFile filePath: String, delimiter del: String) -> Bool {

  var total = ""

  t.forEach {

    var tmp = [String]()

    for numCol in 0..<$0.count {

      tmp.append($0[numCol].description)

    }

    total.append(contentsOf: tmp.joined(separator: del))

    total.append("\n")

  } // t.forEach

  var didwrite = true

  do {

    try total.write(toFile: filePath, atomically: true, encoding: String.Encoding.utf8)

  } catch {

    print(error)

    print("ERROR: writeDoubleTable(_:toFile:delimiter:) could not write to \(filePath)")

    didwrite = false

  }

  return didwrite

  

}


/// Write [[Double]] to columnar file passed as URL

func writeFloatTable(_ t: [[Float]], toURL fileURL: URL, column cols: [Int]) -> Bool {

  let filePath = fileURL.path

  return writeFloatTable(t, toFile: filePath, columns: cols)

}


/// Write [[Double]] to CSV file passed as URL

func writeFloatTable(_ t: [[Float]], toURL fileURL: URL, delimiter del: String) -> Bool {

  let filePath = fileURL.path

  return writeFloatTable(t, toFile: filePath, delimiter: del)

}


// MARK: Writing Int tables  (to string or URL, delimited or columnar)


/// Write [[Int]] to columnar file passed as path

func writeIntTable(_ t: [[Int]], toFile filePath: String, columns cols: [Int]) -> Bool {

  var tmp = ""

  

  t.forEach {

    

    for numCol in 0..<$0.count {

      tmp.append($0[numCol].description.padding(toLength: cols[numCol], withPad: " ", startingAt: 0))

    }

    

    tmp.append("\n")

  } // t.forEach

  var didwrite = true

  do {

    try tmp.write(toFile: filePath, atomically: true, encoding: String.Encoding.utf8)

  } catch {

    print(error)

    print("ERROR: writeDoubleTable(_:toFile:columns:) could not write to \(filePath)")

    didwrite = false

  }

  return didwrite

}


/// Write [[Int]] to CSV file passed as path

func writeIntTable(_ t: [[Int]], toFile filePath: String, delimiter del: String) -> Bool {

  

  var total = ""

  t.forEach {

    var tmp = [String]()

    for numCol in 0..<$0.count {

      tmp.append($0[numCol].description)

    }

    total.append(contentsOf: tmp.joined(separator: del))

    total.append("\n")

  } // t.forEach

  var didwrite = true

  do {

    try total.write(toFile: filePath, atomically: true, encoding: String.Encoding.utf8)

  } catch {

    print(error)

    print("ERROR: writeDoubleTable(_:toFile:delimiter:) could not write to \(filePath)")

    didwrite = false

  }

  return didwrite

  

}


/// Write [[Into]] to columnar file passed as URL

func writeIntTable(_ t: [[Int]], toURL fileURL: URL, columns cols: [Int]) -> Bool {

  let filePath = fileURL.path

  return writeIntTable(t, toFile: filePath, columns: cols)

}


/// Write [[Int]] to CSV file passed as URL

func writeIntTable(_ t: [[Int]], toURL fileURL: URL, delimiter del: String) -> Bool {

  let filePath = fileURL.path

  return writeIntTable(t, toFile: filePath, delimiter: del)

}



//} // End of class RGBArrays