/* 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/. */

#include "ImageOps.h"
#include "imgIContainer.h"
#include "imgINotificationObserver.h"
#include "imgLoader.h"
#include "imgRequestProxy.h"
#include "mozilla/ArrayUtils.h"
#include "mozilla/Assertions.h"
#include "mozilla/dom/Element.h"
#include "mozilla/Preferences.h"
#include "nsAttrValue.h"
#include "nsComputedDOMStyle.h"
#include "nsContentUtils.h"
#include "nsGkAtoms.h"
#include "nsIContent.h"
#include "nsIContentPolicy.h"
#include "nsIDocument.h"
#include "nsILoadGroup.h"
#include "nsImageToPixbuf.h"
#include "nsIPresShell.h"
#include "nsIURI.h"
#include "nsNetUtil.h"
#include "nsPresContext.h"
#include "nsRect.h"
#include "nsServiceManagerUtils.h"
#include "nsString.h"
#include "nsStyleConsts.h"
#include "nsStyleContext.h"
#include "nsStyleStruct.h"
#include "nsUnicharUtils.h"

#include "nsMenuContainer.h"
#include "nsNativeMenuAtoms.h"
#include "nsNativeMenuDocListener.h"

#include <gdk/gdk.h>
#include <glib-object.h>
#include <pango/pango.h>

#include "nsMenuObject.h"

// X11's None clashes with StyleDisplay::None
#include "X11UndefineNone.h"

#undef None

using namespace mozilla;
using mozilla::image::ImageOps;

#define MAX_WIDTH 350000

const char* gPropertyStrings[] = {
#define DBUSMENU_PROPERTY(e, s, b) s,
    DBUSMENU_PROPERTIES
#undef DBUSMENU_PROPERTY
    nullptr
};

nsWeakMenuObject* nsWeakMenuObject::sHead;
PangoLayout* gPangoLayout = nullptr;

class nsMenuObjectIconLoader final : public imgINotificationObserver {
public:
    NS_DECL_ISUPPORTS
    NS_DECL_IMGINOTIFICATIONOBSERVER

    nsMenuObjectIconLoader(nsMenuObject* aOwner) : mOwner(aOwner) { };

    void LoadIcon(nsStyleContext* aStyleContext);
    void Destroy();

private:
    ~nsMenuObjectIconLoader() { };

    nsMenuObject* mOwner;
    RefPtr<imgRequestProxy> mImageRequest;
    nsCOMPtr<nsIURI> mURI;
    nsIntRect mImageRect;
};

NS_IMPL_ISUPPORTS(nsMenuObjectIconLoader, imgINotificationObserver)

