/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * 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/.
 *
 * This file incorporates work covered by the following license notice:
 *
 *   Licensed to the Apache Software Foundation (ASF) under one or more
 *   contributor license agreements. See the NOTICE file distributed
 *   with this work for additional information regarding copyright
 *   ownership. The ASF licenses this file to you under the Apache
 *   License, Version 2.0 (the "License"); you may not use this file
 *   except in compliance with the License. You may obtain a copy of
 *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
 */

#include <sfx2/sfxbasemodel.hxx>
#include <sfx2/objsh.hxx>
#include <sfx2/namedcolor.hxx>
#include <svx/drawitem.hxx>
#include <tbxcolorupdate.hxx>
#include <svx/svxids.hrc>
#include <svx/xdef.hxx>
#include <svx/xlineit0.hxx>
#include <svx/xlndsit.hxx>

#include <utility>
#include <vcl/commandinfoprovider.hxx>
#include <vcl/gdimtf.hxx>
#include <vcl/svapp.hxx>
#include <vcl/toolbox.hxx>
#include <vcl/virdev.hxx>
#include <vcl/weld.hxx>
#include <vcl/settings.hxx>

#include <svx/strings.hrc>
#include <svx/dialmgr.hxx>
#include <unotools/viewoptions.hxx>
#include <comphelper/string.hxx>
#include <o3tl/string_view.hxx>

#include <frozen/unordered_map.h>

