//
//  InstanceFeatures.swift
//  Tusker
//
//  Created by Shadowfacts on 1/23/22.
//  Copyright © 2022 Shadowfacts. All rights reserved.
//

import Foundation
import Combine
import Pachyderm

public class InstanceFeatures: ObservableObject {
    private static let pleromaVersionRegex = try! NSRegularExpression(pattern: "\\(compatible; (pleroma|akkoma) (.*)\\)", options: .caseInsensitive)
    
    private let _featuresUpdated = PassthroughSubject<Void, Never>()
    public var featuresUpdated: some Publisher<Void, Never> { _featuresUpdated }
    
    @Published @_spi(InstanceType) public private(set) var instanceType: InstanceType = .mastodon(.vanilla, nil)
    @Published public private(set) var maxStatusChars = 500
    @Published public private(set) var charsReservedPerURL = 23
    @Published public private(set) var maxPollOptionChars: Int?
    @Published public private(set) var maxPollOptionsCount: Int?
    @Published public private(set) var mediaAttachmentsConfiguration: Instance.MediaAttachmentsConfiguration?
    
    public var localOnlyPosts: Bool {
        switch instanceType {
        case .mastodon(.hometown(_), _), .mastodon(.glitch, _):
            return true
        case .pleroma(.akkoma(_)):
            return true
        default:
            return false
        }
    }
    
    /// Instance types that use a separate visibility to indicate local-only posts.
    public var localOnlyPostsVisibility: Bool {
        if case .pleroma(.akkoma(_)) = instanceType {
            return true
        } else {
            return false
        }
    }
    
    public var mastodonAttachmentRestrictions: Bool {
        instanceType.isMastodon
    }
    
    public var pollsAndAttachments: Bool {
        instanceType.isPleroma
    }
    
    public var boostToOriginalAudience: Bool {
        instanceType.isPleroma || instanceType.isMastodon
    }
    
    public var profilePinnedStatuses: Bool {
        switch instanceType {
        case .pixelfed:
            return false
        default:
            return true
        }
    }
    
    public var trends: Bool {
        instanceType.isMastodon
    }
    
    public var profileSuggestions: Bool {
        instanceType.isMastodon && hasMastodonVersion(3, 4, 0)
    }
    
    public var trendingStatusesAndLinks: Bool {
        instanceType.isMastodon && hasMastodonVersion(3, 5, 0)
    }
    
    public var reblogVisibility: Bool {
        (instanceType.isMastodon && hasMastodonVersion(2, 8, 0))
            || (instanceType.isPleroma && hasPleromaVersion(2, 0, 0))
    }
    
    public var probablySupportsMarkdown: Bool {
        switch instanceType {
        case .pleroma(_), .mastodon(.glitch, _), .mastodon(.hometown(_), _), .firefish(_):
            return true
        default:
            return false
        }
    }
    
    public var needsLocalOnlyEmojiHack: Bool {
        if case .mastodon(.glitch, _) = instanceType {
            return true
        } else {
            return false
        }
    }
    
    public var needsWideColorGamutHack: Bool {
        if case .mastodon(_, let version) = instanceType {
            return version < Version(4, 0, 0)
        } else {
            return true
        }
    }
    
    public var canFollowHashtags: Bool {
        if case .mastodon(_, let version) = instanceType {
            return version >= Version(4, 0, 0)
        } else if case .pleroma(.akkoma(let version)) = instanceType {
            return version >= Version(3, 4, 0)
        } else {
            return false
        }
    }
    
    public var filtersV2: Bool {
        hasMastodonVersion(4, 0, 0)
    }
    
    public var notificationsAllowedTypes: Bool {
        hasMastodonVersion(3, 5, 0)
    }
    
    public var pollVotersCount: Bool {
        instanceType.isMastodon
    }
    
    public var createStatusWithLanguage: Bool {
        instanceType.isMastodon || instanceType.isPleroma(.akkoma(nil))
    }
    
    public var editStatuses: Bool {
        switch instanceType {
        case .mastodon(_, let v) where v >= Version(3, 5, 0):
            return true
        case .pleroma(.vanilla(let v)) where v >= Version(2, 5, 0):
            return true
        case .pleroma(.akkoma(_)):
            return true
        default:
            return false
        }
    }
    
    public var statusEditNotifications: Bool {
        // pleroma doesn't seem to support 'update' type notifications, even though it supports edits
        hasMastodonVersion(3, 5, 0)
    }
    
    public var statusNotifications: Bool {
        // pleroma doesn't support notifications for new posts from an account
        hasMastodonVersion(3, 3, 0)
    }
    
    public var needsEditAttachmentsInSeparateRequest: Bool {
        instanceType.isPleroma(.akkoma(nil))
    }
    
    public var composeDirectStatuses: Bool {
        if case .pixelfed = instanceType {
            return false
        } else {
            return true
        }
    }
    
    public var searchOperators: Bool {
        hasMastodonVersion(4, 2, 0)
    }
    
    public var hasServerPreferences: Bool {
        hasMastodonVersion(2, 8, 0)
    }
    
    public var listRepliesPolicy: Bool {
        hasMastodonVersion(3, 3, 0)
    }
    