NS_IMETHODIMP
nsMenuObjectIconLoader::Notify(imgIRequest* aProxy,
                               int32_t aType, const nsIntRect* aRect) {
    if (!mOwner) {
        return NS_OK;
    }

    if (aProxy != mImageRequest) {
        return NS_ERROR_FAILURE;
    }

    if (aType == imgINotificationObserver::LOAD_COMPLETE) {
        uint32_t status = imgIRequest::STATUS_ERROR;
        if (NS_FAILED(mImageRequest->GetImageStatus(&status)) ||
            (status & imgIRequest::STATUS_ERROR)) {
            mImageRequest->Cancel(NS_BINDING_ABORTED);
            mImageRequest = nullptr;
            return NS_ERROR_FAILURE;
        }

        nsCOMPtr<imgIContainer> image;
        mImageRequest->GetImage(getter_AddRefs(image));
        MOZ_ASSERT(image);

        // Ask the image to decode at its intrinsic size.
        int32_t width = 0, height = 0;
        image->GetWidth(&width);
        image->GetHeight(&height);
        image->RequestDecodeForSize(nsIntSize(width, height), imgIContainer::FLAG_NONE);
        return NS_OK;
    }

    if (aType == imgINotificationObserver::DECODE_COMPLETE) {
        mImageRequest->Cancel(NS_BINDING_ABORTED);
        mImageRequest = nullptr;
        return NS_OK;
    }

    if (aType != imgINotificationObserver::FRAME_COMPLETE) {
        return NS_OK;
    }

    nsCOMPtr<imgIContainer> img;
    mImageRequest->GetImage(getter_AddRefs(img));
    if (!img) {
        return NS_ERROR_FAILURE;
    }

    if (!mImageRect.IsEmpty()) {
        img = ImageOps::Clip(img, mImageRect);
    }

    int32_t width, height;
    img->GetWidth(&width);
    img->GetHeight(&height);

    if (width <= 0 || height <= 0) {
        mOwner->ClearIcon();
        return NS_OK;
    }

    if (width > 100 || height > 100) {
        // The icon data needs to go across DBus. Make sure the icon
        // data isn't too large, else our connection gets terminated and
        // GDbus helpfully aborts the application. Thank you :)
        NS_WARNING("Icon data too large");
        mOwner->ClearIcon();
        return NS_OK;
    }

    GdkPixbuf* pixbuf = nsImageToPixbuf::ImageToPixbuf(img);
    if (pixbuf) {
        dbusmenu_menuitem_property_set_image(mOwner->GetNativeData(),
                                             DBUSMENU_MENUITEM_PROP_ICON_DATA,
                                             pixbuf);
        g_object_unref(pixbuf);
    }

    return NS_OK;
}

void
nsMenuObjectIconLoader::LoadIcon(nsStyleContext* aStyleContext) {
    nsIDocument* doc = mOwner->ContentNode()->OwnerDoc();

    nsCOMPtr<nsIURI> uri;
    nsIntRect imageRect;
    imgRequestProxy* imageRequest = nullptr;

    nsAutoString uriString;
    if (mOwner->ContentNode()->GetAttr(kNameSpaceID_None, nsGkAtoms::image,
                                       uriString)) {
        NS_NewURI(getter_AddRefs(uri), uriString);
    } else {
        nsIPresShell* shell = doc->GetShell();
        if (!shell) {
            return;
        }

        nsPresContext* pc = shell->GetPresContext();
        if (!pc || !aStyleContext) {
            return;
        }

        const nsStyleList* list = aStyleContext->StyleList();
        imageRequest = list->GetListStyleImage();
        if (imageRequest) {
            imageRequest->GetURI(getter_AddRefs(uri));
            imageRect = list->mImageRegion.ToNearestPixels(
                            pc->AppUnitsPerDevPixel());
        }
    }

    if (!uri) {
        mOwner->ClearIcon();
        mURI = nullptr;

        if (mImageRequest) {
            mImageRequest->Cancel(NS_BINDING_ABORTED);
            mImageRequest = nullptr;
        }

        return;
    }

    bool same;
    if (mURI && NS_SUCCEEDED(mURI->Equals(uri, &same)) && same && 
        (!imageRequest || imageRect == mImageRect)) {
        return;
    }

    if (mImageRequest) {
        mImageRequest->Cancel(NS_BINDING_ABORTED);
        mImageRequest = nullptr;
    }

    mURI = uri;

    if (imageRequest) {
        mImageRect = imageRect;
        imageRequest->Clone(this, getter_AddRefs(mImageRequest));
    } else {
        mImageRect.SetEmpty();
        nsCOMPtr<nsILoadGroup> loadGroup = doc->GetDocumentLoadGroup();
        RefPtr<imgLoader> loader =
            nsContentUtils::GetImgLoaderForDocument(doc);
        if (!loader || !loadGroup) {
            NS_WARNING("Failed to get loader or load group for image load");
            return;
        }

        loader->LoadImage(uri, nullptr, nullptr, mozilla::net::RP_Unset,
                          nullptr, loadGroup, this, nullptr, nullptr,
                          nsIRequest::LOAD_NORMAL, nullptr,
                          nsIContentPolicy::TYPE_IMAGE, EmptyString(),
                          getter_AddRefs(mImageRequest));
    }
}

