//
//  ViewController.swift
//  MVMCoreUI
//
//  Created by Scott Pfeil on 11/5/19.
//  Copyright © 2019 Verizon Wireless. All rights reserved.
//

import UIKit
import MVMCore

@objc open class ViewController: UIViewController, MVMCoreViewControllerProtocol, MVMCoreViewManagerViewControllerProtocol, MoleculeDelegateProtocol, FormHolderProtocol, MVMCoreActionDelegateProtocol, ActionDelegateProtocol, MVMCoreLoadDelegateProtocol, UITextFieldDelegate, UITextViewDelegate, ObservingTextFieldDelegate, MVMCoreUIDetailViewProtocol, PageProtocol, PageBehaviorHandlerProtocol {
    
    //--------------------------------------------------
    // MARK: - Properties
    //--------------------------------------------------
    
    @objc public var pageType: String?
    @objc public var loadObject: MVMCoreLoadObject?
    public var model: MVMControllerModelProtocol?
    public var pageModel: PageModelProtocol? {
        get { model }
        set { model = newValue as? MVMControllerModelProtocol }
    }
    
    /// Set if this page is containted in a manager.
    public weak var manager: (UIViewController & MVMCoreViewManagerProtocol)?
    
    /// A temporary iVar backer for delegateObject() until we change the protocol
    open lazy var delegateObjectIVar: MVMCoreUIDelegateObject = {
        MVMCoreUIDelegateObject.create(withDelegateForAll: self)
    }()
    
    public func delegateObject() -> DelegateObject? { delegateObjectIVar }
    
    public var formValidator: FormValidator?
    
    public var behaviors: [PageBehaviorProtocol]?
    
    public var needsUpdateUI = false
    private var observingForResponses = false
    private var initialLoadFinished = false
    public var previousScreenSize = CGSize.zero
    
    public var selectedField: UIView?
    
    /// Checks if the screen width has changed
    open func screenSizeChanged() -> Bool {
        !MVMCoreGetterUtility.cgfequalwiththreshold(previousScreenSize.width, view.bounds.size.width, 0.1)
    }
    
    //--------------------------------------------------
    // MARK: - Response handling
    //--------------------------------------------------
    
    open func observeForResponseJSONUpdates() {
        guard !observingForResponses,
              (pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0)
        else { return }
        
        observingForResponses = true
        NotificationCenter.default.addObserver(self, selector: #selector(responseJSONUpdated(notification:)), name: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil)
    }
    
    open func stopObservingForResponseJSONUpdates() {
        guard observingForResponses else { return }
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil)
        observingForResponses = false
    }
    
    open func pagesToListenFor() -> [String]? {
        guard let pageType = loadObject?.pageType else { return nil }
        return [pageType]
    }
    
    open func modulesToListenFor() -> [String]? {
        loadObject?.requestParameters?.allModules()
    }
    
    @objc open func responseJSONUpdated(notification: Notification) {
        // Checks for a page we are listening for.
        var newData = false
        if let pagesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyPageMap),
           let pageType = pagesToListenFor()?.first(where: { (pageTypeListened) -> Bool in
            guard let page = pagesLoaded.optionalDictionaryForKey(pageTypeListened),
                  let pageType = page.optionalStringForKey(KeyPageType),
                  pageType == pageTypeListened
            else { return false }
            
            return true
           }) {
            newData = true
            loadObject?.pageJSON = pagesLoaded.optionalDictionaryForKey(pageType)
        }
        
