// MIT License
//
// Copyright (c) 2017-2019 MessageKit
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import Foundation

// MARK: - Lorem

public class Lorem {
  // MARK: Public

  /// Return a random word.
  ///
  /// - returns: Returns a random word.
  public class func word() -> String {
    wordList.random()!
  }

  /// Return an array of `count` words.
  ///
  /// - parameter count: The number of words to return.
  ///
  /// - returns: Returns an array of `count` words.
  public class func words(nbWords: Int = 3) -> [String] {
    wordList.random(nbWords)
  }

  /// Return a string of `count` words.
  ///
  /// - parameter count: The number of words the string should contain.
  ///
  /// - returns: Returns a string of `count` words.
  public class func words(nbWords: Int = 3) -> String {
    words(nbWords: nbWords).joined(separator: " ")
  }

  /// Generate a sentence of `nbWords` words.
  /// - parameter nbWords:  The number of words the sentence should contain.
  /// - parameter variable: If `true`, the number of words will vary between
  /// +/- 40% of `nbWords`.
  /// - returns:
  public class func sentence(nbWords: Int = 6, variable: Bool = true) -> String {
    if nbWords <= 0 {
      return ""
    }

    let result: String = words(nbWords: variable ? nbWords.randomize(variation: 40) : nbWords)

    return result.firstCapitalized + "."
  }

  /// Generate an array of sentences.
  /// - parameter nbSentences: The number of sentences to generate.
  ///
  /// - returns: Returns an array of random sentences.
  public class func sentences(nbSentences: Int = 3) -> [String] {
    (0 ..< nbSentences).map { _ in sentence() }
  }

  /// Generate a paragraph with `nbSentences` random sentences.
  /// - parameter nbSentences: The number of sentences the paragraph should
  /// contain.
  /// - parameter variable:    If `true`, the number of sentences will vary
  /// between +/- 40% of `nbSentences`.
  /// - returns: Returns a paragraph with `nbSentences` random sentences.
  public class func paragraph(nbSentences: Int = 3, variable: Bool = true) -> String {
    if nbSentences <= 0 {
      return ""
    }

    return sentences(nbSentences: variable ? nbSentences.randomize(variation: 40) : nbSentences).joined(separator: " ")
  }

  /// Generate an array of random paragraphs.
  /// - parameter nbParagraphs: The number of paragraphs to generate.
  /// - returns: Returns an array of `nbParagraphs` paragraphs.
  public class func paragraphs(nbParagraphs: Int = 3) -> [String] {
    (0 ..< nbParagraphs).map { _ in paragraph() }
  }

  /// Generate a string of random paragraphs.
  /// - parameter nbParagraphs: The number of paragraphs to generate.
  /// - returns: Returns a string of random paragraphs.
  public class func paragraphs(nbParagraphs: Int = 3) -> String {
    paragraphs(nbParagraphs: nbParagraphs).joined(separator: "\n\n")
  }

  /// Generate a string of at most `maxNbChars` characters.
  /// - parameter maxNbChars: The maximum number of characters the string
  /// should contain.
  /// - returns: Returns a string of at most `maxNbChars` characters.
  public class func text(maxNbChars: Int = 200) -> String {
    var result: [String] = []

    if maxNbChars < 5 {
      return ""
    } else if maxNbChars < 25 {
      while result.count == 0 {
        var size = 0

        while size < maxNbChars {
          let w = (size != 0 ? " " : "") + word()
          result.append(w)
          size += w.count
        }

        _ = result.popLast()
      }
    } else if maxNbChars < 100 {
      while result.count == 0 {
        var size = 0

        while size < maxNbChars {
          let s = (size != 0 ? " " : "") + sentence()
          result.append(s)
          size += s.count
        }

        _ = result.popLast()
      }
    } else {
      while result.count == 0 {
        var size = 0

        while size < maxNbChars {
          let p = (size != 0 ? "\n" : "") + paragraph()
          result.append(p)
          size += p.count
        }

        _ = result.popLast()
      }
    }

    return result.joined(separator: "")
  }

  // MARK: Private

