/*
 * Copyright (C) 2006, 2007, 2008, 2009, 2014 Apple Inc. All rights reserved.
 * Copyright (C) 2007 Justin Haygood (jhaygood@reaktix.com)
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#pragma once

#include <WebCore/NativeImage.h>
#include <WebCore/SQLiteDatabase.h>
#include <wtf/Condition.h>
#include <wtf/HashCountedSet.h>
#include <wtf/HashMap.h>
#include <wtf/HashSet.h>
#include <wtf/RunLoop.h>
#include <wtf/glib/RunLoopSourcePriority.h>
#include <wtf/text/StringHash.h>
#include <wtf/text/WTFString.h>

namespace WebCore {
class SharedBuffer;
}

namespace WebKit {

class IconDatabaseClient {
public:
    virtual ~IconDatabaseClient() = default;

    virtual void didImportIconURLForPageURL(const String&) { }
    virtual void didImportIconDataForPageURL(const String&) { }
    virtual void didChangeIconForPageURL(const String&) { }
    virtual void didFinishURLImport() { }
};

class IconDatabase {
    WTF_MAKE_FAST_ALLOCATED;

private:
    class IconSnapshot {
    public:
        IconSnapshot() = default;

        IconSnapshot(const String& iconURL, int timestamp, WebCore::SharedBuffer* data)
            : m_iconURL(iconURL)
            , m_timestamp(timestamp)
            , m_data(data)
        { }

        const String& iconURL() const { return m_iconURL; }
        int timestamp() const { return m_timestamp; }
        WebCore::SharedBuffer* data() const { return m_data.get(); }

    private:
        String m_iconURL;
        int m_timestamp { 0 };
        RefPtr<WebCore::SharedBuffer> m_data;
    };

    class IconRecord : public RefCounted<IconRecord> {
    public:
        static Ref<IconRecord> create(const String& url)
        {
            return adoptRef(*new IconRecord(url));
        }
        ~IconRecord();

        time_t getTimestamp() { return m_stamp; }
        void setTimestamp(time_t stamp) { m_stamp = stamp; }

        void setImageData(RefPtr<WebCore::SharedBuffer>&&);
        WebCore::NativeImagePtr image(const WebCore::IntSize&);

        String iconURL() { return m_iconURL; }

        enum class ImageDataStatus { Present, Missing, Unknown };
        ImageDataStatus imageDataStatus();

        HashSet<String>& retainingPageURLs() { return m_retainingPageURLs; }

        IconSnapshot snapshot(bool forDeletion = false) const;

    private:
        IconRecord(const String& url);

        String m_iconURL;
        time_t m_stamp { 0 };
        RefPtr<WebCore::SharedBuffer> m_imageData;
        WebCore::NativeImagePtr m_image;

        HashSet<String> m_retainingPageURLs;

        // This allows us to cache whether or not a SiteIcon has had its data set yet
        // This helps the IconDatabase know if it has to set the data on a new object or not,
        // and also to determine if the icon is missing data or if it just hasn't been brought
        // in from the DB yet
        bool m_dataSet { false };
    };

    class PageURLSnapshot {
    public:
        PageURLSnapshot() = default;

        PageURLSnapshot(const String& pageURL, const String& iconURL)
            : m_pageURL(pageURL)
            , m_iconURL(iconURL)
        { }

        const String& pageURL() const { return m_pageURL; }
        const String& iconURL() const { return m_iconURL; }

    private:
        String m_pageURL;
        String m_iconURL;
    };

    class PageURLRecord {
        WTF_MAKE_NONCOPYABLE(PageURLRecord); WTF_MAKE_FAST_ALLOCATED;
    public:
        PageURLRecord(const String& pageURL);
        ~PageURLRecord();

        inline String url() const { return m_pageURL; }

        void setIconRecord(RefPtr<IconRecord>&&);
        IconRecord* iconRecord() { return m_iconRecord.get(); }

        PageURLSnapshot snapshot(bool forDeletion = false) const;

        // Returns false if the page wasn't retained beforehand, true if the retain count was already 1 or higher.
        bool retain(int count)
        {
            bool wasRetained = m_retainCount > 0;
            m_retainCount += count;
            return wasRetained;
        }

        // Returns true if the page is still retained after the call. False if the retain count just dropped to 0.
        bool release(int count)
        {
            ASSERT(m_retainCount >= count);
            m_retainCount -= count;
            return m_retainCount > 0;
        }

        int retainCount() const { return m_retainCount; }

    private:
        String m_pageURL;
        RefPtr<IconRecord> m_iconRecord;
        int m_retainCount { 0 };
    };

    class MainThreadNotifier {
    public:
        MainThreadNotifier()
            : m_timer(RunLoop::main(), this, &MainThreadNotifier::timerFired)
        {
            m_timer.setPriority(RunLoopSourcePriority::MainThreadDispatcherTimer);
        }

        void setActive(bool active)
        {
            m_isActive.store(active);
        }

        void notify(Function<void()>&& notification)
        {
            if (!m_isActive.load())
                return;

            {
                LockHolder locker(m_notificationQueueLock);
                m_notificationQueue.append(WTFMove(notification));
            }

            if (!m_timer.isActive())
                m_timer.startOneShot(0_s);
        }

        void stop()
        {
            setActive(false);
            m_timer.stop();
            LockHolder locker(m_notificationQueueLock);
            m_notificationQueue.clear();
        }

    private:
        void timerFired()
        {
            Deque<Function<void()>> notificationQueue;
            {
                LockHolder locker(m_notificationQueueLock);
                notificationQueue = WTFMove(m_notificationQueue);
            }

            if (!m_isActive.load())
                return;

            while (!notificationQueue.isEmpty()) {
                auto function = notificationQueue.takeFirst();
                function();
            }
        }

        Deque<Function<void()>> m_notificationQueue;
        Lock m_notificationQueueLock;
        Atomic<bool> m_isActive;
        RunLoop::Timer<MainThreadNotifier> m_timer;
    };

// *** Main Thread Only ***
public:
    IconDatabase();
    ~IconDatabase();

    enum class IconLoadDecision { Yes, No, Unknown };

    void setClient(std::unique_ptr<IconDatabaseClient>&&);

    bool open(const String& directory, const String& filename);
    void close();

    void removeAllIcons();

    void retainIconForPageURL(const String&);
    void releaseIconForPageURL(const String&);
    void setIconDataForIconURL(RefPtr<WebCore::SharedBuffer>&&, const String& iconURL);
    void setIconURLForPageURL(const String& iconURL, const String& pageURL);

    enum class IsKnownIcon { No, Yes };
    std::pair<WebCore::NativeImagePtr, IsKnownIcon> synchronousIconForPageURL(const String&, const WebCore::IntSize&);
    String synchronousIconURLForPageURL(const String&);
    bool synchronousIconDataKnownForIconURL(const String&);
    IconLoadDecision synchronousLoadDecisionForIconURL(const String&);

    void setEnabled(bool);
    bool isEnabled() const;

    void setPrivateBrowsingEnabled(bool flag);
    bool isPrivateBrowsingEnabled() const;

    static void delayDatabaseCleanup();
    static void allowDatabaseCleanup();
    static void checkIntegrityBeforeOpening();

private:
    void wakeSyncThread();
    void scheduleOrDeferSyncTimer();
    void syncTimerFired();

    RunLoop::Timer<IconDatabase> m_syncTimer;
    RefPtr<Thread> m_syncThread;
    bool m_syncThreadRunning { false };
    bool m_scheduleOrDeferSyncTimerRequested { false };

// *** Any Thread ***
public:
    bool isOpen() const;
    String databasePath() const;
    static String defaultDatabaseFilename();

private:
    Ref<IconRecord> getOrCreateIconRecord(const String& iconURL);
    PageURLRecord* getOrCreatePageURLRecord(const String& pageURL);

    bool m_isEnabled { false };
    bool m_privateBrowsingEnabled { false };

    mutable Lock m_syncLock;
    Condition m_syncCondition;
    String m_databaseDirectory;
    // Holding m_syncLock is required when accessing m_completeDatabasePath
    String m_completeDatabasePath;

    bool m_threadTerminationRequested { false };
    bool m_removeIconsRequested { false };
    bool m_iconURLImportComplete { false };
    bool m_syncThreadHasWorkToDo { false };

    Lock m_urlAndIconLock;
    // Holding m_urlAndIconLock is required when accessing any of the following data structures or the objects they contain
    HashMap<String, IconRecord*> m_iconURLToRecordMap;
    HashMap<String, PageURLRecord*> m_pageURLToRecordMap;
    HashSet<String> m_retainedPageURLs;

    Lock m_pendingSyncLock;
    // Holding m_pendingSyncLock is required when accessing any of the following data structures
    HashMap<String, PageURLSnapshot> m_pageURLsPendingSync;
    HashMap<String, IconSnapshot> m_iconsPendingSync;

    Lock m_pendingReadingLock;
    // Holding m_pendingSyncLock is required when accessing any of the following data structures - when dealing with IconRecord*s, holding m_urlAndIconLock is also required
    HashSet<String> m_pageURLsPendingImport;
    HashSet<String> m_pageURLsInterestedInIcons;
    HashSet<IconRecord*> m_iconsPendingReading;

    Lock m_urlsToRetainOrReleaseLock;
    // Holding m_urlsToRetainOrReleaseLock is required when accessing any of the following data structures.
    HashCountedSet<String> m_urlsToRetain;
    HashCountedSet<String> m_urlsToRelease;
    bool m_retainOrReleaseIconRequested { false };

// *** Sync Thread Only ***
public:
    bool shouldStopThreadActivity() const;

private:
    void iconDatabaseSyncThread();

    // The following block of methods are called exclusively by the sync thread to manage i/o to and from the database
    // Each method should periodically monitor m_threadTerminationRequested when it makes sense to return early on shutdown
    void performOpenInitialization();
    bool checkIntegrity();
    void performURLImport();
    void syncThreadMainLoop();
    bool readFromDatabase();
    bool writeToDatabase();
    void pruneUnretainedIcons();
    void checkForDanglingPageURLs(bool pruneIfFound);
    void removeAllIconsOnThread();
    void deleteAllPreparedStatements();
    void* cleanupSyncThread();
    void performRetainIconForPageURL(const String&, int retainCount);
    void performReleaseIconForPageURL(const String&, int releaseCount);

    bool m_initialPruningComplete { false };

    void setIconURLForPageURLInSQLDatabase(const String&, const String&);
    void setIconIDForPageURLInSQLDatabase(int64_t, const String&);
    void removePageURLFromSQLDatabase(const String& pageURL);
    int64_t getIconIDForIconURLFromSQLDatabase(const String& iconURL);
    int64_t addIconURLToSQLDatabase(const String&);
    RefPtr<WebCore::SharedBuffer> getImageDataForIconURLFromSQLDatabase(const String& iconURL);
    void removeIconFromSQLDatabase(const String& iconURL);
    void writeIconSnapshotToSQLDatabase(const IconSnapshot&);

    void performPendingRetainAndReleaseOperations();

    // Methods to dispatch client callbacks on the main thread
    void dispatchDidImportIconURLForPageURLOnMainThread(const String&);
    void dispatchDidImportIconDataForPageURLOnMainThread(const String&);
    void dispatchDidFinishURLImportOnMainThread();

    // The client is set by the main thread before the thread starts, and from then on is only used by the sync thread
    std::unique_ptr<IconDatabaseClient> m_client;

    WebCore::SQLiteDatabase m_syncDB;

    std::unique_ptr<WebCore::SQLiteStatement> m_setIconIDForPageURLStatement;
    std::unique_ptr<WebCore::SQLiteStatement> m_removePageURLStatement;
    std::unique_ptr<WebCore::SQLiteStatement> m_getIconIDForIconURLStatement;
    std::unique_ptr<WebCore::SQLiteStatement> m_getImageDataForIconURLStatement;
    std::unique_ptr<WebCore::SQLiteStatement> m_addIconToIconInfoStatement;
    std::unique_ptr<WebCore::SQLiteStatement> m_addIconToIconDataStatement;
    std::unique_ptr<WebCore::SQLiteStatement> m_getImageDataStatement;
    std::unique_ptr<WebCore::SQLiteStatement> m_deletePageURLsForIconURLStatement;
    std::unique_ptr<WebCore::SQLiteStatement> m_deleteIconFromIconInfoStatement;
    std::unique_ptr<WebCore::SQLiteStatement> m_deleteIconFromIconDataStatement;
    std::unique_ptr<WebCore::SQLiteStatement> m_updateIconInfoStatement;
    std::unique_ptr<WebCore::SQLiteStatement> m_updateIconDataStatement;
    std::unique_ptr<WebCore::SQLiteStatement> m_setIconInfoStatement;
    std::unique_ptr<WebCore::SQLiteStatement> m_setIconDataStatement;

    MainThreadNotifier m_mainThreadNotifier;
};

} // namespace WebKit
