//
//  Copyright © 2018 WireGuard LLC. All rights reserved.
//

import UIKit
import os.log
import CoreData

import BNRCoreDataStack
import NetworkExtension

protocol TunnelsTableViewControllerDelegate: class {
    func addProvider(tunnelsTableViewController: TunnelsTableViewController)
    func connect(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController)
    func disconnect(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController)
    func info(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController)
    func delete(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController)
    func status(for tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) -> NEVPNStatus
    func showSettings()
}

class TunnelsTableViewController: UITableViewController {
    weak var delegate: TunnelsTableViewControllerDelegate?

    var viewContext: NSManagedObjectContext!

    @IBOutlet var settingsButton: UIBarButtonItem!
    @IBOutlet var editButton: UIBarButtonItem!
    @IBOutlet var doneButton: UIBarButtonItem!

    private lazy var fetchedResultsController: FetchedResultsController<Tunnel> = {
        let fetchRequest = NSFetchRequest<Tunnel>()
        fetchRequest.entity = Tunnel.entity()
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "title", ascending: true)]
        let frc = FetchedResultsController<Tunnel>(fetchRequest: fetchRequest,
                                                    managedObjectContext: viewContext)
        frc.setDelegate(self.frcDelegate)
        return frc
    }()

    public func updateStatus(for tunnelIdentifier: String) {
        viewContext.perform {
            do {
                let tunnel = try Tunnel.findFirstInContext(self.viewContext, predicate: NSPredicate(format: "tunnelIdentifier == %@", tunnelIdentifier))
                if let tunnel = tunnel {
                    if let indexPath = self.fetchedResultsController.indexPathForObject(tunnel) {
                        self.tableView.reloadRows(at: [indexPath], with: .none)
                    }
                }
            } catch {
                os_log("Unable to load tunnel for tunnel identifier: %{public}@", log: Log.general, type: .error, error.localizedDescription)

            }
        }
    }

    private lazy var frcDelegate: TunnelFetchedResultsControllerDelegate = { // swiftlint:disable:this weak_delegate
        return TunnelFetchedResultsControllerDelegate(tableView: self.tableView)
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        do {
            try fetchedResultsController.performFetch()
        } catch {
            print("Failed to fetch objects: \(error)")
        }

        // Get rid of seperator lines in table.
        tableView.tableFooterView = UIView(frame: CGRect.zero)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        updateBarButtons()
    }

    @IBAction func editTunnels(_ sender: Any) {
        tableView.setEditing(!tableView.isEditing, animated: true)
        updateBarButtons()

    }

    private func updateBarButtons() {
        navigationController?.setToolbarHidden(tableView.isEditing, animated: true)
        if tableView.isEditing {
            self.navigationItem.setRightBarButtonItems([doneButton], animated: true)
        } else {
            self.navigationItem.setRightBarButtonItems([settingsButton, editButton], animated: true)
        }
    }

    @IBAction func showSettings(_ sender: Any) {
        delegate?.showSettings()
    }

    @IBAction func addProvider(_ sender: UIBarButtonItem) {
        delegate?.addProvider(tunnelsTableViewController: self)
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return fetchedResultsController.sections?[0].objects.count ?? 0
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(type: TunnelTableViewCell.self, for: indexPath)
        cell.delegate = self

        guard let sections = fetchedResultsController.sections else {
            fatalError("FetchedResultsController \(fetchedResultsController) should have sections, but found nil")
        }

        let section = sections[indexPath.section]
        let tunnel = section.objects[indexPath.row]

        cell.configure(tunnel: tunnel, status: delegate?.status(for: tunnel, tunnelsTableViewController: self) ?? .invalid)

        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let sections = fetchedResultsController.sections else {
            fatalError("FetchedResultsController \(fetchedResultsController) should have sections, but found nil")
        }

        let section = sections[indexPath.section]
        let tunnel = section.objects[indexPath.row]

        delegate?.info(tunnel: tunnel, tunnelsTableViewController: self)

        tableView.deselectRow(at: indexPath, animated: true)
    }

    override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
        guard let sections = fetchedResultsController.sections else {
            fatalError("FetchedResultsController \(fetchedResultsController) should have sections, but found nil")
        }

        let section = sections[indexPath.section]
        let tunnel = section.objects[indexPath.row]

        delegate?.info(tunnel: tunnel, tunnelsTableViewController: self)

    }

    override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }

    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {

            guard let sections = fetchedResultsController.sections else {
                fatalError("FetchedResultsController \(fetchedResultsController) should have sections, but found nil")
            }

            let section = sections[indexPath.section]
            let tunnel = section.objects[indexPath.row]

            delegate?.delete(tunnel: tunnel, tunnelsTableViewController: self)
        }
    }
}