void
nsMenuObjectIconLoader::Destroy() {
    if (mImageRequest) {
        mImageRequest->CancelAndForgetObserver(NS_BINDING_ABORTED);
        mImageRequest = nullptr;
    }

    mOwner = nullptr;
}

static int
CalculateTextWidth(const nsAString& aText) {
    if (!gPangoLayout) {
        PangoFontMap* fontmap = pango_cairo_font_map_get_default();
        PangoContext* ctx = pango_font_map_create_context(fontmap);
        gPangoLayout = pango_layout_new(ctx);
        g_object_unref(ctx);
    }

    pango_layout_set_text(gPangoLayout, NS_ConvertUTF16toUTF8(aText).get(), -1);

    int width, dummy;
    pango_layout_get_size(gPangoLayout, &width, &dummy);

    return width;
}

static const nsDependentString
GetEllipsis() {
    static char16_t sBuf[4] = { 0, 0, 0, 0 };
    if (!sBuf[0]) {
        nsAdoptingString ellipsis = Preferences::GetLocalizedString("intl.ellipsis");
        if (!ellipsis.IsEmpty()) {
            uint32_t l = ellipsis.Length();
            const nsAdoptingString::char_type* c = ellipsis.BeginReading();
            uint32_t i = 0;
            while (i < 3 && i < l) {
                sBuf[i++] =* (c++);
            }
        } else {
            sBuf[0] = '.';
            sBuf[1] = '.';
            sBuf[2] = '.';
        }
    }

    return nsDependentString(sBuf);
}

static int
GetEllipsisWidth() {
    static int sEllipsisWidth = -1;

    if (sEllipsisWidth == -1) {
        sEllipsisWidth = CalculateTextWidth(GetEllipsis());
    }

    return sEllipsisWidth;
}

nsMenuObject::nsMenuObject(nsMenuContainer* aParent, nsIContent* aContent) :
    mContent(aContent),
    mListener(aParent->DocListener()),
    mParent(aParent),
    mNativeData(nullptr) {
    MOZ_ASSERT(mContent);
    MOZ_ASSERT(mListener);
    MOZ_ASSERT(mParent);
}

nsMenuObject::nsMenuObject(nsNativeMenuDocListener* aListener,
                           nsIContent* aContent) :
    mContent(aContent),
    mListener(aListener),
    mParent(nullptr),
    mNativeData(nullptr) {
    MOZ_ASSERT(mContent);
    MOZ_ASSERT(mListener);
}