    public var exclusiveLists: Bool {
        hasMastodonVersion(4, 2, 0) || instanceType.isMastodon(.hometown(nil))
    }
    
    public init() {
    }
    
    public func update(instance: InstanceInfo, nodeInfo: NodeInfo?) {
        let ver = instance.version.lowercased()
        // check glitch first b/c it still reports "mastodon" as the software in nodeinfo
        if ver.contains("glitch") {
            instanceType = .mastodon(.glitch, Version(string: ver))
        } else if nodeInfo?.software.name == "mastodon" {
            instanceType = .mastodon(.vanilla, Version(string: ver))
        } else if nodeInfo?.software.name == "hometown" {
            var mastoVersion: Version?
            var hometownVersion: Version?
            let parts = ver.split(separator: "+")
            if parts.count == 2,
               let first = Version(string: String(parts[0])) {
                if first > Version(1, 0, 8) {
                    // like 3.5.5+hometown-1.0.9
                    mastoVersion = first
                    if parts[1].starts(with: "hometown-") {
                        hometownVersion = Version(string: String(parts[1][parts[1].index(parts[1].startIndex, offsetBy: "hometown-".count + 1)...]))
                    }
                } else {
                    // like "1.0.6+3.5.2"
                    hometownVersion = first
                    mastoVersion = Version(string: String(parts[1]))
                }
            } else {
                mastoVersion = Version(string: ver)
            }
            instanceType = .mastodon(.hometown(hometownVersion), mastoVersion)
        } else if let match = InstanceFeatures.pleromaVersionRegex.firstMatch(in: ver, range: NSRange(location: 0, length: ver.utf16.count)) {
            var pleromaVersion: Version?
            let type = (ver as NSString).substring(with: match.range(at: 1))
            pleromaVersion = Version(string: (ver as NSString).substring(with: match.range(at: 2)))
            if type == "akkoma" {
                instanceType = .pleroma(.akkoma(pleromaVersion))
            } else {
                instanceType = .pleroma(.vanilla(pleromaVersion))
            }
        } else if ver.contains("pixelfed") {
            instanceType = .pixelfed
        } else if nodeInfo?.software.name == "gotosocial" {
            instanceType = .gotosocial
        } else if ver.contains("firefish") || ver.contains("iceshrimp") || ver.contains("calckey") {
            instanceType = .firefish(nodeInfo?.software.version)
        } else {
            instanceType = .mastodon(.vanilla, Version(string: ver))
        }
        
        maxStatusChars = instance.maxStatusCharacters ?? 500
        charsReservedPerURL = instance.configuration?.statuses.charactersReservedPerURL ?? 23 // default Mastodon link length
        if let pollsConfig = instance.pollsConfiguration {
            maxPollOptionChars = pollsConfig.maxCharactersPerOption
            maxPollOptionsCount = pollsConfig.maxOptions
        }
        mediaAttachmentsConfiguration = instance.configuration?.mediaAttachments
        
        _featuresUpdated.send()
    }
    
    public func hasMastodonVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool {
        if case .mastodon(_, let version) = instanceType {
            return version >= Version(major, minor, patch)
        } else {
            return false
        }
    }
    
    func hasPleromaVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool {
        switch instanceType {
        case .pleroma(.vanilla(let version)), .pleroma(.akkoma(let version)):
            return version >= Version(major, minor, patch)
        default:
            return false
        }
    }
}

extension InstanceFeatures {
    @_spi(InstanceType) public enum InstanceType {
        case mastodon(MastodonType, Version?)
        case pleroma(PleromaType)
        case pixelfed
        case gotosocial
        case firefish(String?)
        
        var isMastodon: Bool {
            if case .mastodon(_, _) = self {
                return true
            } else {
                return false
            }
        }
        
        func isMastodon(_ subtype: MastodonType) -> Bool {
            if case .mastodon(let t, _) = self,
               t.equalsIgnoreVersion(subtype) {
                return true
            } else {
                return false
            }
        }
        
        var isPleroma: Bool {
            if case .pleroma(_) = self {
                return true
            } else {
                return false
            }
        }
        
        func isPleroma(_ subtype: PleromaType) -> Bool {
            if case .pleroma(let t) = self,
               t.equalsIgnoreVersion(subtype) {
                return true
            } else {
                return false
            }
        }
    }
    
    @_spi(InstanceType) public enum MastodonType {
        case vanilla
        case hometown(Version?)
        case glitch
        
        func equalsIgnoreVersion(_ other: MastodonType) -> Bool {
            switch (self, other) {
            case (.vanilla, .vanilla):
                return true
            case (.hometown(_), .hometown(_)):
                return true
            case (.glitch, .glitch):
                return true
            default:
                return false
            }
        }
    }
    
    @_spi(InstanceType) public enum PleromaType {
        case vanilla(Version?)
        case akkoma(Version?)
        
        func equalsIgnoreVersion(_ other: PleromaType) -> Bool {
            switch (self, other) {
            case (.vanilla(_), .vanilla(_)):
                return true
            case (.akkoma(_), .akkoma(_)):
                return true
            default:
                return false
            }
        }
    }
}
