import Cocoa

extension FileManagerViewPreferences {
    struct ListViewColumnInfo: Equatable {
        let id: FileManagerColumnID
        let isVisible: Bool
        let width: CGFloat
    }

    struct ListViewInfo: Equatable {
        let sortKey: String
        let ascending: Bool
        let columns: [ListViewColumnInfo]
    }

    struct ResolvedListViewColumn: Equatable {
        let column: FileManagerColumn
        let width: CGFloat
    }

    static let fileSystemListViewFolderTypeID = "FSFolder"
    static let listViewPreferencesResetUserInfoKey = "FileManager.ListViewPreferencesReset"

    #if DEBUG
        private static let disableListViewInfoPersistenceEnvironmentKey = "SHICHIZIP_DISABLE_LIST_VIEW_INFO_PERSISTENCE"
    #endif

    private static let listViewInfoKeyPrefix = "FileManager.ListViewInfo."
    private static let listViewInfoVersion = 1
    private static let maximumStoredColumnWidth: CGFloat = 4000

    static var isListViewInfoPersistenceDisabled: Bool {
        #if DEBUG
            return getenv(disableListViewInfoPersistenceEnvironmentKey) != nil
        #else
            return false
        #endif
    }

    static func archiveListViewFolderTypeID(formatName: String?) -> String {
        let trimmedName = formatName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        return trimmedName.isEmpty ? "7-Zip" : "7-Zip." + trimmedName
    }

    static func listViewInfo(forFolderTypeID folderTypeID: String,
                             defaults: UserDefaults = SZSharedUserDefaults.defaults) -> ListViewInfo?
    {
        guard let data = defaults.data(forKey: listViewInfoDefaultsKey(forFolderTypeID: folderTypeID)),
              let storedInfo = try? PropertyListDecoder().decode(StoredListViewInfo.self, from: data),
              storedInfo.version == listViewInfoVersion,
              let info = ListViewInfo(storedInfo: storedInfo)
        else {
            return nil
        }

        return info
    }

    static func setListViewInfo(_ info: ListViewInfo,
                                forFolderTypeID folderTypeID: String,
                                defaults: UserDefaults = SZSharedUserDefaults.defaults)
    {
        let storedInfo = StoredListViewInfo(info: info, version: listViewInfoVersion)
        guard let data = try? PropertyListEncoder().encode(storedInfo) else { return }
        defaults.set(data, forKey: listViewInfoDefaultsKey(forFolderTypeID: folderTypeID))
    }

    static func removeAllListViewInfos(defaults: UserDefaults = SZSharedUserDefaults.defaults,
                                       postsChangeNotification: Bool = true)
    {
        for key in defaults.dictionaryRepresentation().keys where key.hasPrefix(listViewInfoKeyPrefix) {
            defaults.removeObject(forKey: key)
        }

        if postsChangeNotification {
            NotificationCenter.default.post(name: .fileManagerViewPreferencesDidChange,
                                            object: nil,
                                            userInfo: [listViewPreferencesResetUserInfoKey: true])
        }
    }

    static func resolvedListViewColumns(_ columns: [FileManagerColumn],
                                        using info: ListViewInfo?) -> [ResolvedListViewColumn]
    {
        guard let info else {
            return columns
                .filter(\.defaultVisible)
                .map { ResolvedListViewColumn(column: $0, width: $0.width) }
        }

        let availableColumns = Dictionary(uniqueKeysWithValues: columns.map { ($0.id, $0) })
        var resolvedColumns: [ResolvedListViewColumn] = []
        var seenColumnIDs = Set<FileManagerColumnID>()

        for columnInfo in info.columns {
            guard let column = availableColumns[columnInfo.id],
                  !seenColumnIDs.contains(columnInfo.id)
            else {
                continue
            }

            seenColumnIDs.insert(columnInfo.id)
            guard columnInfo.isVisible || columnInfo.id == .name else { continue }

            resolvedColumns.append(ResolvedListViewColumn(column: column,
                                                          width: normalizedColumnWidth(columnInfo.width,
                                                                                       for: column)))
        }

        for column in columns where !seenColumnIDs.contains(column.id) && column.defaultVisible {
            resolvedColumns.append(ResolvedListViewColumn(column: column, width: column.width))
        }

        return resolvedColumns
    }