extension TunnelsTableViewController: TunnelTableViewCellDelegate {
    func connect(tunnelIdentifier: String) {
        let tunnel = try? Tunnel.findFirstInContext(self.viewContext, predicate: NSPredicate(format: "tunnelIdentifier == %@", tunnelIdentifier))
        if let tunnel = tunnel {
            self.delegate?.connect(tunnel: tunnel!, tunnelsTableViewController: self)
        }
    }

    func disconnect(tunnelIdentifier: String) {
        let tunnel = try? Tunnel.findFirstInContext(self.viewContext, predicate: NSPredicate(format: "tunnelIdentifier == %@", tunnelIdentifier))
        if let tunnel = tunnel {
            self.delegate?.disconnect(tunnel: tunnel!, tunnelsTableViewController: self)
        }
    }

}

extension TunnelsTableViewController: Identifyable {}

class TunnelFetchedResultsControllerDelegate: NSObject, FetchedResultsControllerDelegate {

    private weak var tableView: UITableView?
    private var arrowImage: UIImageView?

    // MARK: - Lifecycle
    init(tableView: UITableView) {
        self.tableView = tableView
    }

    func fetchedResultsControllerDidPerformFetch(_ controller: FetchedResultsController<Tunnel>) {
        tableView?.reloadData()
        updateEmptyIndicator(controller)
    }

    func fetchedResultsControllerWillChangeContent(_ controller: FetchedResultsController<Tunnel>) {
        tableView?.beginUpdates()
    }

    func fetchedResultsControllerDidChangeContent(_ controller: FetchedResultsController<Tunnel>) {
        tableView?.endUpdates()
        updateEmptyIndicator(controller)
    }

    func fetchedResultsController(_ controller: FetchedResultsController<Tunnel>, didChangeObject change: FetchedResultsObjectChange<Tunnel>) {
        guard let tableView = tableView else { return }
        switch change {
        case let .insert(_, indexPath):
            tableView.insertRows(at: [indexPath], with: .automatic)

        case let .delete(_, indexPath):
            tableView.deleteRows(at: [indexPath], with: .automatic)

        case let .move(_, fromIndexPath, toIndexPath):
            tableView.moveRow(at: fromIndexPath, to: toIndexPath)

        case let .update(_, indexPath):
            tableView.reloadRows(at: [indexPath], with: .automatic)
        }
    }

    func fetchedResultsController(_ controller: FetchedResultsController<Tunnel>, didChangeSection change: FetchedResultsSectionChange<Tunnel>) {
        guard let tableView = tableView else { return }
        switch change {
        case let .insert(_, index):
            tableView.insertSections(IndexSet(integer: index), with: .automatic)

        case let .delete(_, index):
            tableView.deleteSections(IndexSet(integer: index), with: .automatic)
        }
    }

    private func updateEmptyIndicator(_ controller: FetchedResultsController<Tunnel>) {
        guard let tableView = tableView else { return }
        if controller.count > 0 {
            tableView.backgroundView = nil
            arrowImage = nil
        } else {
            if arrowImage == nil {
                let imageView = UIImageView(image: UIImage(named: "Arrow"))
                imageView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
                imageView.frame = tableView.bounds
                imageView.contentMode = .bottomRight
                tableView.backgroundView = imageView
                arrowImage = imageView
            }
        }
    }
}

protocol TunnelTableViewCellDelegate: class {
    func connect(tunnelIdentifier: String)
    func disconnect(tunnelIdentifier: String)
}

class TunnelTableViewCell: UITableViewCell {

    @IBOutlet weak var tunnelTitleLabel: UILabel!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
    @IBOutlet weak var tunnelSwitch: UISwitch!

    weak var delegate: TunnelTableViewCellDelegate?
    private var tunnelIdentifier: String?

    @IBAction func tunnelSwitchChanged(_ sender: Any) {
        tunnelSwitch.isEnabled = false
        guard let tunnelIdentifier = tunnelIdentifier else {
            return
        }

        if tunnelSwitch.isOn {
            delegate?.connect(tunnelIdentifier: tunnelIdentifier)
        } else {
            delegate?.disconnect(tunnelIdentifier: tunnelIdentifier)
        }
    }

    func configure(tunnel: Tunnel, status: NEVPNStatus) {
        self.tunnelTitleLabel?.text = tunnel.title
        tunnelIdentifier = tunnel.tunnelIdentifier

        if status == .connecting || status == .disconnecting || status == .reasserting {
            activityIndicator.startAnimating()
            tunnelSwitch.isHidden = true
        } else {
            activityIndicator.stopAnimating()
            tunnelSwitch.isHidden = false
        }

        tunnelSwitch.isOn = status == .connected
        tunnelSwitch.onTintColor = status == .invalid || status == .reasserting ? .gray : .green
        tunnelSwitch.isEnabled = true
    }

    override func setEditing(_ editing: Bool, animated: Bool) {
        super.setEditing(editing, animated: animated)
        tunnelSwitch.isHidden = editing
    }
}

extension TunnelTableViewCell: Identifyable {}
