//
//  SyncResultViewController.swift
//  Delta
//
//  Created by Riley Testut on 11/28/18.
//  Copyright © 2018 Riley Testut. All rights reserved.
//

import UIKit

import Roxas
import Harmony

@objc(SyncResultTableViewCell)
private class SyncResultTableViewCell: UITableViewCell
{
    @IBOutlet var nameLabel: UILabel!
    @IBOutlet var errorLabel: UILabel!
}

extension SyncResultViewController
{
    private enum Group: Hashable
    {
        case game(RecordID)
        case saveState(gameID: RecordID)
        case cheat(gameID: RecordID)
        case controllerSkin
        case gameControllerInputMapping
        case gameCollection
        case other
        
        var sortIndex: Int {
            switch self
            {
            case .game: return 0
            case .saveState: return 1
            case .cheat: return 2
            case .controllerSkin: return 3
            case .gameControllerInputMapping: return 4
            case .gameCollection: return 5
            case .other: return 6
            }
        }
    }
}

extension Record
{
    var localizedTitle: String {
        guard let type = SyncManager.RecordType(rawValue: self.recordID.type) else { return self.localizedName ?? NSLocalizedString("Unknown", comment: "") }
        
        switch type
        {
        case .game: return NSLocalizedString("Game", comment: "")
        case .gameSave: return NSLocalizedString("Game Save", comment: "")
        case .saveState, .cheat, .controllerSkin, .gameCollection, .gameControllerInputMapping: return self.localizedName ?? type.localizedName
        }
    }
}

class SyncResultViewController: UITableViewController
{
    var result: Result<[Record<NSManagedObject>: Result<Void, RecordError>], SyncError>!
    
    private lazy var dataSource = self.makeDataSource()
    
    private lazy var sortedErrors = self.processResults()
    private lazy var gameNamesByRecordID = self.fetchGameNames()
    
    private init()
    {
        super.init(style: .grouped)
    }
    
    required init?(coder aDecoder: NSCoder)
    {
        super.init(coder: aDecoder)
    }
    
    override func viewDidLoad()
    {
        super.viewDidLoad()
        
        self.tableView.dataSource = self.dataSource
        
        if let navigationController = self.navigationController, navigationController.viewControllers.count != 1
        {
            self.navigationItem.rightBarButtonItem = nil
        }
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?)
    {
        guard segue.identifier == "showRecordStatus" else { return }
        
        guard let cell = sender as? UITableViewCell, let indexPath = self.tableView.indexPath(for: cell) else { return }
        
        guard let recordError = self.dataSource.item(at: indexPath).value as? RecordError else { return }
        
        let recordSyncStatusViewController = segue.destination as! RecordSyncStatusViewController
        recordSyncStatusViewController.record = recordError.record
    }
}

extension SyncResultViewController
{
    class func make(result: Result<[Record<NSManagedObject>: Result<Void, RecordError>], SyncError>) -> UINavigationController
    {
        let storyboard = UIStoryboard(name: "SyncResultsViewController", bundle: nil)
        
        let navigationController = storyboard.instantiateInitialViewController() as! UINavigationController
        
        let syncResultViewController = navigationController.viewControllers[0] as! SyncResultViewController
        syncResultViewController.result = result
        
        return navigationController
    }
}

