// Copyright 2023 Google LLC
//
// 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.

#if os(iOS)

  import Foundation
  import SafariServices
  import UIKit
  import WebKit

  /// A Class responsible for presenting URL via SFSafariViewController or WKWebView.
  @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  class AuthURLPresenter: NSObject,
    SFSafariViewControllerDelegate, AuthWebViewControllerDelegate {
    /// Presents an URL to interact with user.
    /// - Parameter url: The URL to present.
    /// - Parameter uiDelegate: The UI delegate to present view controller.
    /// - Parameter completion: A block to be called either synchronously if the presentation fails
    /// to start, or asynchronously in future on an unspecified thread once the presentation
    /// finishes.
    func present(_ url: URL,
                 uiDelegate: AuthUIDelegate?,
                 callbackMatcher: @escaping (URL?) -> Bool,
                 completion: @escaping (URL?, Error?) -> Void) {
      if isPresenting {
        // Unable to start a new presentation on top of another.
        // Invoke the new completion closure and leave the old one as-is
        // to be invoked when the presentation finishes.
        DispatchQueue.main.async {
          completion(nil, AuthErrorUtils.webContextCancelledError(message: nil))
        }
        return
      }
      isPresenting = true
      self.callbackMatcher = callbackMatcher
      self.completion = completion
      DispatchQueue.main.async {
        self.uiDelegate = uiDelegate ?? AuthDefaultUIDelegate.defaultUIDelegate()
        #if targetEnvironment(macCatalyst)
          self.webViewController = AuthWebViewController(url: url, delegate: self)
          if let webViewController = self.webViewController {
            let navController = UINavigationController(rootViewController: webViewController)
            if let fakeUIDelegate = self.fakeUIDelegate {
              fakeUIDelegate.present(navController, animated: true)
            } else {
              self.uiDelegate?.present(navController, animated: true)
            }
          }
        #else
          self.safariViewController = SFSafariViewController(url: url)
          self.safariViewController?.delegate = self
          if let safariViewController = self.safariViewController {
            if let fakeUIDelegate = self.fakeUIDelegate {
              fakeUIDelegate.present(safariViewController, animated: true)
            } else {
              self.uiDelegate?.present(safariViewController, animated: true)
            }
          }
        #endif
      }
    }

    /// Determines if a URL was produced by the currently presented URL.
    /// - Parameter url: The URL to handle.
    /// - Returns: Whether the URL could be handled or not.
    func canHandle(url: URL) -> Bool {
      if isPresenting,
         let callbackMatcher = callbackMatcher,
         callbackMatcher(url) {
        finishPresentation(withURL: url, error: nil)
        return true
      }
      return false
    }

    // MARK: SFSafariViewControllerDelegate

    func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
      kAuthGlobalWorkQueue.async {
        if controller == self.safariViewController {
          self.safariViewController = nil
          // TODO: Ensure that the SFSafariViewController is actually removed from the screen
          // before invoking finishPresentation
          self.finishPresentation(withURL: nil,
                                  error: AuthErrorUtils.webContextCancelledError(message: nil))
        }
      }
    }

    // MARK: AuthWebViewControllerDelegate

    func webViewControllerDidCancel(_ controller: AuthWebViewController) {
      kAuthGlobalWorkQueue.async {
        if self.webViewController == controller {
          self.finishPresentation(withURL: nil,
                                  error: AuthErrorUtils.webContextCancelledError(message: nil))
        }
      }
    }

    func webViewController(_ controller: AuthWebViewController, canHandle url: URL) -> Bool {
      var result = false
      kAuthGlobalWorkQueue.sync {
        if self.webViewController == controller {
          result = self.canHandle(url: url)
        }
      }
      return result
    }

    func webViewController(_ controller: AuthWebViewController,
                           didFailWithError error: Error) {
      kAuthGlobalWorkQueue.async {
        if self.webViewController == controller {
          self.finishPresentation(withURL: nil, error: error)
        }
      }
    }

    /// Whether or not some web-based content is being presented.
    ///
    /// Accesses to this property are serialized on the global Auth work queue
    /// and thus this variable should not be read or written outside of the work queue.
    private var isPresenting: Bool = false

    /// The callback URL matcher for the current presentation, if one is active.
    private var callbackMatcher: ((URL) -> Bool)?

    /// The SFSafariViewController used for the current presentation, if any.
    private var safariViewController: SFSafariViewController?

    /// The `AuthWebViewController` used for the current presentation, if any.
    private var webViewController: AuthWebViewController?

    /// The UIDelegate used to present the SFSafariViewController.
    var uiDelegate: AuthUIDelegate?

    /// The completion handler for the current presentation, if one is active.
    ///
    /// Accesses to this variable are serialized on the global Auth work queue
    /// and thus this variable should not be read or written outside of the work queue.
    ///
    /// This variable is also used as a flag to indicate a presentation is active.
    var completion: ((URL?, Error?) -> Void)?

    /// Test-only option to validate the calls to the uiDelegate.
    var fakeUIDelegate: AuthUIDelegate?

    // MARK: Private methods

    private func finishPresentation(withURL url: URL?, error: Error?) {
      callbackMatcher = nil
      let uiDelegate = self.uiDelegate
      self.uiDelegate = nil
      let completion = self.completion
      self.completion = nil
      let safariViewController = self.safariViewController
      self.safariViewController = nil
      let webViewController = self.webViewController
      self.webViewController = nil
      if safariViewController != nil || webViewController != nil {
        DispatchQueue.main.async {
          uiDelegate?.dismiss(animated: true) {
            self.isPresenting = false
            if let completion {
              completion(url, error)
            }
          }
        }
      } else {
        isPresenting = false
        if let completion {
          completion(url, error)
        }
      }
    }
  }
#endif
