import Foundation
import NitroModules
import RiveRuntime
import UIKit

private struct DefaultConfiguration {
  static let autoPlay = true
  static let alignment = RiveAlignment.center
  static let fit = RiveFit.contain
  static let layoutScaleFactor = RiveRuntime.RiveViewModel.layoutScaleFactorAutomatic
}

typealias HybridDataBindMode = Variant__any_HybridViewModelInstanceSpec__DataBindMode_DataBindByName

extension Optional
where Wrapped == HybridDataBindMode {
  func toDataBingMode() throws -> BindData {
    guard let value = self else {
      return .auto
    }

    switch value {
    case .first(let viewModelInstance):
      if let instance = (viewModelInstance as? HybridViewModelInstance)?.viewModelInstance {
        return .instance(instance)
      } else {
        throw RuntimeError.error(withMessage: "Invalid ViewModelInstance")
      }
    case .second(let mode):
      switch mode {
      case .auto:
        return .auto
      case .none:
        return .none
      }
    case .third(let dataBindByName):
      return .byName(dataBindByName.byName)
    }
  }
  
  /// Check if two HybridDataBindMode values are semantically equal
  func isEqual(to other: HybridDataBindMode?) -> Bool {
    guard let lhs = self, let rhs = other else {
      return self == nil && other == nil
    }
    
    switch (lhs, rhs) {
    case (.first(let lhsInstance), .first(let rhsInstance)):
      // Compare ViewModelInstance references
      let lhsVMI = (lhsInstance as? HybridViewModelInstance)?.viewModelInstance
      let rhsVMI = (rhsInstance as? HybridViewModelInstance)?.viewModelInstance
      return lhsVMI === rhsVMI
      
    case (.second(let lhsMode), .second(let rhsMode)):
      // Compare DataBindMode enums
      return lhsMode == rhsMode
      
    case (.third(let lhsByName), .third(let rhsByName)):
      // Compare byName strings
      return lhsByName.byName == rhsByName.byName
      
    default:
      return false
    }
  }
}

class HybridRiveView: HybridRiveViewSpec {
  func play() throws -> NitroModules.Promise<Void> {
    return Promise.async {
      try await self.getRiveView().play()
    }
  }

  func pause() throws -> NitroModules.Promise<Void> {
    return Promise.async {
      try await self.getRiveView().pause()
    }
  }

  func reset() throws -> NitroModules.Promise<Void> {
    return Promise.async {
      try await self.getRiveView().reset()
    }
  }
  
  func playIfNeeded() {
    try? self.getRiveView().playIfNeeded()
  }

  // MARK: View Props
  var dataBind: HybridDataBindMode? {
    didSet {
      if !dataBind.isEqual(to: oldValue) {
        dataBindingChanged = true
      }
    }
  }

  var artboardName: String? { didSet { needsReload = true } }
  var stateMachineName: String? { didSet { needsReload = true } }
  var autoPlay: Bool? { didSet { needsReload = true } }
  var file: (any HybridRiveFileSpec) = HybridRiveFile() {
    didSet { needsReload = true }
  }
  var alignment: Alignment?
  var fit: Fit?
  var layoutScaleFactor: Double?
  var onError: (RiveError) -> Void = { _ in }

  func awaitViewReady() throws -> Promise<Bool> {
    return Promise.async { [self] in
      return try await getRiveView().awaitViewReady()
    }
  }

  func bindViewModelInstance(viewModelInstance: (any HybridViewModelInstanceSpec)) throws {
    guard let viewModelInstance = (viewModelInstance as? HybridViewModelInstance)?.viewModelInstance
    else { return }
    try MainThread.run { try getRiveView().bindViewModelInstance(viewModelInstance: viewModelInstance) }
  }

  func getViewModelInstance() throws -> (any HybridViewModelInstanceSpec)? {
    let viewModelInstance = try MainThread.run { try getRiveView().getViewModelInstance() }
    guard let viewModelInstance else { return nil }
    return HybridViewModelInstance(viewModelInstance: viewModelInstance)
  }

  func onEventListener(onEvent: @escaping (UnifiedRiveEvent) -> Void) throws {
    try getRiveView().addEventListener(onEvent)
  }

  func removeEventListeners() throws { try getRiveView().removeEventListeners() }

  func setNumberInputValue(name: String, value: Double, path: String?) throws {
    try MainThread.run {
      try getRiveView().setNumberInputValue(name: name, value: Float(value), path: path)
    }
  }

  func getNumberInputValue(name: String, path: String?) throws -> Double {
    try MainThread.run {
      try Double(getRiveView().getNumberInputValue(name: name, path: path))
    }
  }

  func setBooleanInputValue(name: String, value: Bool, path: String?) throws {
    try MainThread.run {
      try getRiveView().setBooleanInputValue(name: name, value: value, path: path)
    }
  }