private extension SyncResultViewController
{
    func makeDataSource() -> RSTCompositeTableViewDataSource<Box<Error>>
    {
        let dataSources = self.sortedErrors.map { (_, errors) -> RSTArrayTableViewDataSource<Box<Error>> in
            let dataSource = RSTArrayTableViewDataSource<Box<Error>>(items: errors.map(Box.init))
            dataSource.cellConfigurationHandler = { (cell, error, indexPath) in
                let cell = cell as! SyncResultTableViewCell
                
                let title: String?
                let errorMessage: String?
                
                switch error.value
                {
                case let error as RecordError:
                    title = error.record.localizedTitle
                    
                    switch error
                    {
                    case .filesFailed(_, let errors):
                        
                        let isRestricted = errors.contains(where: { (error) -> Bool in
                            switch error
                            {
                            case .restricted: return true
                            default: return false
                            }
                        })
                        
                        if isRestricted
                        {
                            errorMessage = NSLocalizedString("Dropbox has flagged this game as copyrighted material, so it cannot be downloaded. Please re-import this game to continue syncing.", comment: "")
                        }
                        else
                        {
                            var messages = [String]()
                            
                            for error in errors
                            {
                                messages.append(error.localizedDescription)
                            }
                            
                            errorMessage = messages.joined(separator: "\n")
                        }
                        
                    case .other(_, let error as NSError): errorMessage = error.localizedFailureReason ?? error.localizedDescription
                    default: errorMessage = error.failureReason
                    }
                    
                case let error as HarmonyError:
                    title = error.failureDescription
                    errorMessage = error.failureReason
                    
                case let error:
                    assertionFailure("Only HarmonyErrors should be thrown by syncing logic.")
                    
                    title = nil
                    errorMessage = error.localizedDescription
                }
                
                cell.nameLabel.text = title
                cell.errorLabel.text = errorMessage
            }
            
            return dataSource
        }
        
        let placeholderView = RSTPlaceholderView()
        placeholderView.textLabel.text = NSLocalizedString("Sync Successful", comment: "")
        placeholderView.detailTextLabel.text = NSLocalizedString("There were no errors during last sync.", comment: "")
        
        let compositeDataSource = RSTCompositeTableViewDataSource(dataSources: dataSources)
        compositeDataSource.proxy = self
        compositeDataSource.placeholderView = placeholderView
        return compositeDataSource
    }
    
    private func processResults() -> [(group: Group, errors: [Error])]
    {
        var errors = [Error]()
        
        switch self.result!
        {
        case .success: break
        case .failure(.partial(let recordResults)):
            for (_, result) in recordResults
            {
                guard case .failure(let error) = result else { continue }
                errors.append(error)
            }
        
        case .failure(.other(GeneralError.cancelled)):
            // Do nothing
            break
            
        case .failure(let error):
            let error = error.underlyingError ?? error
            errors.append(error)
        }
        
        var errorsByGroup = [Group: [Error]]()
        
        for error in errors
        {
            let group: Group
            
            switch error
            {
            case let error as RecordError:
                guard let recordType = SyncManager.RecordType(rawValue: error.record.recordID.type) else { continue }
                
                let metadata = error.record.localMetadata ?? error.record.remoteMetadata
                
                switch recordType
                {
                case .game: group = .game(error.record.recordID)
                case .gameCollection: group = .gameCollection
                case .controllerSkin: group = .controllerSkin
                case .gameControllerInputMapping: group = .gameControllerInputMapping
                    
                case .gameSave:
                    guard let gameID = metadata?[.gameID] else { continue }
                    
                    let recordID = RecordID(type: SyncManager.RecordType.game.rawValue, identifier: gameID)
                    group = .game(recordID)
                    
                case .saveState:
                    guard let gameID = metadata?[.gameID] else { continue }
                    
                    let recordID = RecordID(type: SyncManager.RecordType.game.rawValue, identifier: gameID)
                    group = .saveState(gameID: recordID)
                    
                case .cheat:
                    guard let gameID = metadata?[.gameID] else { continue }
                    
                    let recordID = RecordID(type: SyncManager.RecordType.game.rawValue, identifier: gameID)
                    group = .cheat(gameID: recordID)
                }
                
            default: group = .other
            }
            
            errorsByGroup[group, default: []].append(error)
        }
        
        for (group, errors) in errorsByGroup
        {
            let sortedErrors = errors.sorted { (a, b) -> Bool in
                guard let a = a as? RecordError, let b = b as? RecordError else { return false }
                return a.record.localizedTitle < b.record.localizedTitle
            }
            
            errorsByGroup[group] = sortedErrors
        }
        
        let sortedErrors = errorsByGroup.sorted { (a, b) in
            let groupA = a.key
            let groupB = b.key
            
            // Sort initially by game, then sort by type.
            // This way games and their associated records (such as save states) are visually grouped together.
            switch (groupA, groupB)
            {
            // Game-related records, but different game identifiers, so sort by game identifiers (implicitly grouping related game records together).
            // Using `fallthrough` for these cases seg faults the compiler (as of Swift 4.2.1), so we just duplicate the return expression.
            case (.game(let a), .game(let b)) where a != b: return a.identifier < b.identifier
            case (.game(let a), .saveState(let b)) where a != b: return a.identifier < b.identifier
            case (.game(let a), .cheat(let b)) where a != b: return a.identifier < b.identifier
            case (.saveState(let a), .game(let b)) where a != b: return a.identifier < b.identifier
            case (.saveState(let a), .saveState(let b)) where a != b: return a.identifier < b.identifier
            case (.saveState(let a), .cheat(let b)) where a != b: return a.identifier < b.identifier
            case (.cheat(let a), .game(let b)) where a != b: return a.identifier < b.identifier
            case (.cheat(let a), .saveState(let b)) where a != b: return a.identifier < b.identifier
            case (.cheat(let a), .cheat(let b)) where a != b: return a.identifier < b.identifier
                
            // Otherwise, just return their relative ordering.
            case (.game, _): fallthrough
            case (.saveState, _): fallthrough
            case (.cheat, _): fallthrough
            case (.controllerSkin, _): fallthrough
            case (.gameControllerInputMapping, _): fallthrough
            case (.gameCollection, _): fallthrough
            case (.other, _): return groupA.sortIndex < groupB.sortIndex
            }
        }
        
        return sortedErrors.map { (group: $0.key, errors: $0.value) }
    }
    
