/*
 * Copyright 2021 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.
 */

import SwiftUI

#if SWIFT_PACKAGE
  @_exported import FirebaseFirestoreInternalWrapper
#else
  @_exported import FirebaseFirestoreInternal
#endif // SWIFT_PACKAGE

@available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, *)
class FirestoreQueryObservable<T>: ObservableObject {
  @Published var items: T

  private let firestore = Firestore.firestore()
  private var listener: ListenerRegistration?

  private var setupListener: (() -> Void)!

  var shouldUpdateListener = true
  var configuration: FirestoreQuery<T>.Configuration {
    didSet {
      // prevent never-ending update cycle when updating the error field
      guard shouldUpdateListener else { return }

      removeListener()
      setupListener()
    }
  }

  init<U: Decodable>(configuration: FirestoreQuery<T>.Configuration) where T == [U] {
    items = []
    self.configuration = configuration
    setupListener = createListener { [weak self] querySnapshot, error in
      guard let self = self else { return }
      if let error {
        self.animated {
          self.items = []
          self.projectError(error)
        }
        return
      } else {
        self.animated {
          self.projectError(nil)
        }
      }

      guard let documents = querySnapshot?.documents else {
        self.animated {
          self.items = []
        }
        return
      }

      let decodedDocuments: [U] = documents.compactMap { queryDocumentSnapshot in
        let result = Result { try queryDocumentSnapshot.data(as: U.self) }
        switch result {
        case let .success(decodedDocument):
          return decodedDocument
        case let .failure(error):
          self.animated {
            self.projectError(error)
          }
          return nil
        }
      }

      if configuration.error != nil {
        if configuration.decodingFailureStrategy == .raise {
          self.animated {
            self.items = []
          }
        } else {
          self.animated {
            self.items = decodedDocuments
          }
        }
      } else {
        self.animated {
          self.items = decodedDocuments
        }
      }
    }

    setupListener()
  }

  init<U: Decodable>(configuration: FirestoreQuery<T>.Configuration) where T == Result<[U], Error> {
    items = .success([])
    self.configuration = configuration
    setupListener = createListener { [weak self] querySnapshot, error in
      guard let self = self else { return }
      if let error {
        self.animated {
          self.items = .failure(error)
          self.projectError(error)
        }
        return
      } else {
        self.animated {
          self.projectError(nil)
        }
      }

      guard let documents = querySnapshot?.documents else {
        self.animated {
          self.items = .success([])
        }
        return
      }

      let decodedDocuments: [U] = documents.compactMap { queryDocumentSnapshot in
        let result = Result { try queryDocumentSnapshot.data(as: U.self) }
        switch result {
        case let .success(decodedDocument):
          return decodedDocument
        case let .failure(error):
          self.animated {
            self.projectError(error)
          }
          return nil
        }
      }

      if let error = self.configuration.error {
        if configuration.decodingFailureStrategy == .raise {
          self.animated {
            self.items = .failure(error)
          }
        } else {
          self.animated {
            self.items = .success(decodedDocuments)
          }
        }
      } else {
        self.animated {
          self.items = .success(decodedDocuments)
        }
      }
    }

    setupListener()
  }

  deinit {
    removeListener()
  }

  private func createListener(with handler: @escaping (QuerySnapshot?, Error?) -> Void)
    -> () -> Void {
    return {
      var query: Query = self.firestore.collection(self.configuration.path)

      for predicate in self.configuration.predicates {
        switch predicate {
        case let .isEqualTo(field, value):
          query = query.whereField(field, isEqualTo: value)
        case let .isIn(field, values):
          query = query.whereField(field, in: values)
        case let .isNotIn(field, values):
          query = query.whereField(field, notIn: values)
        case let .arrayContains(field, value):
          query = query.whereField(field, arrayContains: value)
        case let .arrayContainsAny(field, values):
          query = query.whereField(field, arrayContainsAny: values)
        case let .isLessThan(field, value):
          query = query.whereField(field, isLessThan: value)
        case let .isGreaterThan(field, value):
          query = query.whereField(field, isGreaterThan: value)
        case let .isLessThanOrEqualTo(field, value):
          query = query.whereField(field, isLessThanOrEqualTo: value)
        case let .isGreaterThanOrEqualTo(field, value):
          query = query.whereField(field, isGreaterThanOrEqualTo: value)
        case let .orderBy(field, value):
          query = query.order(by: field, descending: value)
        case let .limitTo(field):
          query = query.limit(to: field)
        case let .limitToLast(field):
          query = query.limit(toLast: field)
        }
      }

      self.listener = query.addSnapshotListener(handler)
    }
  }

  private func projectError(_ error: Error?) {
    shouldUpdateListener = false
    configuration.error = error
    shouldUpdateListener = true
  }

  private func removeListener() {
    listener?.remove()
    listener = nil
  }

  private func animated(_ body: () -> Void) {
    if let animation = configuration.animation {
      withAnimation(animation) {
        body()
      }
    } else {
      body()
    }
  }
}
