// 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 UIKit

// MARK: - MessageLabel

open class MessageLabel: UILabel {
  // MARK: Lifecycle

  // MARK: - Initializers

  public override init(frame: CGRect) {
    super.init(frame: frame)
    setupView()
  }

  public required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setupView()
  }

  // MARK: Open

  // MARK: - Public Properties

  open weak var delegate: MessageLabelDelegate?

  open internal(set) var addressAttributes: [NSAttributedString.Key: Any] = defaultAttributes

  open internal(set) var dateAttributes: [NSAttributedString.Key: Any] = defaultAttributes

  open internal(set) var phoneNumberAttributes: [NSAttributedString.Key: Any] = defaultAttributes

  open internal(set) var urlAttributes: [NSAttributedString.Key: Any] = defaultAttributes

  open internal(set) var transitInformationAttributes: [NSAttributedString.Key: Any] = defaultAttributes

  open internal(set) var hashtagAttributes: [NSAttributedString.Key: Any] = defaultAttributes

  open internal(set) var mentionAttributes: [NSAttributedString.Key: Any] = defaultAttributes

  open internal(set) var customAttributes: [NSRegularExpression: [NSAttributedString.Key: Any]] = [:]

  open var enabledDetectors: [DetectorType] = [] {
    didSet {
      setTextStorage(attributedText, shouldParse: true)
    }
  }

  open override var attributedText: NSAttributedString? {
    didSet {
      setTextStorage(attributedText, shouldParse: true)
    }
  }

  open override var text: String? {
    didSet {
      setTextStorage(attributedText, shouldParse: true)
    }
  }

  // swiftlint:disable:next implicitly_unwrapped_optional
  open override var font: UIFont! {
    didSet {
      setTextStorage(attributedText, shouldParse: false)
    }
  }

  // swiftlint:disable:next implicitly_unwrapped_optional
  open override var textColor: UIColor! {
    didSet {
      setTextStorage(attributedText, shouldParse: false)
    }
  }

  open override var lineBreakMode: NSLineBreakMode {
    didSet {
      textContainer.lineBreakMode = lineBreakMode
      if !isConfiguring { setNeedsDisplay() }
    }
  }

  open override var numberOfLines: Int {
    didSet {
      textContainer.maximumNumberOfLines = numberOfLines
      if !isConfiguring { setNeedsDisplay() }
    }
  }

  open override var textAlignment: NSTextAlignment {
    didSet {
      setTextStorage(attributedText, shouldParse: false)
    }
  }

  open var textInsets: UIEdgeInsets = .zero {
    didSet {
      if !isConfiguring { setNeedsDisplay() }
    }
  }

  open override var intrinsicContentSize: CGSize {
    var size = super.intrinsicContentSize
    size.width += textInsets.horizontal
    size.height += textInsets.vertical
    return size
  }

  // MARK: - Open Methods

  open override func drawText(in rect: CGRect) {
    let insetRect = rect.inset(by: textInsets)
    textContainer.size = CGSize(width: insetRect.width, height: insetRect.height)

    let origin = insetRect.origin
    let range = layoutManager.glyphRange(for: textContainer)

    layoutManager.drawBackground(forGlyphRange: range, at: origin)
    layoutManager.drawGlyphs(forGlyphRange: range, at: origin)
  }

  open func handleGesture(_ touchLocation: CGPoint) -> Bool {
    guard let index = stringIndex(at: touchLocation) else { return false }

    for (detectorType, ranges) in rangesForDetectors {
      for (range, value) in ranges {
        if range.contains(index) {
          handleGesture(for: detectorType, value: value)
          return true
        }
      }
    }
    return false
  }

  // MARK: Public

  public static var defaultAttributes: [NSAttributedString.Key: Any] = {
    [
      NSAttributedString.Key.foregroundColor: UIColor.darkText,
      NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
      NSAttributedString.Key.underlineColor: UIColor.darkText,
    ]
  }()

  public func setAttributes(_ attributes: [NSAttributedString.Key: Any], detector: DetectorType) {
    switch detector {
    case .phoneNumber:
      phoneNumberAttributes = attributes
    case .address:
      addressAttributes = attributes
    case .date:
      dateAttributes = attributes
    case .url:
      urlAttributes = attributes
    case .transitInformation:
      transitInformationAttributes = attributes
    case .mention:
      mentionAttributes = attributes
    case .hashtag:
      hashtagAttributes = attributes
    case .custom(let regex):
      customAttributes[regex] = attributes
    }
    if isConfiguring {
      attributesNeedUpdate = true
    } else {
      updateAttributes(for: [detector])
    }
  }

  // MARK: - Public Methods

  public func configure(block: () -> Void) {
    isConfiguring = true
    block()
    if attributesNeedUpdate {
      updateAttributes(for: enabledDetectors)
    }
    attributesNeedUpdate = false
    isConfiguring = false
    setNeedsDisplay()
  }

  // MARK: Internal

  internal lazy var rangesForDetectors: [DetectorType: [(NSRange, MessageTextCheckingType)]] = [:]

  internal var messageLabelFont: UIFont?

  // MARK: Private

  // MARK: - Private Properties

  private lazy var layoutManager: NSLayoutManager = {
    let layoutManager = NSLayoutManager()
    layoutManager.addTextContainer(self.textContainer)
    return layoutManager
  }()

  private lazy var textContainer: NSTextContainer = {
    let textContainer = NSTextContainer()
    textContainer.lineFragmentPadding = 0
    textContainer.maximumNumberOfLines = self.numberOfLines
    textContainer.lineBreakMode = self.lineBreakMode
    textContainer.size = self.bounds.size
    return textContainer
  }()

  private lazy var textStorage: NSTextStorage = {
    let textStorage = NSTextStorage()
    textStorage.addLayoutManager(self.layoutManager)
    return textStorage
  }()

  private var isConfiguring = false

  private var attributesNeedUpdate = false

  // MARK: - Private Methods

  private func setTextStorage(_ newText: NSAttributedString?, shouldParse: Bool) {
    guard let newText = newText, newText.length > 0 else {
      textStorage.setAttributedString(NSAttributedString())
      setNeedsDisplay()
      return
    }

    let style = paragraphStyle(for: newText)
    let range = NSRange(location: 0, length: newText.length)

    let mutableText = NSMutableAttributedString(attributedString: newText)
    mutableText.addAttribute(.paragraphStyle, value: style, range: range)

    if shouldParse {
      rangesForDetectors.removeAll()
      let results = parse(text: mutableText)
      setRangesForDetectors(in: results)
    }

    for (detector, rangeTuples) in rangesForDetectors {
      if enabledDetectors.contains(detector) {
        let attributes = detectorAttributes(for: detector)
        rangeTuples.forEach { range, _ in
          mutableText.addAttributes(attributes, range: range)
        }
      }
    }

    let modifiedText = NSAttributedString(attributedString: mutableText)
    textStorage.setAttributedString(modifiedText)

    if !isConfiguring { setNeedsDisplay() }
  }

  private func paragraphStyle(for text: NSAttributedString) -> NSParagraphStyle {
    guard text.length > 0 else { return NSParagraphStyle() }

    var range = NSRange(location: 0, length: text.length)
    let existingStyle = text.attribute(.paragraphStyle, at: 0, effectiveRange: &range) as? NSMutableParagraphStyle
    let style = existingStyle ?? NSMutableParagraphStyle()

    style.lineBreakMode = lineBreakMode
    style.alignment = textAlignment

    return style
  }

  private func updateAttributes(for detectors: [DetectorType]) {
    guard let attributedText = attributedText, attributedText.length > 0 else { return }
    let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText)

    for detector in detectors {
      guard let rangeTuples = rangesForDetectors[detector] else { continue }

      for (range, _) in rangeTuples {
        // This will enable us to attribute it with our own styles, since `UILabel` does not provide link attribute overrides like `UITextView` does
        if detector.textCheckingType == .link {
          mutableAttributedString.removeAttribute(NSAttributedString.Key.link, range: range)
        }

        let attributes = detectorAttributes(for: detector)
        mutableAttributedString.addAttributes(attributes, range: range)
      }

      let updatedString = NSAttributedString(attributedString: mutableAttributedString)
      textStorage.setAttributedString(updatedString)
    }
  }

  private func detectorAttributes(for detectorType: DetectorType) -> [NSAttributedString.Key: Any] {
    switch detectorType {
    case .address:
      return addressAttributes
    case .date:
      return dateAttributes
    case .phoneNumber:
      return phoneNumberAttributes
    case .url:
      return urlAttributes
    case .transitInformation:
      return transitInformationAttributes
    case .mention:
      return mentionAttributes
    case .hashtag:
      return hashtagAttributes
    case .custom(let regex):
      return customAttributes[regex] ?? MessageLabel.defaultAttributes
    }
  }

  private func detectorAttributes(for checkingResultType: NSTextCheckingResult.CheckingType) -> [NSAttributedString.Key: Any] {
    switch checkingResultType {
    case .address:
      return addressAttributes
    case .date:
      return dateAttributes
    case .phoneNumber:
      return phoneNumberAttributes
    case .link:
      return urlAttributes
    case .transitInformation:
      return transitInformationAttributes
    default:
      fatalError(MessageKitError.unrecognizedCheckingResult)
    }
  }

  private func setupView() {
    numberOfLines = 0
    lineBreakMode = .byWordWrapping
  }

  // MARK: - Parsing Text

  private func parse(text: NSAttributedString) -> [NSTextCheckingResult] {
    guard enabledDetectors.isEmpty == false else { return [] }
    let range = NSRange(location: 0, length: text.length)
    var matches = [NSTextCheckingResult]()

    // Get matches of all .custom DetectorType and add it to matches array
    let regexs = enabledDetectors
      .filter { $0.isCustom }
      .map { parseForMatches(with: $0, in: text, for: range) }
      .joined()
    matches.append(contentsOf: removeOverlappingResults(Array(regexs)))

    // Get all Checking Types of detectors, except for .custom because they contain their own regex
    let detectorCheckingTypes = enabledDetectors
      .filter { !$0.isCustom }
      .reduce(0) { $0 | $1.textCheckingType.rawValue }
    if detectorCheckingTypes > 0, let detector = try? NSDataDetector(types: detectorCheckingTypes) {
      let detectorMatches = detector.matches(in: text.string, options: [], range: range)
      matches.append(contentsOf: detectorMatches)
    }

    guard enabledDetectors.contains(.url) else {
      return matches
    }

    // Enumerate NSAttributedString NSLinks and append ranges
    var results: [NSTextCheckingResult] = matches

    text.enumerateAttribute(NSAttributedString.Key.link, in: range, options: []) { value, range, _ in
      guard let url = value as? URL else { return }
      let result = NSTextCheckingResult.linkCheckingResult(range: range, url: url)
      results.append(result)
    }

    return results
  }

  private func parseForMatches(
    with detector: DetectorType,
    in text: NSAttributedString,
    for range: NSRange) -> [NSTextCheckingResult]
  {
    switch detector {
    case .custom(let regex):
      return regex.matches(in: text.string, options: [], range: range)
    default:
      fatalError("You must pass a .custom DetectorType")
    }
  }
  
  private func removeOverlappingResults(_ results: [NSTextCheckingResult]) -> [NSTextCheckingResult]
  {
    var filteredResults: [NSTextCheckingResult] = []
    
    for result in results {
      let overlappingResults = results.filter { $0.range.intersection(result.range)?.length ?? 0 > 0 }
      
      if overlappingResults.count <= 1 {
        filteredResults.append(result)
        continue
      }
      
      guard !filteredResults.contains(where: { $0.range == result.range }) else { continue }
      let maxRangeResult = overlappingResults.max { $0.range.upperBound - $0.range.lowerBound < $1.range.upperBound - $1.range.lowerBound }
      if let maxRangeResult {
        filteredResults.append(maxRangeResult)
      }
    }
    
    return filteredResults
  }

  private func setRangesForDetectors(in checkingResults: [NSTextCheckingResult]) {
    guard checkingResults.isEmpty == false else { return }

    for result in checkingResults {
      switch result.resultType {
      case .address:
        var ranges = rangesForDetectors[.address] ?? []
        let tuple: (NSRange, MessageTextCheckingType) = (result.range, .addressComponents(result.addressComponents))
        ranges.append(tuple)
        rangesForDetectors.updateValue(ranges, forKey: .address)
      case .date:
        var ranges = rangesForDetectors[.date] ?? []
        let tuple: (NSRange, MessageTextCheckingType) = (result.range, .date(result.date))
        ranges.append(tuple)
        rangesForDetectors.updateValue(ranges, forKey: .date)
      case .phoneNumber:
        var ranges = rangesForDetectors[.phoneNumber] ?? []
        let tuple: (NSRange, MessageTextCheckingType) = (result.range, .phoneNumber(result.phoneNumber))
        ranges.append(tuple)
        rangesForDetectors.updateValue(ranges, forKey: .phoneNumber)
      case .link:
        var ranges = rangesForDetectors[.url] ?? []
        let tuple: (NSRange, MessageTextCheckingType) = (result.range, .link(result.url))
        ranges.append(tuple)
        rangesForDetectors.updateValue(ranges, forKey: .url)
      case .transitInformation:
        var ranges = rangesForDetectors[.transitInformation] ?? []
        let tuple: (NSRange, MessageTextCheckingType) = (result.range, .transitInfoComponents(result.components))
        ranges.append(tuple)
        rangesForDetectors.updateValue(ranges, forKey: .transitInformation)
      case .regularExpression:
        guard
          let text = text, let regex = result.regularExpression,
          let range = Range(result.range, in: text) else { return }
        let detector = DetectorType.custom(regex)
        var ranges = rangesForDetectors[detector] ?? []
        let tuple: (NSRange, MessageTextCheckingType) = (
          result.range,
          .custom(pattern: regex.pattern, match: String(text[range])))
        ranges.append(tuple)
        rangesForDetectors.updateValue(ranges, forKey: detector)
      default:
        fatalError("Received an unrecognized NSTextCheckingResult.CheckingType")
      }
    }
  }

  // MARK: - Gesture Handling

  private func stringIndex(at location: CGPoint) -> Int? {
    guard textStorage.length > 0 else { return nil }

    var location = location

    location.x -= textInsets.left
    location.y -= textInsets.top

    let index = layoutManager.glyphIndex(for: location, in: textContainer)

    let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: index, effectiveRange: nil)

    var characterIndex: Int?

    if lineRect.contains(location) {
      characterIndex = layoutManager.characterIndexForGlyph(at: index)
    }

    return characterIndex
  }

  // swiftlint:disable cyclomatic_complexity
  private func handleGesture(for detectorType: DetectorType, value: MessageTextCheckingType) {
    switch value {
    case .addressComponents(let addressComponents):
      var transformedAddressComponents = [String: String]()
      guard let addressComponents = addressComponents else { return }
      addressComponents.forEach { key, value in
        transformedAddressComponents[key.rawValue] = value
      }
      handleAddress(transformedAddressComponents)
    case .phoneNumber(let phoneNumber):
      guard let phoneNumber = phoneNumber else { return }
      handlePhoneNumber(phoneNumber)
    case .date(let date):
      guard let date = date else { return }
      handleDate(date)
    case .link(let url):
      guard let url = url else { return }
      handleURL(url)
    case .transitInfoComponents(let transitInformation):
      var transformedTransitInformation = [String: String]()
      guard let transitInformation = transitInformation else { return }
      transitInformation.forEach { key, value in
        transformedTransitInformation[key.rawValue] = value
      }
      handleTransitInformation(transformedTransitInformation)
    case .custom(let pattern, let match):
      guard let match = match else { return }
      switch detectorType {
      case .hashtag:
        handleHashtag(match)
      case .mention:
        handleMention(match)
      default:
        handleCustom(pattern, match: match)
      }
    }
  }

  // swiftlint:enable cyclomatic_complexity

  private func handleAddress(_ addressComponents: [String: String]) {
    delegate?.didSelectAddress(addressComponents)
  }

  private func handleDate(_ date: Date) {
    delegate?.didSelectDate(date)
  }

  private func handleURL(_ url: URL) {
    delegate?.didSelectURL(url)
  }

  private func handlePhoneNumber(_ phoneNumber: String) {
    delegate?.didSelectPhoneNumber(phoneNumber)
  }

  private func handleTransitInformation(_ components: [String: String]) {
    delegate?.didSelectTransitInformation(components)
  }

  private func handleHashtag(_ hashtag: String) {
    delegate?.didSelectHashtag(hashtag)
  }

  private func handleMention(_ mention: String) {
    delegate?.didSelectMention(mention)
  }

  private func handleCustom(_ pattern: String, match: String) {
    delegate?.didSelectCustom(pattern, match: match)
  }
}

// MARK: - MessageTextCheckingType

internal enum MessageTextCheckingType {
  case addressComponents([NSTextCheckingKey: String]?)
  case date(Date?)
  case phoneNumber(String?)
  case link(URL?)
  case transitInfoComponents([NSTextCheckingKey: String]?)
  case custom(pattern: String, match: String?)
}