    func fetchGameNames() -> [RecordID: String]
    {
        let fetchRequest = Game.fetchRequest() as NSFetchRequest<Game>
        fetchRequest.propertiesToFetch = [#keyPath(Game.name), #keyPath(Game.identifier)]
        
        do
        {
            let games = try DatabaseManager.shared.viewContext.fetch(fetchRequest)
            
            let gameNames = Dictionary(uniqueKeysWithValues: games.map { (RecordID(type: SyncManager.RecordType.game.rawValue, identifier: $0.identifier), $0.name) })
            return gameNames
        }
        catch
        {
            print("Failed to fetch game names.", error)
            
            return [:]
        }
    }
}

private extension SyncResultViewController
{
    @IBAction func dismiss()
    {
        self.presentingViewController?.dismiss(animated: true, completion: nil)
    }
}

extension SyncResultViewController
{
    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
    {
        let section = self.sortedErrors[section]
        
        switch section.group
        {
        case .controllerSkin: return NSLocalizedString("Controller Skins", comment: "")
        case .gameCollection: return NSLocalizedString("Game Collections", comment: "")
        case .gameControllerInputMapping: return NSLocalizedString("Input Mappings", comment: "")
        case .other: return NSLocalizedString("Misc.", comment: "")
            
        case .game:
            guard let error = section.errors.first as? RecordError else { return nil }
            return error.record.localizedName
            
        case .saveState(let gameID):
            guard let error = section.errors.first as? RecordError else { return nil }
            
            if let gameName = self.gameNamesByRecordID[gameID] ?? error.record.localMetadata?[.gameName] ?? error.record.remoteMetadata?[.gameName]
            {
                return gameName + " - " + NSLocalizedString("Save States", comment: "")
            }
            else
            {
                return NSLocalizedString("Save States", comment: "")
            }
            
        case .cheat(let gameID):
            guard let error = section.errors.first as? RecordError else { return nil }
            
            if let gameName = self.gameNamesByRecordID[gameID] ?? error.record.localMetadata?[.gameName] ?? error.record.remoteMetadata?[.gameName]
            {
                return gameName + " - " + NSLocalizedString("Cheats", comment: "")
            }
            else
            {
                return NSLocalizedString("Cheats", comment: "")
            }
        }
    }
    
    override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath?
    {
        let section = self.sortedErrors[indexPath.section]
        
        switch section.group
        {
        case .other: return nil
        default: return indexPath
        }
    }
}
