/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import Foundation
#if canImport(MozillaRustComponents)
    import MozillaRustComponents
#endif

/// Snarfed from firefox-ios, although we don't have the fake desktop root,
/// and we only have the `All` Set.
public enum BookmarkRoots {
    public static let RootGUID = "root________"
    public static let MobileFolderGUID = "mobile______"
    public static let MenuFolderGUID = "menu________"
    public static let ToolbarFolderGUID = "toolbar_____"
    public static let UnfiledFolderGUID = "unfiled_____"

    public static let All = Set<String>([
        BookmarkRoots.RootGUID,
        BookmarkRoots.MobileFolderGUID,
        BookmarkRoots.MenuFolderGUID,
        BookmarkRoots.ToolbarFolderGUID,
        BookmarkRoots.UnfiledFolderGUID,
    ])
}

// Keeping `BookmarkNodeType` in the swift wrapper because the iOS code relies on the raw value of the variants of
// this enum.
public enum BookmarkNodeType: Int32 {
    // Note: these values need to match the Rust BookmarkType
    // enum in types.rs
    case bookmark = 1
    case folder = 2
    case separator = 3
    // The other node types are either queries (which we handle as
    // normal bookmarks), or have been removed from desktop, and
    // are not supported
}

/**
 * A base class containing the set of fields common to all nodes
 * in the bookmark tree.
 */
public class BookmarkNodeData {
    /**
     * The type of this bookmark.
     */
    public let type: BookmarkNodeType

    /**
     * The guid of this record. Bookmark guids are always 12 characters in the url-safe
     * base64 character set.
     */
    public let guid: String

    /**
     * Creation time, in milliseconds since the unix epoch.
     *
     * May not be a local timestamp.
     */
    public let dateAdded: Int64

    /**
     * Last modification time, in milliseconds since the unix epoch.
     */
    public let lastModified: Int64

    /**
     * The guid of this record's parent, or null if the record is the bookmark root.
     */
    public let parentGUID: String?

    /**
     * The (0-based) position of this record within it's parent.
     */
    public let position: UInt32
    // We use this from tests.
    // swiftformat:disable redundantFileprivate
    fileprivate init(type: BookmarkNodeType,
                     guid: String,
                     dateAdded: Int64,
                     lastModified: Int64,
                     parentGUID: String?,
                     position: UInt32)
    {
        self.type = type
        self.guid = guid
        self.dateAdded = dateAdded
        self.lastModified = lastModified
        self.parentGUID = parentGUID
        self.position = position
    }

    // swiftformat:enable redundantFileprivate
    /**
     * Returns true if this record is a bookmark root.
     *
     * - Note: This is determined entirely by inspecting the GUID.
     */
    public var isRoot: Bool {
        return BookmarkRoots.All.contains(guid)
    }
}

public extension BookmarkItem {
    var asBookmarkNodeData: BookmarkNodeData {
        switch self {
        case let .separator(s):
            return BookmarkSeparatorData(guid: s.guid,
                                         dateAdded: s.dateAdded,
                                         lastModified: s.lastModified,
                                         parentGUID: s.parentGuid,
                                         position: s.position)
        case let .bookmark(b):
            return BookmarkItemData(guid: b.guid,
                                    dateAdded: b.dateAdded,
                                    lastModified: b.lastModified,
                                    parentGUID: b.parentGuid,
                                    position: b.position,
                                    url: b.url,
                                    title: b.title ?? "")
        case let .folder(f):
            return BookmarkFolderData(guid: f.guid,
                                      dateAdded: f.dateAdded,
                                      lastModified: f.lastModified,
                                      parentGUID: f.parentGuid,
                                      position: f.position,
                                      title: f.title ?? "",
                                      childGUIDs: f.childGuids ?? [String](),
                                      children: f.childNodes?.map { child in child.asBookmarkNodeData })
        }
    }
}