        // Checks for modules we are listening for.
        if let modulesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyModuleMap),
           let modulesListened = modulesToListenFor() {
            for moduleName in modulesListened {
                if let module = modulesLoaded.optionalDictionaryForKey(moduleName) {
                    newData = true
                    var currentModules = loadObject?.modulesJSON ?? [:]
                    currentModules.updateValue(module, forKey: moduleName)
                    loadObject?.modulesJSON = currentModules
                }
            }
        }
        
        guard newData else { return }
        
        do {
            try parsePageJSON()
            MVMCoreDispatchUtility.performBlock(onMainThread: {
                self.handleNewDataAndUpdateUI()
            })
        } catch {
            if let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: "updateJSON for pageType: \(String(describing: pageType))") {
                MVMCoreLoggingHandler.addError(toLog: coreError)
            }
        }
    }
    
    open func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>) -> Bool {
        pageType = loadObject.pageType
        self.loadObject = loadObject
        
        // Verifies all modules needed are loaded.
        guard ViewController.verifyRequiredModulesLoaded(for: loadObject, error: error) else { return false }
        
        // Parse the model for the page.
        do {
            try parsePageJSON()
        } catch let parsingError {
            // Log all parsing errors and fail load.
            handleLoggingFor(parsingError: parsingError)
            if let errorObject = MVMCoreErrorObject.createErrorObject(for: parsingError, location: MVMCoreLoadHandler.sharedGlobal()?.errorLocation(forRequest: loadObject)) {
                errorObject.messageToDisplay = MVMCoreGetterUtility.hardcodedString(withKey: HardcodedErrorUnableToProcess)
                error.pointee = errorObject
            }
            return false
        }
        
        if let pageData = loadObject.dataForPage {
            executeBehaviors { (behavior: PageLocalDataShareBehavior) in
                behavior.receiveLocalPageData(pageData, delegateObjectIVar)
            }
        }
        
        return true
    }
    
    func handleLoggingFor(parsingError: Error) {
        if let registryError = parsingError as? ModelRegistry.Error {
            switch (registryError) {
            case .decoderErrorModelNotMapped(let identifier, let codingKey, let codingPath) where identifier != nil && codingKey != nil && codingPath != nil:
                MVMCoreLoggingHandler.shared()?.handleDebugMessage("Error parsing template: Model identifier \"\(identifier!)\" is not mapped for  \"\(codingKey!.stringValue)\" @ \(codingPath!.map { return $0.stringValue })")
                
            case .decoderErrorObjectNotPresent(let codingKey, codingPath: let codingPath):
                MVMCoreLoggingHandler.shared()?.handleDebugMessage("Error parsing template: Required model \"\(codingKey.stringValue)\" was not found @ \(codingPath.map { return $0.stringValue })")
                
            default:
                MVMCoreLoggingHandler.shared()?.handleDebugMessage("Error parsing template: Registry error: \(registryError)")
            }
        }
        if let decodingError = parsingError as? DecodingError {
            switch (decodingError) {
            case .keyNotFound(let codingKey, let context):
                MVMCoreLoggingHandler.shared()?.handleDebugMessage("Error parsing template: Key \(codingKey.stringValue) was not found @ \(context.codingPath.map { return $0.stringValue })")
                
            case .valueNotFound(_, let context):
                MVMCoreLoggingHandler.shared()?.handleDebugMessage("Error parsing template: Value not found @ \(context.codingPath.map { return $0.stringValue })")
                
            case .typeMismatch(_, let context):
                MVMCoreLoggingHandler.shared()?.handleDebugMessage("Error parsing template: Type mismatch @ \(context.codingPath.map { return $0.stringValue })")
                
            case .dataCorrupted(let context):
                MVMCoreLoggingHandler.shared()?.handleDebugMessage("Error parsing template: Data corrupted @ \(context.codingPath.map { return $0.stringValue })")
                
            @unknown default:
                MVMCoreLoggingHandler.shared()?.handleDebugMessage("Error parsing template: \(parsingError)")
            }
        }
    }
    
    open func parsePageJSON() throws { }
    
    open class func verifyRequiredModulesLoaded(for loadObject: MVMCoreLoadObject?, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>) -> Bool {
        guard let pageType = loadObject?.pageType,
              var modulesRequired = MVMCoreUIViewControllerMappingObject.shared()?.modulesRequired(forPageType: pageType),
              !modulesRequired.isEmpty
        else { return true }
        
        guard let loadedModules = loadObject?.modulesJSON else { return false }
        
        for case let key as String in Array(loadedModules.keys) {
            guard modulesRequired.count > 0 else { break }
            if let index = modulesRequired.firstIndex(where: {($0 as? String) == key}) {
                modulesRequired.remove(at: index)
            }
        }
        
        guard modulesRequired.count == 0 else {
            if let errorObject = MVMCoreErrorObject(title: nil, message: MVMCoreGetterUtility.hardcodedString(withKey: HardcodedErrorCritical), messageToLog: modulesRequired.description, code: ErrorCode.requiredModuleNotPresent.rawValue, domain: ErrorDomainNative, location: MVMCoreLoadHandler.sharedGlobal()?.errorLocation(forRequest: loadObject!)) { 
                error.pointee = errorObject
            }
            return false
        }
        return true
    }
    
    /// Calls processNewData and then sets the ui to update with updateView
    open func handleNewDataAndUpdateUI() {
        handleNewData()
        needsUpdateUI = true
        view.setNeedsLayout()
    }
    
    /// Creates a legacy navigation model.
    open func createDefaultLegacyNavigationModel() -> NavigationItemModel {
        let navigationModel = NavigationItemModel()
        navigationModel.title = model?.screenHeading
        return navigationModel
    }
    
    /// Processes any new data. Called after the page is loaded the first time and on response updates for this page,
    open func handleNewData() {
        if model?.navigationBar == nil {
            let navigationItem = createDefaultLegacyNavigationModel()
            model?.navigationBar = navigationItem
        }
        
        executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in
            behavior.onPageNew(rootMolecules: getRootMolecules(), delegateObjectIVar)
        }
        
        if formValidator == nil {
            let rules = model?.formRules
            formValidator = FormValidator(rules)
        }
        
        if let backgroundColor = model?.backgroundColor {
            view.backgroundColor = backgroundColor.uiColor
        }
        
        // Notify the manager of new data
        manager?.newDataReceived?(in: self)
    }
    
    public func generateMoleculeView(from model: MoleculeModelProtocol) -> MoleculeViewProtocol? {
        executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in
            behavior.willSetupMolecule(with: model, updating: nil)
        }
        guard let moleculeView = ModelRegistry.createMolecule(model, delegateObject: delegateObjectIVar) else { return nil }
        executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in
            behavior.didSetupMolecule(view: moleculeView, withModel: model)
        }
        return moleculeView
    }
    
    public func updateMoleculeView(_ view: MoleculeViewProtocol, from model: MoleculeModelProtocol) {
        executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in
            behavior.willSetupMolecule(with: model, updating: view)
        }
        view.set(with: model, delegateObjectIVar, nil)
        executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in
            behavior.didSetupMolecule(view: view, withModel: model)
        }
    }
    
    //--------------------------------------------------
    // MARK: - Navigation Item
    //--------------------------------------------------
    
    open func getNavigationModel() -> NavigationItemModelProtocol? {
        // TODO: remove legacy. Temporary, convert legacy to navigation model.
        if model?.navigationBar == nil {
            let navigationItem = createDefaultLegacyNavigationModel()
            model?.navigationBar = navigationItem
        }
        return model?.navigationBar
    }
    
    //--------------------------------------------------
    // MARK: - View Lifecycle
    //--------------------------------------------------
    
    /// Called only once in viewDidLoad
    open func initialLoad() {
        observeForResponseJSONUpdates()
    }
    
    /// Called on screen size update.
    open func updateViews() {
        _ = formValidator?.validate()
    }
    
    override open func viewDidLoad() {
        super.viewDidLoad()
        
        // Do any additional setup after loading the view.
        MVMCoreLoggingHandler.logDebugMessage(withDelegate: "View Controller Loaded : \(self)")
        
        // We use our own margins.
        viewRespectsSystemMinimumLayoutMargins = false
        
        // Presents from the bottom.
        modalPresentationStyle = MVMCoreGetterUtility.isOnIPad() ? .formSheet : .overCurrentContext
        
        // Do some initial loading.
        if !initialLoadFinished {
            initialLoadFinished = true
            initialLoad()
        }
        
        handleNewDataAndUpdateUI()
    }
    
    open override func viewDidLayoutSubviews() {
        // Add to fix a constraint bug where the width is zero and things get messed up.
        guard isViewLoaded, view.bounds.width > 1 else {
            super.viewDidLayoutSubviews()
            return
        }
        
        // First update should be explicit (hence the zero check)
        if needsUpdateUI || (previousScreenSize != .zero && screenSizeChanged()) {
            needsUpdateUI = false
            updateViews()
        }
        
        previousScreenSize = view.bounds.size;
        
        super.viewDidLayoutSubviews()
    }
    
    open func pageShown() {
        // Track.
        MVMCoreUISession.sharedGlobal()?.currentPageType = pageType
        MVMCoreUILoggingHandler.shared()?.defaultLogPageState(forController: self)
        
        executeBehaviors { [weak self] (behavior: PageVisibilityBehavior) in
            behavior.onPageShown(self?.delegateObjectIVar)
        }
    }
    
    open override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        executeBehaviors { [weak self] (behavior: PageVisibilityBehavior) in
            behavior.willShowPage(self?.delegateObjectIVar)
        }
    }
    
    open override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        if manager == nil {
            pageShown()
        }
    }
    
    open override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        executeBehaviors { [weak self] (behavior: PageVisibilityBehavior) in
            behavior.willHidePage(self?.delegateObjectIVar)
        }
    }
    
    open override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        
        executeBehaviors { [weak self] (behavior: PageVisibilityBehavior) in
            behavior.onPageHidden(self?.delegateObjectIVar)
        }
    }
    
    deinit {
        stopObservingForResponseJSONUpdates()
        MVMCoreLoggingHandler.logDebugMessage(withDelegate: "View Controller Deallocated : \(self)")
    }
    
    open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        MVMCoreGetterUtility.isOnIPad() ? UIInterfaceOrientationMask.all : UIInterfaceOrientationMask.portrait
    }
    
    open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        
        // Updates the detail view width
        coordinator.animate(alongsideTransition: { UIViewControllerTransitionCoordinatorContext in
        }) { UIViewControllerTransitionCoordinatorContext in
            self.view.setNeedsLayout()
        }
    }
    //--------------------------------------------------
    // MARK: - MVMCoreViewManagerViewControllerProtocol
    //--------------------------------------------------
    
    open func viewControllerReady(inManager manager: UIViewController & MVMCoreViewManagerProtocol) {
        pageShown()
    }
    
    //--------------------------------------------------
    // MARK: - MVMCoreLoadDelegateProtocol
    //--------------------------------------------------
    
    // TODO: Move this function out of here after architecture cleanup.
    open func loadFinished(_ loadObject: MVMCoreLoadObject?, loadedViewController: (UIViewController & MVMCoreViewControllerProtocol)?, error: MVMCoreErrorObject?) {
        
        MVMCoreUILoggingHandler.log(withDelegateLoadFinished: loadObject, loadedViewController: loadedViewController, error: error)
        
        // Open the support panel
        if error == nil,
           loadObject?.requestParameters?.openSupportPanel ?? (loadObject?.systemParametersJSON?.boolForKey(KeyOpenSupport) ?? false) == true {
            MVMCoreUISession.sharedGlobal()?.splitViewController?.showRightPanel(animated: true)
        }
    }
    
    /// Override this method to avoid adding form params.
    open func addFormParams(requestParameters: MVMCoreRequestParameters, additionalData: [AnyHashable: Any]?) {
        formValidator?.addFormParams(requestParameters: requestParameters, model: MVMCoreUIActionHandler.getSourceModel(from: additionalData) as? MoleculeModelProtocol & FormItemProtocol)
    }
    
    public func handleFieldErrors(_ fieldErrors: [Any]?, loadObject: MVMCoreLoadObject) {
        
        for case let fieldError as [AnyHashable: Any] in fieldErrors ?? [] {
            
            guard let fieldName = fieldError["fieldName"] as? String,
                  let userError = fieldError["userMessage"] as? String,
                  let entryFieldModel = formValidator?.fields[fieldName] as? EntryFieldModel
            else { continue }
            
            entryFieldModel.shouldClearText = fieldError["clearText"] as? Bool ?? true
            entryFieldModel.dynamicErrorMessage = userError
        }
    }
    
    //--------------------------------------------------
    // MARK: - MVMCoreActionDelegateProtocol
    //--------------------------------------------------
    
    open func logAction(withActionInformation actionInformation: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?) {
        MVMCoreUILoggingHandler.shared()?.defaultLogAction(forController: self, actionInformation: actionInformation, additionalData: additionalData)
    }
    
    open func performAction(with model: ActionModelProtocol, additionalData: [AnyHashable : Any]?, delegateObject: DelegateObject?) async throws {
        var additionalData = additionalData
        if let model = model as? ActionOpenPageModel {
            addFormParams(requestParameters: model.requestParameters, additionalData: additionalData)
            model.requestParameters.parentPageType = loadObject?.pageJSON?.optionalStringForKey("parentPageType")
            
            var pageForwardedData = additionalData ?? [:]
            executeBehaviors { (behavior: PageLocalDataShareBehavior) in
                let dataMap = behavior.compileLocalPageDataForTransfer(delegateObjectIVar)
                pageForwardedData.merge(dataMap) { current, _ in current }
            }
            additionalData = pageForwardedData
        }
        
        do {
            if let behavior = behaviors?.compactMap({ $0 as? PageCustomActionHandlerBehavior }).first(where: { $0.canHandleAction(with: model, additionalData: additionalData) }) {
                logAction(withActionInformation: model.toJSON(), additionalData: additionalData)
                try await behavior.handleAction(with: model, additionalData: additionalData)
            } else {
                try await MVMCoreUIActionHandler.shared()?.handleAction(with: model, additionalData: additionalData, delegateObject: delegateObject)
            }
        } catch {
            handleAction(error: error, model: model, additionalData: additionalData, delegateObject: delegateObject)
            throw error
        }
    }
    
    open func handleAction(error: Error, model: ActionModelProtocol, additionalData: [AnyHashable : Any]?, delegateObject: DelegateObject?) {
        let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: MVMCoreActionHandler.getErrorLocation(with: delegateObject?.actionDelegate, actionType: model.actionType))!
        MVMCoreUIActionHandler.shared()?.defaultHandleActionError(errorObject, additionalData: additionalData)
    }
    
    //--------------------------------------------------
    // MARK: - MoleculeDelegateProtocol
    //--------------------------------------------------
    
    open func getRootMolecules() -> [MoleculeModelProtocol] {
        model?.rootMolecules ?? []
    }
    
    // Needed otherwise when subclassed, the extension gets called.
    open func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) { }
    
    //--------------------------------------------------
    // MARK: - MVMCoreUIDetailViewProtocol
    //--------------------------------------------------
    
    public func isLeftPanelAccessible() -> Bool {
        // TODO: Remove when hamburger menu is fully phased out.
        if loadObject?.pageJSON?.boolForKey(KeyHideMainMenu) ?? false {
            return false
        }
        return MVMCoreUISession.sharedGlobal()?.launchAppLoadedSuccessfully ?? false
    }
    
    public func isRightPanelAccessible() -> Bool {
        // TODO: Remove when FAB is 100%.
        if loadObject?.pageJSON?.boolForKey(KeyHideMainMenu) ?? false {
            return false
        }
        return (MVMCoreUISession.sharedGlobal()?.launchAppLoadedSuccessfully ?? false) || showRightPanelForScreenBeforeLaunchApp()
    }
    
    open func showRightPanelForScreenBeforeLaunchApp() -> Bool {
        loadObject?.pageJSON?.lenientBoolForKey("showRightPanel") ?? false
    }
    
    // TODO: make molecular
    open func isOverridingRightButton() -> Bool {
        guard let rightPanelLink = loadObject?.pageJSON?.optionalDictionaryForKey("rightPanelButtonLink")
        else { return false }
        MVMCoreActionHandler.shared()?.handleAction(with: rightPanelLink, additionalData: nil, delegateObject: delegateObject())
        return true
    }
    
    // TODO: make molecular
    open func isOverridingLeftButton() -> Bool {
        guard let leftPanelLink = loadObject?.pageJSON?.optionalDictionaryForKey("leftPanelButtonLink")
        else { return false }
        MVMCoreActionHandler.shared()?.handleAction(with: leftPanelLink, additionalData: nil, delegateObject: delegateObject())
        return true
    }
    
    // Eventually will be moved to Model
    open func bottomProgress() -> Float? {
        guard let progressString = loadObject?.pageJSON?.optionalStringForKey(KeyProgressPercent),
              let progress = Float(progressString)
        else { return nil }
        
        return progress / Float(100)
    }
    
    //--------------------------------------------------
    // MARK: - UITextFieldDelegate
    //--------------------------------------------------
    
    // To Remove TextFields Bug: Keyboard is not dismissing after reaching textfield max length limit
    open func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
    
    open func textFieldDidBeginEditing(_ textField: UITextField) {
        selectedField = textField
        
        // TODO: Make this into a protocol
        if UIAccessibility.isVoiceOverRunning {
            if let toolBar = textField.inputAccessoryView as? UIToolbar,
               let _ = toolBar.items?.last,
               let pickerView = textField.inputView as? UIPickerView {
                
                view.accessibilityElements = [pickerView, toolBar]
            }
            
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                UIAccessibility.post(notification: .layoutChanged, argument: textField.inputView)
            }
        }
    }
    
    open func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
        
        if textField === selectedField {
            if UIAccessibility.isVoiceOverRunning {
                view.accessibilityElements = nil
                UIAccessibility.post(notification: .layoutChanged, argument: textField)
            }
            
            selectedField = nil
        }
    }
    
    @objc open func dismissFieldInput(_ sender: Any?) {
        selectedField?.resignFirstResponder()
    }
    
    //--------------------------------------------------
    // MARK: - UITextViewDelegate
    //--------------------------------------------------
    
    public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
        selectedField = textView
        return true
    }
    
    open func textViewDidBeginEditing(_ textView: UITextView) {
        selectedField = textView
    }
    
    open func textViewDidEndEditing(_ textView: UITextView) {
        if textView === selectedField {
            selectedField = nil
        }
    }
}