    static func listViewColumnInfosPreservingHiddenColumns(availableColumns: [FileManagerColumn],
                                                           visibleColumns: [ListViewColumnInfo],
                                                           previousInfo: ListViewInfo?) -> [ListViewColumnInfo]
    {
        let availableColumnsByID = Dictionary(uniqueKeysWithValues: availableColumns.map { ($0.id, $0) })
        var visibleColumnsByID: [FileManagerColumnID: ListViewColumnInfo] = [:]
        var orderedIDs: [FileManagerColumnID] = []
        var seenColumnIDs = Set<FileManagerColumnID>()

        for visibleColumn in visibleColumns where availableColumnsByID[visibleColumn.id] != nil {
            guard seenColumnIDs.insert(visibleColumn.id).inserted else { continue }
            orderedIDs.append(visibleColumn.id)
            visibleColumnsByID[visibleColumn.id] = visibleColumn
        }

        let previousColumnInfos = previousInfo?.columns ?? []
        var previousOrderedIDs: [FileManagerColumnID] = []
        var previousColumnInfosByID: [FileManagerColumnID: ListViewColumnInfo] = [:]
        seenColumnIDs.removeAll()
        for previousColumn in previousColumnInfos where availableColumnsByID[previousColumn.id] != nil {
            guard seenColumnIDs.insert(previousColumn.id).inserted else { continue }
            previousOrderedIDs.append(previousColumn.id)
            previousColumnInfosByID[previousColumn.id] = previousColumn
        }

        let visibleIDs = Set(visibleColumnsByID.keys)
        for hiddenID in previousOrderedIDs where !visibleIDs.contains(hiddenID) && !orderedIDs.contains(hiddenID) {
            let predecessors = previousOrderedIDs.prefix { $0 != hiddenID }
            let insertionIndex = predecessors
                .compactMap { orderedIDs.firstIndex(of: $0) }
                .max()
                .map { $0 + 1 } ?? 0
            orderedIDs.insert(hiddenID, at: min(insertionIndex, orderedIDs.count))
        }

        for column in availableColumns where !orderedIDs.contains(column.id) {
            orderedIDs.append(column.id)
        }

        return orderedIDs.compactMap { columnID in
            guard let column = availableColumnsByID[columnID] else { return nil }
            if let visibleColumn = visibleColumnsByID[columnID] {
                return ListViewColumnInfo(id: columnID,
                                          isVisible: true,
                                          width: normalizedColumnWidth(visibleColumn.width, for: column))
            }

            return ListViewColumnInfo(id: columnID,
                                      isVisible: columnID == .name,
                                      width: normalizedColumnWidth(previousColumnInfosByID[columnID]?.width ?? column.width,
                                                                   for: column))
        }
    }

    static func resolvedListViewSortDescriptor(using info: ListViewInfo?,
                                               columns: [FileManagerColumn]) -> NSSortDescriptor?
    {
        guard let info else {
            return columns.first(where: { $0.id == .name })?.sortDescriptorPrototype
        }

        return sortDescriptor(sortKey: info.sortKey,
                              ascending: info.ascending,
                              columns: columns)
            ?? columns.first(where: { $0.id == .name })?.sortDescriptorPrototype
    }

    static func highlightedColumnID(for sortKey: String,
                                    columns: [FileManagerColumn]) -> FileManagerColumnID?
    {
        if sortKey == "type" {
            return columns.contains(where: { $0.id == .name }) ? .name : nil
        }
        return columns.first(where: { $0.sortKey == sortKey })?.id
    }

    static func storedListViewColumnWidth(for column: FileManagerColumn,
                                          folderTypeID: String,
                                          defaults: UserDefaults = SZSharedUserDefaults.defaults) -> CGFloat
    {
        let storedWidth = listViewInfo(forFolderTypeID: folderTypeID,
                                       defaults: defaults)?
            .columns
            .first(where: { $0.id == column.id })?
            .width
        guard let storedWidth, storedWidth.isFinite, storedWidth > 0 else {
            return column.width
        }
        return max(storedWidth, column.minWidth)
    }

    static func storedListViewColumnOrderIDs(folderTypeID: String,
                                             availableColumns: [FileManagerColumn],
                                             defaults: UserDefaults = SZSharedUserDefaults.defaults) -> [FileManagerColumnID]
    {
        let availableIDs = Set(availableColumns.map(\.id))
        var orderedIDs: [FileManagerColumnID] = []
        var seenIDs = Set<FileManagerColumnID>()

        let storedColumns = listViewInfo(forFolderTypeID: folderTypeID,
                                         defaults: defaults)?.columns ?? []
        for storedColumn in storedColumns where availableIDs.contains(storedColumn.id) {
            guard seenIDs.insert(storedColumn.id).inserted else { continue }
            orderedIDs.append(storedColumn.id)
        }

        for column in availableColumns {
            guard seenIDs.insert(column.id).inserted else { continue }
            orderedIDs.append(column.id)
        }

        return orderedIDs
    }