  private static let wordList = [
    "alias", "consequatur", "aut", "perferendis", "sit", "voluptatem",
    "accusantium", "doloremque", "aperiam", "eaque", "ipsa", "quae", "ab",
    "illo", "inventore", "veritatis", "et", "quasi", "architecto",
    "beatae", "vitae", "dicta", "sunt", "explicabo", "aspernatur", "aut",
    "odit", "aut", "fugit", "sed", "quia", "consequuntur", "magni",
    "dolores", "eos", "qui", "ratione", "voluptatem", "sequi", "nesciunt",
    "neque", "dolorem", "ipsum", "quia", "dolor", "sit", "amet",
    "consectetur", "adipisci", "velit", "sed", "quia", "non", "numquam",
    "eius", "modi", "tempora", "incidunt", "ut", "labore", "et", "dolore",
    "magnam", "aliquam", "quaerat", "voluptatem", "ut", "enim", "ad",
    "minima", "veniam", "quis", "nostrum", "exercitationem", "ullam",
    "corporis", "nemo", "enim", "ipsam", "voluptatem", "quia", "voluptas",
    "sit", "suscipit", "laboriosam", "nisi", "ut", "aliquid", "ex", "ea",
    "commodi", "consequatur", "quis", "autem", "vel", "eum", "iure",
    "reprehenderit", "qui", "in", "ea", "voluptate", "velit", "esse",
    "quam", "nihil", "molestiae", "et", "iusto", "odio", "dignissimos",
    "ducimus", "qui", "blanditiis", "praesentium", "laudantium", "totam",
    "rem", "voluptatum", "deleniti", "atque", "corrupti", "quos",
    "dolores", "et", "quas", "molestias", "excepturi", "sint",
    "occaecati", "cupiditate", "non", "provident", "sed", "ut",
    "perspiciatis", "unde", "omnis", "iste", "natus", "error",
    "similique", "sunt", "in", "culpa", "qui", "officia", "deserunt",
    "mollitia", "animi", "id", "est", "laborum", "et", "dolorum", "fuga",
    "et", "harum", "quidem", "rerum", "facilis", "est", "et", "expedita",
    "distinctio", "nam", "libero", "tempore", "cum", "soluta", "nobis",
    "est", "eligendi", "optio", "cumque", "nihil", "impedit", "quo",
    "porro", "quisquam", "est", "qui", "minus", "id", "quod", "maxime",
    "placeat", "facere", "possimus", "omnis", "voluptas", "assumenda",
    "est", "omnis", "dolor", "repellendus", "temporibus", "autem",
    "quibusdam", "et", "aut", "consequatur", "vel", "illum", "qui",
    "dolorem", "eum", "fugiat", "quo", "voluptas", "nulla", "pariatur",
    "at", "vero", "eos", "et", "accusamus", "officiis", "debitis", "aut",
    "rerum", "necessitatibus", "saepe", "eveniet", "ut", "et",
    "voluptates", "repudiandae", "sint", "et", "molestiae", "non",
    "recusandae", "itaque", "earum", "rerum", "hic", "tenetur", "a",
    "sapiente", "delectus", "ut", "aut", "reiciendis", "voluptatibus",
    "maiores", "doloribus", "asperiores", "repellat",
  ]
}

extension String {
  var firstCapitalized: String {
    var string = self
    string.replaceSubrange(string.startIndex ... string.startIndex, with: String(string[string.startIndex]).capitalized)
    return string
  }
}

extension Array {
  /// Shuffle the array in-place using the Fisher-Yates algorithm.
  public mutating func shuffle() {
    if count == 0 {
      return
    }

    for i in 0 ..< (count - 1) {
      let j = Int(arc4random_uniform(UInt32(count - i))) + i
      if j != i {
        swapAt(i, j)
      }
    }
  }

  /// Return a shuffled version of the array using the Fisher-Yates
  /// algorithm.
  ///
  /// - returns: Returns a shuffled version of the array.
  public func shuffled() -> [Element] {
    var list = self
    list.shuffle()

    return list
  }

  /// Return a random element from the array.
  /// - returns: Returns a random element from the array or `nil` if the
  /// array is empty.
  public func random() -> Element? {
    (count > 0) ? shuffled()[0] : nil
  }

  /// Return a random subset of `cnt` elements from the array.
  /// - returns: Returns a random subset of `cnt` elements from the array.
  public func random(_ count: Int = 1) -> [Element] {
    let result = shuffled()

    return (count > result.count) ? result : Array(result[0 ..< count])
  }
}

extension Int {
  /// Return a random number between `min` and `max`.
  /// - note: The maximum value cannot be more than `UInt32.max - 1`
  ///
  /// - parameter min: The minimum value of the random value (defaults to `0`).
  /// - parameter max: The maximum value of the random value (defaults to `UInt32.max - 1`)
  ///
  /// - returns: Returns a random value between `min` and `max`.
  public static func random(min: Int = 0, max: Int = Int.max) -> Int {
    precondition(min <= max, "attempt to call random() with min > max")

    let diff = UInt(bitPattern: max &- min)
    let result = UInt.random(min: 0, max: diff)

    return min + Int(bitPattern: result)
  }

  public func randomize(variation: Int) -> Int {
    let multiplier = Double(Int.random(min: 100 - variation, max: 100 + variation)) / 100
    let randomized = Double(self) * multiplier

    return Int(randomized) + 1
  }
}

extension UInt {
  fileprivate static func random(min: UInt, max: UInt) -> UInt {
    precondition(min <= max, "attempt to call random() with min > max")

    if min == UInt.min, max == UInt.max {
      var result: UInt = 0
      arc4random_buf(&result, MemoryLayout.size(ofValue: result))

      return result
    } else {
      let range = max - min + 1
      let limit = UInt.max - UInt.max % range
      var result: UInt = 0

      repeat {
        arc4random_buf(&result, MemoryLayout.size(ofValue: result))
      } while result >= limit

      result = result % range

      return min + result
    }
  }
}