void
nsMenuObject::UpdateLabel() {
    // Goanna stores the label and access key in separate attributes
    // so we need to convert label="Foo_Bar"/accesskey="F" in to
    // label="_Foo__Bar" for dbusmenu

    nsAutoString label;
    mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, label);

    nsAutoString accesskey;
    mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey, accesskey);

    const nsAutoString::char_type* akey = accesskey.BeginReading();
    char16_t keyLower = ToLowerCase(*akey);
    char16_t keyUpper = ToUpperCase(*akey);

    const nsAutoString::char_type* iter = label.BeginReading();
    const nsAutoString::char_type* end = label.EndReading();
    uint32_t length = label.Length();
    uint32_t pos = 0;
    bool foundAccessKey = false;

    while (iter != end) {
        if (*iter != char16_t('_')) {
            if ((*iter != keyLower &&* iter != keyUpper) || foundAccessKey) {
                ++iter;
                ++pos;
                continue;
            }
            foundAccessKey = true;
        }

        label.SetLength(++length);

        iter = label.BeginReading() + pos;
        end = label.EndReading();
        nsAutoString::char_type* cur = label.BeginWriting() + pos;

        memmove(cur + 1, cur, (length - 1 - pos)*  sizeof(nsAutoString::char_type));
       * cur = nsAutoString::char_type('_');

        iter += 2;
        pos += 2;
    }

    if (CalculateTextWidth(label) <= MAX_WIDTH) {
        dbusmenu_menuitem_property_set(mNativeData,
                                       DBUSMENU_MENUITEM_PROP_LABEL,
                                       NS_ConvertUTF16toUTF8(label).get());
        return;
    }

    // This sucks.
    // This should be done at the point where the menu is drawn (hello Unity),
    // but unfortunately it doesn't do that and will happily fill your entire
    // screen width with a menu if you have a bookmark with a really long title.
    // This leaves us with no other option but to ellipsize here, with no proper
    // knowledge of Unity's render path, font size etc. This is better than nothing
    nsAutoString truncated;
    int target = MAX_WIDTH - GetEllipsisWidth();
    length = label.Length();

    static nsIContent::AttrValuesArray strings[] = {
        &nsGkAtoms::left, &nsGkAtoms::start,
        &nsGkAtoms::center, &nsGkAtoms::right,
        &nsGkAtoms::end, nullptr
    };

    int32_t type = mContent->FindAttrValueIn(kNameSpaceID_None,
                                             nsGkAtoms::crop,
                                             strings, eCaseMatters);

    switch (type) {
        case 0:
        case 1:
            // FIXME: Implement left cropping
        case 2:
            // FIXME: Implement center cropping
        case 3:
        case 4:
        default:
            for (uint32_t i = 0; i < length; i++) {
                truncated.Append(label.CharAt(i));
                if (CalculateTextWidth(truncated) > target) {
                    break;
                }
            }

            truncated.Append(GetEllipsis());
    }

    dbusmenu_menuitem_property_set(mNativeData,
                                   DBUSMENU_MENUITEM_PROP_LABEL,
                                   NS_ConvertUTF16toUTF8(truncated).get());
}

void
nsMenuObject::UpdateVisibility(nsStyleContext* aStyleContext) {
    bool vis = true;

    if (aStyleContext &&
        (aStyleContext->StyleDisplay()->mDisplay == StyleDisplay::None ||
         aStyleContext->StyleVisibility()->mVisible ==
            NS_STYLE_VISIBILITY_COLLAPSE)) {
        vis = false;
    }

    dbusmenu_menuitem_property_set_bool(mNativeData,
                                        DBUSMENU_MENUITEM_PROP_VISIBLE,
                                        vis);
}

void
nsMenuObject::UpdateSensitivity() {
    bool disabled = mContent->AttrValueIs(kNameSpaceID_None,
                                          nsGkAtoms::disabled,
                                          nsGkAtoms::_true, eCaseMatters);

    dbusmenu_menuitem_property_set_bool(mNativeData,
                                        DBUSMENU_MENUITEM_PROP_ENABLED,
                                        !disabled);

}

void
nsMenuObject::UpdateIcon(nsStyleContext* aStyleContext) {
    if (ShouldShowIcon()) {
        if (!mIconLoader) {
            mIconLoader = new nsMenuObjectIconLoader(this);
        }

        mIconLoader->LoadIcon(aStyleContext);
    } else {
        if (mIconLoader) {
            mIconLoader->Destroy();
            mIconLoader = nullptr;
        }

        ClearIcon();
    }
}

already_AddRefed<nsStyleContext>
nsMenuObject::GetStyleContext() {
    nsIPresShell* shell = mContent->OwnerDoc()->GetShell();
    if (!shell) {
        return nullptr;
    }

    RefPtr<nsStyleContext> sc =
        nsComputedDOMStyle::GetStyleContextForElementNoFlush(
            mContent->AsElement(), nullptr, shell);

    return sc.forget();
}

void
nsMenuObject::InitializeNativeData() {
}

nsMenuObject::PropertyFlags
nsMenuObject::SupportedProperties() const {
    return static_cast<nsMenuObject::PropertyFlags>(0);
}

bool
nsMenuObject::IsCompatibleWithNativeData(DbusmenuMenuitem* aNativeData) const {
    return true;
}

void
nsMenuObject::UpdateContentAttributes() {
}