// XXX - This function exists to convert the return types of the `bookmarksGetAllWithUrl`,
// `bookmarksSearch`, and `bookmarksGetRecent` functions which will always return the `BookmarkData`
// variant of the `BookmarkItem` enum. This function should be removed once the return types of the
// backing rust functions have been converted from `BookmarkItem`.
func toBookmarkItemDataList(items: [BookmarkItem]) -> [BookmarkItemData] {
    func asBookmarkItemData(item: BookmarkItem) -> BookmarkItemData? {
        if case let .bookmark(b) = item {
            return BookmarkItemData(guid: b.guid,
                                    dateAdded: b.dateAdded,
                                    lastModified: b.lastModified,
                                    parentGUID: b.parentGuid,
                                    position: b.position,
                                    url: b.url,
                                    title: b.title ?? "")
        }
        return nil
    }

    return items.map { asBookmarkItemData(item: $0)! }
}

/**
 * A bookmark which is a separator.
 *
 * It's type is always `BookmarkNodeType.separator`, and it has no fields
 * besides those defined by `BookmarkNodeData`.
 */
public class BookmarkSeparatorData: BookmarkNodeData {
    public init(guid: String, dateAdded: Int64, lastModified: Int64, parentGUID: String?, position: UInt32) {
        super.init(
            type: .separator,
            guid: guid,
            dateAdded: dateAdded,
            lastModified: lastModified,
            parentGUID: parentGUID,
            position: position
        )
    }
}

/**
 * A bookmark tree node that actually represents a bookmark.
 *
 * It's type is always `BookmarkNodeType.bookmark`,  and in addition to the
 * fields provided by `BookmarkNodeData`, it has a `title` and a `url`.
 */
public class BookmarkItemData: BookmarkNodeData {
    /**
     * The URL of this bookmark.
     */
    public let url: String

    /**
     * The title of the bookmark.
     *
     * Note that the bookmark storage layer treats NULL and the
     * empty string as equivalent in titles.
     */
    public let title: String

    public init(guid: String,
                dateAdded: Int64,
                lastModified: Int64,
                parentGUID: String?,
                position: UInt32,
                url: String,
                title: String)
    {
        self.url = url
        self.title = title
        super.init(
            type: .bookmark,
            guid: guid,
            dateAdded: dateAdded,
            lastModified: lastModified,
            parentGUID: parentGUID,
            position: position
        )
    }
}

/**
 * A bookmark which is a folder.
 *
 * It's type is always `BookmarkNodeType.folder`, and in addition to the
 * fields provided by `BookmarkNodeData`, it has a `title`, a list of `childGUIDs`,
 * and possibly a list of `children`.
 */
public class BookmarkFolderData: BookmarkNodeData {
    /**
     * The title of this bookmark folder.
     *
     * Note that the bookmark storage layer treats NULL and the
     * empty string as equivalent in titles.
     */
    public let title: String

    /**
     * The GUIDs of this folder's list of children.
     */
    public let childGUIDs: [String]

    /**
     * If this node was returned from the `PlacesReadConnection.getBookmarksTree` function,
     * then this should have the list of children, otherwise it will be nil.
     *
     * Note that if `recursive = false` is passed to the `getBookmarksTree` function, and
     * this is a child (or grandchild, etc) of the directly returned node, then `children`
     * will *not* be present (as that is the point of `recursive = false`).
     */
    public let children: [BookmarkNodeData]?

    public init(guid: String,
                dateAdded: Int64,
                lastModified: Int64,
                parentGUID: String?,
                position: UInt32,
                title: String,
                childGUIDs: [String],
                children: [BookmarkNodeData]?)
    {
        self.title = title
        self.childGUIDs = childGUIDs
        self.children = children
        super.init(
            type: .folder,
            guid: guid,
            dateAdded: dateAdded,
            lastModified: lastModified,
            parentGUID: parentGUID,
            position: position
        )
    }
}