    static func restoredListViewColumnMove(for columnID: FileManagerColumnID,
                                           currentColumnIDs: [FileManagerColumnID],
                                           folderTypeID: String,
                                           availableColumns: [FileManagerColumn],
                                           defaults: UserDefaults = SZSharedUserDefaults.defaults) -> (from: Int, to: Int)?
    {
        let orderedIDs = storedListViewColumnOrderIDs(folderTypeID: folderTypeID,
                                                      availableColumns: availableColumns,
                                                      defaults: defaults)
        guard let restoredOrderIndex = orderedIDs.firstIndex(of: columnID) else { return nil }

        let precedingColumnIDs = Set(orderedIDs.prefix(upTo: restoredOrderIndex))
        let targetIndex = currentColumnIDs.count(where: { precedingColumnIDs.contains($0) })
        guard let currentIndex = currentColumnIDs.firstIndex(of: columnID),
              targetIndex != currentIndex
        else {
            return nil
        }

        return (from: currentIndex,
                to: min(targetIndex, currentColumnIDs.count - 1))
    }

    static func sortDescriptorsByResettingUnavailableColumn(_ sortDescriptors: [NSSortDescriptor],
                                                            visibleColumnIDs: Set<FileManagerColumnID>,
                                                            availableColumns: [FileManagerColumn]) -> [NSSortDescriptor]
    {
        guard let sortKey = sortDescriptors.first?.key else {
            return sortDescriptors
        }

        let sortedColumnID = highlightedColumnID(for: sortKey,
                                                 columns: availableColumns)
        guard sortedColumnID.map({ !visibleColumnIDs.contains($0) }) ?? true else {
            return sortDescriptors
        }

        return availableColumns
            .first(where: { $0.id == .name })
            .map { [$0.sortDescriptorPrototype] } ?? []
    }

    static func listViewInfoDefaultsKey(forFolderTypeID folderTypeID: String) -> String {
        listViewInfoKeyPrefix + folderTypeID
    }

    private static func sortDescriptor(sortKey: String,
                                       ascending: Bool,
                                       columns: [FileManagerColumn]) -> NSSortDescriptor?
    {
        if sortKey == "type", columns.contains(where: { $0.id == .name }) {
            return NSSortDescriptor(key: sortKey,
                                    ascending: ascending,
                                    selector: #selector(NSString.localizedStandardCompare(_:)))
        }

        guard let column = columns.first(where: { $0.sortKey == sortKey }) else {
            return nil
        }

        if let sortSelector = column.sortSelector {
            return NSSortDescriptor(key: column.sortKey,
                                    ascending: ascending,
                                    selector: sortSelector)
        }
        return NSSortDescriptor(key: column.sortKey,
                                ascending: ascending)
    }

    private static func normalizedColumnWidth(_ width: CGFloat,
                                              for column: FileManagerColumn) -> CGFloat
    {
        guard width.isFinite, width > 0 else {
            return column.width
        }
        return min(max(width, column.minWidth), maximumStoredColumnWidth)
    }
}

private struct StoredListViewColumnInfo: Codable {
    let id: String
    let isVisible: Bool
    let width: Double
}

private struct StoredListViewInfo: Codable {
    let version: Int
    let sortKey: String
    let ascending: Bool
    let columns: [StoredListViewColumnInfo]
}

private extension StoredListViewInfo {
    init(info: FileManagerViewPreferences.ListViewInfo,
         version: Int)
    {
        self.init(version: version,
                  sortKey: info.sortKey,
                  ascending: info.ascending,
                  columns: info.columns.map {
                      StoredListViewColumnInfo(id: $0.id.rawValue,
                                               isVisible: $0.isVisible,
                                               width: Double($0.width))
                  })
    }
}

private extension FileManagerViewPreferences.ListViewInfo {
    init?(storedInfo: StoredListViewInfo) {
        let columns = storedInfo.columns.map { storedColumn in
            FileManagerViewPreferences.ListViewColumnInfo(id: FileManagerColumnID(rawValue: storedColumn.id),
                                                          isVisible: storedColumn.isVisible,
                                                          width: CGFloat(storedColumn.width))
        }

        self.init(sortKey: storedInfo.sortKey,
                  ascending: storedInfo.ascending,
                  columns: columns)
    }
}