void
nsMenuObject::Update(nsStyleContext* aStyleContext) {
}

bool
nsMenuObject::ShouldShowIcon() const {
    // Ideally we want to know the visibility of the anonymous XUL image in
    // our menuitem, but this isn't created because we don't have a frame.
    // The following works by default (because xul.css hides images in menuitems
    // that don't have the "menuitem-with-favicon" class). It's possible a third
    // party theme could override this, but, oh well...
    const nsAttrValue* classes = mContent->AsElement()->GetClasses();
    if (!classes) {
        return false;
    }

    for (uint32_t i = 0; i < classes->GetAtomCount(); ++i) {
        if (classes->AtomAt(i) == nsNativeMenuAtoms::menuitem_with_favicon) {
            return true;
        }
    }

    return false;
}

void
nsMenuObject::ClearIcon() {
    dbusmenu_menuitem_property_remove(mNativeData,
                                      DBUSMENU_MENUITEM_PROP_ICON_DATA);
}

nsMenuObject::~nsMenuObject() {
    nsWeakMenuObject::NotifyDestroyed(this);

    if (mIconLoader) {
        mIconLoader->Destroy();
    }

    if (mListener) {
        mListener->UnregisterForContentChanges(mContent);
    }

    if (mNativeData) {
        g_object_unref(mNativeData);
        mNativeData = nullptr;
    }
}

void
nsMenuObject::CreateNativeData() {
    MOZ_ASSERT(mNativeData == nullptr, "This node already has a DbusmenuMenuitem. The old one will be leaked");

    mNativeData = dbusmenu_menuitem_new();
    InitializeNativeData();
    if (mParent && mParent->IsBeingDisplayed()) {
        ContainerIsOpening();
    }

    mListener->RegisterForContentChanges(mContent, this);
}

nsresult
nsMenuObject::AdoptNativeData(DbusmenuMenuitem* aNativeData) {
    MOZ_ASSERT(mNativeData == nullptr, "This node already has a DbusmenuMenuitem. The old one will be leaked");

    if (!IsCompatibleWithNativeData(aNativeData)) {
        return NS_ERROR_FAILURE;
    }

    mNativeData = aNativeData;
    g_object_ref(mNativeData);

    PropertyFlags supported = SupportedProperties();
    PropertyFlags mask = static_cast<PropertyFlags>(1);

    for (uint32_t i = 0; gPropertyStrings[i]; ++i) {
        if (!(mask & supported)) {
            dbusmenu_menuitem_property_remove(mNativeData, gPropertyStrings[i]);
        }
        mask = static_cast<PropertyFlags>(mask << 1);
    }

    InitializeNativeData();
    if (mParent && mParent->IsBeingDisplayed()) {
        ContainerIsOpening();
    }

    mListener->RegisterForContentChanges(mContent, this);

    return NS_OK;
}

void
nsMenuObject::ContainerIsOpening() {
    MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());

    UpdateContentAttributes();

    RefPtr<nsStyleContext> sc = GetStyleContext();
    Update(sc);
}

/* static */ void
nsWeakMenuObject::AddWeakReference(nsWeakMenuObject* aWeak) {
    aWeak->mPrev = sHead;
    sHead = aWeak;
}

/* static */ void
nsWeakMenuObject::RemoveWeakReference(nsWeakMenuObject* aWeak) {
    if (aWeak == sHead) {
        sHead = aWeak->mPrev;
        return;
    }

    nsWeakMenuObject* weak = sHead;
    while (weak && weak->mPrev != aWeak) {
        weak = weak->mPrev;
    }

    if (weak) {
        weak->mPrev = aWeak->mPrev;
    }
}

/* static */ void
nsWeakMenuObject::NotifyDestroyed(nsMenuObject* aMenuObject) {
    nsWeakMenuObject* weak = sHead;
    while (weak) {
        if (weak->mMenuObject == aMenuObject) {
            weak->mMenuObject = nullptr;
        }

        weak = weak->mPrev;
    }
}