namespace svx
{
ToolboxButtonColorUpdaterBase::ToolboxButtonColorUpdaterBase(
    bool bWideButton, OUString aCommandLabel, OUString aCommandURL, sal_uInt16 nSlotId,
    css::uno::Reference<css::frame::XFrame> xFrame)
    : mbWideButton(bWideButton)
    , mbWasHiContrastMode(Application::GetSettings().GetStyleSettings().GetHighContrastMode())
    , mnSlotId(nSlotId)
    , maCurColor(COL_TRANSPARENT)
    , meImageType(vcl::ImageType::Size16)
    , maCommandLabel(std::move(aCommandLabel))
    , maCommandURL(std::move(aCommandURL))
    , mxFrame(std::move(xFrame))
{
}

void ToolboxButtonColorUpdaterBase::Init(sal_uInt16 nSlotId)
{
    if (mbWideButton)
    {
        Update(COL_TRANSPARENT, true);
        return;
    }

    if (rtl::Reference xModel
        = dynamic_cast<SfxBaseModel*>(mxFrame->getController()->getModel().get()))
    {
        auto pDocSh = xModel->GetObjectShell();
        StartListening(*pDocSh);
        if (auto oColor = pDocSh->GetRecentColor(nSlotId))
        {
            Update(*oColor);
            return;
        }
    }

    // tdf#72991 - remember last used color depending on slot id
    const auto aSlotNamedColorMap = frozen::make_unordered_map<sal_uInt16, NamedColor>(
        { { SID_ATTR_CHAR_COLOR,
            NamedColor(COL_DEFAULT_FONT, SvxResId(RID_SVXSTR_COLOR_DEFAULT_FONT)) },
          { SID_ATTR_CHAR_COLOR2,
            NamedColor(COL_DEFAULT_FONT, SvxResId(RID_SVXSTR_COLOR_DEFAULT_FONT)) },
          { SID_FRAME_LINECOLOR,
            NamedColor(COL_DEFAULT_FRAMELINE, SvxResId(RID_SVXSTR_COLOR_DEFAULT_FRAMELINE)) },
          { SID_ATTR_CHAR_COLOR_BACKGROUND,
            NamedColor(COL_DEFAULT_HIGHLIGHT, SvxResId(RID_SVXSTR_COLOR_DEFAULT_HIGHLIGHT)) },
          { SID_ATTR_CHAR_BACK_COLOR,
            NamedColor(COL_DEFAULT_HIGHLIGHT, SvxResId(RID_SVXSTR_COLOR_DEFAULT_HIGHLIGHT)) },
          { SID_BACKGROUND_COLOR,
            NamedColor(COL_DEFAULT_HIGHLIGHT, SvxResId(RID_SVXSTR_COLOR_DEFAULT_HIGHLIGHT)) },
          { SID_TABLE_CELL_BACKGROUND_COLOR,
            NamedColor(COL_DEFAULT_HIGHLIGHT, SvxResId(RID_SVXSTR_COLOR_DEFAULT_HIGHLIGHT)) },
          { SID_ATTR_LINE_COLOR,
            NamedColor(COL_DEFAULT_SHAPE_STROKE, SvxResId(RID_SVXSTR_COLOR_DEFAULT_SHAPE_STROKE)) },
          { SID_ATTR_FILL_COLOR, NamedColor(COL_DEFAULT_SHAPE_FILLING,
                                            SvxResId(RID_SVXSTR_COLOR_DEFAULT_SHAPE_FILLING)) }

        });

    const auto aIterator = aSlotNamedColorMap.find(nSlotId);
    if (aIterator != aSlotNamedColorMap.end())
    {
        NamedColor aNamedColor(aIterator->second);
        SvtViewOptions aViewOpt(EViewType::Dialog, u"ToolboxButtonColor"_ustr);
        if (aViewOpt.Exists())
        {
            css::uno::Any aUserItem = aViewOpt.GetUserItem(OUString::number(nSlotId));
            OUString aUserData;
            if (aUserItem >>= aUserData)
            {
                sal_Int32 nIdx = 0;
                aNamedColor.m_aName = o3tl::getToken(aUserData, 0, ';', nIdx);
                aNamedColor.m_aColor
                    = Color(ColorTransparencyTag::ColorTransparency,
                            o3tl::toUInt32(o3tl::getToken(aUserData, 0, ';', nIdx)));
            }
        }
        SetRecentColor(aNamedColor, /*Broadcast=*/false);
        Update(aNamedColor);
    }
    else
        Update(COL_TRANSPARENT);
}

void ToolboxButtonColorUpdaterBase::Notify(SfxBroadcaster& rBC, const SfxHint& rHint)
{
    if (rHint.GetId() == SfxHintId::Dying)
    {
        EndListeningAll();
    }
    else if (rHint.GetId() == SfxHintId::ColorsChanged)
    {
        if (auto oColor = static_cast<SfxObjectShell&>(rBC).GetRecentColor(mnSlotId))
        {
            Update(*oColor);
            // tdf#72991 - remember last used color depending on slot id
            const OUString aUserData
                = oColor->m_aName + ";"
                  + OUString::number(static_cast<sal_uInt32>(oColor->m_aColor));
            SvtViewOptions(EViewType::Dialog, u"ToolboxButtonColor"_ustr)
                .SetUserItem(OUString::number(mnSlotId), css::uno::Any(aUserData));
        }
    }
}

void ToolboxButtonColorUpdaterBase::SetRecentColor(const NamedColor& rNamedColor, bool bBroadcast)
{
    if (rtl::Reference xModel
        = dynamic_cast<SfxBaseModel*>(mxFrame->getController()->getModel().get()))
        xModel->GetObjectShell()->SetRecentColor(mnSlotId, rNamedColor, bBroadcast);
    else if (!mbWideButton)
        Update(rNamedColor);
}

VclToolboxButtonColorUpdater::VclToolboxButtonColorUpdater(
    sal_uInt16 nSlotId, ToolBoxItemId nTbxBtnId, ToolBox* pToolBox, bool bWideButton,
    const OUString& rCommandLabel, const OUString& rCommandURL,
    const css::uno::Reference<css::frame::XFrame>& rFrame)
    : ToolboxButtonColorUpdaterBase(bWideButton, rCommandLabel, rCommandURL, nSlotId, rFrame)
    , mnBtnId(nTbxBtnId)
    , mpTbx(pToolBox)
{
    Init(nSlotId);
}

void VclToolboxButtonColorUpdater::SetQuickHelpText(const OUString& rText)
{
    mpTbx->SetQuickHelpText(mnBtnId, rText);
}

OUString VclToolboxButtonColorUpdater::GetQuickHelpText() const
{
    return mpTbx->GetQuickHelpText(mnBtnId);
}

void VclToolboxButtonColorUpdater::SetImage(VirtualDevice* pVirDev)
{
    GDIMetaFile* pMtf = pVirDev->GetConnectMetaFile();

    assert(pMtf && "should have been set in ToolboxButtonColorUpdaterBase::Update");

    pMtf->Stop();
    pMtf->WindStart();

    Graphic aGraphic(*pMtf);

    mpTbx->SetItemImage(mnBtnId, Image(aGraphic.GetXGraphic()));
}

VclPtr<VirtualDevice> VclToolboxButtonColorUpdater::CreateVirtualDevice() const
{
    auto xVD = VclPtr<VirtualDevice>::Create(*mpTbx->GetOutDev());
    xVD->SetBackground(Wallpaper(COL_WHITE));
    return xVD;
}

vcl::ImageType VclToolboxButtonColorUpdater::GetImageSize() const { return mpTbx->GetImageSize(); }

Size VclToolboxButtonColorUpdater::GetItemSize(const Size& rImageSize) const
{
    if (mbWideButton)
        return mpTbx->GetItemContentSize(mnBtnId);
    return rImageSize;
}

ToolboxButtonColorUpdaterBase::~ToolboxButtonColorUpdaterBase() {}

void ToolboxButtonColorUpdaterBase::Update(const NamedColor& rNamedColor)
{
    Update(rNamedColor.m_aColor);

    // Also show the current color as QuickHelpText
    OUString colorSuffix = u" (%1)"_ustr.replaceFirst("%1", rNamedColor.m_aName);
    OUString colorHelpText = maCommandLabel + colorSuffix;
    SetQuickHelpText(colorHelpText);
}

void ToolboxButtonColorUpdaterBase::Update(const Color& rColor, bool bForceUpdate)
{
    vcl::ImageType eImageType = GetImageSize();

#ifdef IOS // tdf#126966
    eImageType = vcl::ImageType::Size32;
#endif

    const bool bSizeChanged = (meImageType != eImageType);
    meImageType = eImageType;
    const bool bDisplayModeChanged
        = (mbWasHiContrastMode
           != Application::GetSettings().GetStyleSettings().GetHighContrastMode());
    Color aColor(rColor);

    // !!! #109290# Workaround for SetFillColor with COL_AUTO
    if (aColor == COL_AUTO)
        aColor = COL_TRANSPARENT;

    if ((maCurColor == aColor) && !bSizeChanged && !bDisplayModeChanged && !bForceUpdate)
        return;

    const Image aImage
        = vcl::CommandInfoProvider::GetImageForCommand(maCommandURL, mxFrame, meImageType);

    const Size aItemSize = GetItemSize(aImage.GetSizePixel());
    if (!aItemSize.Width() || !aItemSize.Height())
        return;

    ScopedVclPtr<VirtualDevice> pVirDev(CreateVirtualDevice());
    pVirDev->SetOutputSizePixel(aItemSize, /*bErase*/ true, /*bAlphaMaskTransparent*/ true);

    std::unique_ptr<GDIMetaFile> xMetaFile;
    if (RecordVirtualDevice())
    {
        xMetaFile.reset(new GDIMetaFile);
        xMetaFile->SetPrefSize(pVirDev->GetOutputSize());
        xMetaFile->SetPrefMapMode(pVirDev->GetMapMode());
        xMetaFile->Record(pVirDev.get());
        pVirDev->EnableOutput(false);
    }

    tools::Rectangle aUpdateRect;
    if (aItemSize.Width() == aItemSize.Height())
        // tdf#84985 align color bar with icon bottom edge; integer arithmetic e.g. 26 - 26/4 <> 26 * 3/4
        aUpdateRect = tools::Rectangle(Point(0, aItemSize.Height() - aItemSize.Height() / 4),
                                       Size(aItemSize.Width(), aItemSize.Height() / 4));
    else
        aUpdateRect = tools::Rectangle(Point(aItemSize.Height() + 2, 2),
                                       Point(aItemSize.Width() - 3, aItemSize.Height() - 3));

    pVirDev->Push(vcl::PushFlags::CLIPREGION);

    // tdf#135121 don't include the part of the image which we will
    // overwrite with the target color so that for the transparent color
    // case the original background of the device is shown
    vcl::Region aRegion(tools::Rectangle(Point(0, 0), aItemSize));
    aRegion.Exclude(aUpdateRect);
    pVirDev->SetClipRegion(aRegion);

    pVirDev->DrawImage(Point(0, 0), aImage);

    pVirDev->Pop();

    const StyleSettings& rStyleSettings = Application::GetSettings().GetStyleSettings();
    mbWasHiContrastMode = rStyleSettings.GetHighContrastMode();

    if ((COL_TRANSPARENT != aColor) && (aItemSize.Width() == aItemSize.Height()))
        pVirDev->SetLineColor(aColor);
    else
        pVirDev->SetLineColor(rStyleSettings.GetDisableColor());

    // use not only COL_TRANSPARENT for detection of transparence,
    // but the method/way which is designed to do that
    const bool bIsFullyTransparent(aColor.IsFullyTransparent());
    maCurColor = aColor;

    if (bIsFullyTransparent)
    {
        pVirDev->SetFillColor();
    }
    else
    {
        pVirDev->SetFillColor(maCurColor);
    }

    pVirDev->DrawRect(aUpdateRect);

    SetImage(pVirDev.get());
}

OUString ToolboxButtonColorUpdaterBase::GetCurrentColorName() const
{
    OUString sColorName = GetQuickHelpText();
    // The obtained string is of format: color context (color name)
    // Generate a substring which contains only the color name
    sal_Int32 nStart = sColorName.indexOf('(');
    sColorName = sColorName.copy(nStart + 1);
    sal_Int32 nLength = sColorName.getLength();
    if (nLength > 0)
        sColorName = sColorName.copy(0, nLength - 1);
    return sColorName;
}

ToolboxButtonColorUpdater::ToolboxButtonColorUpdater(
    sal_uInt16 nSlotId, const OUString& rTbxBtnId, weld::Toolbar* ptrTbx, bool bWideButton,
    const OUString& rCommandLabel, const css::uno::Reference<css::frame::XFrame>& rFrame)
    : ToolboxButtonColorUpdaterBase(bWideButton, rCommandLabel, rTbxBtnId, nSlotId, rFrame)
    , msBtnId(rTbxBtnId)
    , mpTbx(ptrTbx)
{
    Init(nSlotId);
}

void ToolboxButtonColorUpdater::SetQuickHelpText(const OUString& rText)
{
    mpTbx->set_item_tooltip_text(msBtnId, rText);
}

OUString ToolboxButtonColorUpdater::GetQuickHelpText() const
{
    return mpTbx->get_item_tooltip_text(msBtnId);
}

void ToolboxButtonColorUpdater::SetImage(VirtualDevice* pVirDev)
{
    mpTbx->set_item_image(msBtnId, pVirDev);
}

VclPtr<VirtualDevice> ToolboxButtonColorUpdater::CreateVirtualDevice() const
{
    return mpTbx->create_virtual_device();
}

vcl::ImageType ToolboxButtonColorUpdater::GetImageSize() const { return mpTbx->get_icon_size(); }

Size ToolboxButtonColorUpdater::GetItemSize(const Size& rImageSize) const
{
    auto nWidth = rImageSize.Width();
    if (mbWideButton)
        nWidth = nWidth * 5;
    return Size(nWidth, rImageSize.Height());
}

ToolboxButtonLineStyleUpdater::ToolboxButtonLineStyleUpdater()
    : m_eXLS(css::drawing::LineStyle_NONE)
    , m_nDashStyleIndex(-1)
{
}

void ToolboxButtonLineStyleUpdater::Update(const css::frame::FeatureStateEvent& rEvent)
{
    if (rEvent.FeatureURL.Complete == ".uno:LineDash")
    {
        m_nDashStyleIndex = -1;

        SfxObjectShell* pSh = SfxObjectShell::Current();
        if (!pSh)
            return;
        const SvxDashListItem* pItem = pSh->GetItem(SID_DASH_LIST);
        if (!pItem)
            return;

        XLineDashItem aDashItem;
        aDashItem.PutValue(rEvent.State, 0);
        const XDash& rDash = aDashItem.GetDashValue();

        XDashListRef xLineStyleList = pItem->GetDashList();
        for (tools::Long i = 0; i < xLineStyleList->Count(); ++i)
        {
            const XDashEntry* pEntry = xLineStyleList->GetDash(i);
            const XDash& rEntry = pEntry->GetDash();
            if (rDash == rEntry)
            {
                m_nDashStyleIndex = i;
                break;
            }
        }
    }
    else if (rEvent.FeatureURL.Complete == ".uno:XLineStyle")
    {
        XLineStyleItem aLineStyleItem;
        aLineStyleItem.PutValue(rEvent.State, 0);

        m_eXLS = aLineStyleItem.GetValue();
    }
}

int ToolboxButtonLineStyleUpdater::GetStyleIndex() const
{
    int nRet;
    switch (m_eXLS)
    {
        case css::drawing::LineStyle_NONE:
            nRet = 0;
            break;
        case css::drawing::LineStyle_SOLID:
            nRet = 1;
            break;
        default:
            nRet = m_nDashStyleIndex + 2;
            break;
    }
    return nRet;
}
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