  func getBooleanInputValue(name: String, path: String?) throws -> Bool {
    try MainThread.run {
      try getRiveView().getBooleanInputValue(name: name, path: path)
    }
  }

  func triggerInput(name: String, path: String?) throws {
    try MainThread.run {
      try getRiveView().triggerInput(name: name, path: path)
    }
  }

  func setTextRunValue(name: String, value: String, path: String?) throws {
    try MainThread.run {
      try getRiveView().setTextRunValue(name: name, value: value, path: path)
    }
  }

  func getTextRunValue(name: String, path: String?) throws -> String {
    try MainThread.run {
      try getRiveView().getTextRunValue(name: name, path: path)
    }
  }

  // MARK: Views
  var view: UIView = RiveReactNativeView()
  func getRiveView() throws -> RiveReactNativeView {
    guard let riveView = view as? RiveReactNativeView else {
      throw RuntimeError.error(withMessage: "RiveReactNativeView is null or not configured")
    }
    return riveView
  }

  // MARK: Update
  func afterUpdate() {
    logged(tag: "HybridRiveView", note: "afterUpdate") {
      guard let hybridFile = file as? HybridRiveFile,
        let file = hybridFile.riveFile
      else { return }

      let config = ViewConfiguration(
        artboardName: artboardName,
        stateMachineName: stateMachineName,
        autoPlay: autoPlay ?? DefaultConfiguration.autoPlay,
        riveFile: file,
        viewSource: hybridFile,
        alignment: convertAlignment(alignment) ?? DefaultConfiguration.alignment,
        fit: convertFit(fit) ?? DefaultConfiguration.fit,
        layoutScaleFactor: layoutScaleFactor ?? DefaultConfiguration.layoutScaleFactor,
        bindData: try dataBind.toDataBingMode()
      )

      let riveView = try getRiveView()
      try riveView.configure(
        config, dataBindingChanged: dataBindingChanged, reload: needsReload,
        initialUpdate: initialUpdate)
      needsReload = false
      dataBindingChanged = false
      initialUpdate = false
    }
  }

  func dispose() {
    let riveView = view as? RiveReactNativeView
    DispatchQueue.main.async {
      riveView?.cleanup()
    }
  }

  // MARK: Internal State
  private var needsReload = false
  private var dataBindingChanged = false
  private var initialUpdate = true

  // MARK: Helpers
  private func convertAlignment(_ alignment: Alignment?) -> RiveAlignment? {
    guard let alignment = alignment else { return nil }

    switch alignment {
    case .topleft: return .topLeft
    case .topcenter: return .topCenter
    case .topright: return .topRight
    case .centerleft: return .centerLeft
    case .center: return .center
    case .centerright: return .centerRight
    case .bottomleft: return .bottomLeft
    case .bottomcenter: return .bottomCenter
    case .bottomright: return .bottomRight
    }
  }

  private func convertFit(_ fit: Fit?) -> RiveFit? {
    guard let fit = fit else { return nil }

    switch fit {
    case .fill: return .fill
    case .contain: return .contain
    case .cover: return .cover
    case .fitwidth: return .fitWidth
    case .fitheight: return .fitHeight
    case .none: return .noFit
    case .scaledown: return .scaleDown
    case .layout: return .layout
    }
  }
}

extension HybridRiveView {
  func logged(tag: String, note: String? = nil, _ fn: () throws -> Void) {
    do {
      return try fn()
    } catch let e {
      let (errorType, errorDescription) = detectErrorType(e)
      let noteString = note.map { " \($0)" } ?? ""
      let errorMessage = "[RIVE] \(tag)\(noteString) \(errorDescription)"

      let riveError = RiveError(
        message: errorMessage,
        type: errorType
      )
      onError(riveError)
    }
  }

  private func detectErrorType(_ error: Error) -> (RiveErrorType, String) {
    switch error {
    case NitroRiveError.instanceNotFound(let message):
      return (.viewmodelinstancenotfound, message)
    case NitroRiveError.fileNotFound(let message):
      return (.filenotfound, message)
    default:
      break
    }

    let nsError = error as NSError
    let message = nsError.localizedDescription

    // RiveErrorCode from RiveRuntime
    switch nsError.code {
    case RiveErrorCode.noArtboardFound.rawValue:
      return (.incorrectartboardname, message)
    case RiveErrorCode.noStateMachineFound.rawValue:
      return (.incorrectstatemachinename, message)
    case RiveErrorCode.noAnimationFound.rawValue:
      return (.unknown, message)
    case RiveErrorCode.malformedFile.rawValue:
      return (.malformedfile, message)
    case RiveErrorCode.noStateMachineInputFound.rawValue:
      return (.incorrectstatemachineinputname, message)
    default:
      return (.unknown, message)
    }
  }
}
