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

#include <strings.hrc>
#include <dialmgr.hxx>

#include <basegfx/range/b2irange.hxx>
#include <com/sun/star/ui/dialogs/TemplateDescription.hpp>
#include <com/sun/star/ui/dialogs/ExecutableDialogResults.hpp>
#include <com/sun/star/ui/dialogs/XFilePicker3.hpp>

#include <comphelper/random.hxx>
#include <basegfx/polygon/b2dpolygontools.hxx>
#include <sfx2/filedlghelper.hxx>
#include <tools/stream.hxx>
#include <tools/urlobj.hxx>
#include <vcl/bitmapex.hxx>
#include <vcl/customweld.hxx>
#include <vcl/event.hxx>
#include <vcl/filter/PngImageWriter.hxx>
#include <vcl/svapp.hxx>
#include <vcl/salgtype.hxx>
#include <vcl/virdev.hxx>
#include <vcl/weld.hxx>
#include <svtools/optionsdrawinglayer.hxx>
#include <basegfx/matrix/b2dhommatrix.hxx>
#include <set>
#include <string_view>

using namespace com::sun::star;

namespace
{
    OUString lcl_genRandom( std::u16string_view rId )
    {
        //FIXME: plus timestamp
        unsigned int nRand = comphelper::rng::uniform_uint_distribution(0, 0xFFFF);
        return OUString( rId + OUString::number( nRand ) );
    }


    OUString lcl_AltDescr()
    {
        OUString aTempl(u"<alt id=\"%1\">"
                        " " //FIXME real dialog title or something
                        "</alt>"_ustr);
        aTempl = aTempl.replaceFirst( "%1", lcl_genRandom(u"alt_id") );

        return aTempl;
    }

    OUString lcl_Image( std::u16string_view rScreenshotId, const Size& rSize )
    {
        OUString aTempl(u"<image id=\"%1\" src=\"media/screenshots/%2.png\""
                           " width=\"%3cm\"  height=\"%4cm\">"
                           "%5"
                        "</image>"_ustr);
        aTempl = aTempl.replaceFirst( "%1", lcl_genRandom(u"img_id") );
        aTempl = aTempl.replaceFirst( "%2", rScreenshotId );
        aTempl = aTempl.replaceFirst( "%3", OUString::number( rSize.Width() ) );
        aTempl = aTempl.replaceFirst( "%4", OUString::number( rSize.Height() ) );
        aTempl = aTempl.replaceFirst( "%5", lcl_AltDescr() );

        return aTempl;
    }

    OUString lcl_ParagraphWithImage( std::u16string_view rScreenshotId, const Size& rSize )
    {
        OUString aTempl( u"<paragraph id=\"%1\" role=\"paragraph\">%2"
                         "</paragraph>"  SAL_NEWLINE_STRING ""_ustr );
        aTempl = aTempl.replaceFirst( "%1", lcl_genRandom(u"par_id") );
        aTempl = aTempl.replaceFirst( "%2", lcl_Image(rScreenshotId, rSize) );

        return aTempl;
    }

    OUString lcl_Bookmark( std::u16string_view rWidgetId )
    {
        OUString aTempl = u"<!-- Bookmark for widget %1 -->" SAL_NEWLINE_STRING
                          "<bookmark branch=\"hid/%2\" id=\"%3\" localize=\"false\"/>" SAL_NEWLINE_STRING ""_ustr;
        aTempl = aTempl.replaceFirst( "%1", rWidgetId );
        aTempl = aTempl.replaceFirst( "%2", rWidgetId );
        aTempl = aTempl.replaceFirst( "%3", lcl_genRandom(u"bm_id") );

        return aTempl;
    }
}

namespace
{
    class Picture : public weld::CustomWidgetController
    {
    private:
        ScreenshotAnnotationDlg_Impl *m_pDialog;
        bool m_bMouseOver;
    private:
        virtual void Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle&) override;
        virtual bool MouseMove(const MouseEvent& rMouseEvent) override;
        virtual bool MouseButtonUp(const MouseEvent& rMouseEvent) override;
    public:
        Picture(ScreenshotAnnotationDlg_Impl* pDialog)
            : m_pDialog(pDialog)
            , m_bMouseOver(false)
        {
        }

        bool IsMouseOver() const
        {
            return m_bMouseOver;
        }
    };
}

class ScreenshotAnnotationDlg_Impl
{
public:
    ScreenshotAnnotationDlg_Impl(
        weld::Window* pParent,
        weld::Builder& rParent,
        weld::Dialog& rParentDialog);
    ~ScreenshotAnnotationDlg_Impl();

private:
    // Handler for click on save
    DECL_LINK(saveButtonHandler, weld::Button&, void);

    // helper methods
    weld::ScreenShotEntry* CheckHit(const basegfx::B2IPoint& rPosition);
    void PaintScreenShotEntry(
        const weld::ScreenShotEntry& rEntry,
        const Color& rColor,
        double fLineWidth,
        double fTransparency);
    void RepaintToBuffer(
        bool bUseDimmed = false,
        bool bPaintHilight = false);
    void RepaintPictureElement();
    Point GetOffsetInPicture() const;

    // local variables
    weld::Window*               mpParentWindow;
    weld::Dialog&               mrParentDialog;
    BitmapEx                    maParentDialogBitmap;
    BitmapEx                    maDimmedDialogBitmap;
    Size                        maParentDialogSize;

    // VirtualDevice for buffered interaction paints
    VclPtr<VirtualDevice>       mxVirtualBufferDevice;

    // all detected children
    weld::ScreenShotCollection  maAllChildren;

    // highlighted/selected children
    weld::ScreenShotEntry*           mpHilighted;
    std::set< weld::ScreenShotEntry* >
                                maSelected;

    // list of detected controls
    Picture maPicture;
    std::unique_ptr<weld::CustomWeld> mxPicture;
    std::unique_ptr<weld::TextView> mxText;
    std::unique_ptr<weld::Button> mxSave;

    // save as text
    OUString                    maSaveAsText;
    OUString                    maMainMarkupText;

    // folder URL
    static OUString             maLastFolderURL;
public:
    void Paint(vcl::RenderContext& rRenderContext);
    bool MouseMove(const MouseEvent& rMouseEvent);
    bool MouseButtonUp();
};

OUString ScreenshotAnnotationDlg_Impl::maLastFolderURL = OUString();

ScreenshotAnnotationDlg_Impl::ScreenshotAnnotationDlg_Impl(
    weld::Window* pParent,
    weld::Builder& rParentBuilder,
    weld::Dialog& rParentDialog)
:   mpParentWindow(pParent),
    mrParentDialog(rParentDialog),
    mxVirtualBufferDevice(nullptr),
    mpHilighted(nullptr),
    maPicture(this),
    maSaveAsText(CuiResId(RID_CUISTR_SAVE_SCREENSHOT_AS))
{
    VclPtr<VirtualDevice> xParentDialogSurface(rParentDialog.screenshot());
    maParentDialogSize = xParentDialogSurface->GetOutputSizePixel();
    maParentDialogBitmap = xParentDialogSurface->GetBitmapEx(Point(), maParentDialogSize);
    maDimmedDialogBitmap = maParentDialogBitmap;

    // image ain't empty
    assert(!maParentDialogBitmap.IsEmpty());
    assert(0 != maParentDialogBitmap.GetSizePixel().Width());
    assert(0 != maParentDialogBitmap.GetSizePixel().Height());

    // get needed widgets
    mxPicture.reset(new weld::CustomWeld(rParentBuilder, u"picture"_ustr, maPicture));
    assert(mxPicture);
    mxText = rParentBuilder.weld_text_view(u"text"_ustr);
    assert(mxText);
    mxSave = rParentBuilder.weld_button(u"save"_ustr);
    assert(mxSave);

    // set screenshot image at DrawingArea, resize, set event listener
    if (mxPicture)
    {
        maAllChildren = mrParentDialog.collect_screenshot_data();

        // to make clear that maParentDialogBitmap is a background image, adjust
        // luminance a bit for maDimmedDialogBitmap - other methods may be applied
        maDimmedDialogBitmap.Adjust(-15, 0, 0, 0, 0);

        // init paint buffering VirtualDevice
        mxVirtualBufferDevice = VclPtr<VirtualDevice>::Create(*Application::GetDefaultDevice(), DeviceFormat::WITHOUT_ALPHA);
        mxVirtualBufferDevice->SetOutputSizePixel(maParentDialogSize);
        mxVirtualBufferDevice->SetFillColor(COL_TRANSPARENT);

        // initially set image for picture control
        mxVirtualBufferDevice->DrawBitmapEx(Point(0, 0), maDimmedDialogBitmap);

        // set size for picture control, this will re-layout so that
        // the picture control shows the whole dialog
        maPicture.SetOutputSizePixel(maParentDialogSize);
        mxPicture->set_size_request(maParentDialogSize.Width(), maParentDialogSize.Height());

        mxPicture->queue_draw();
    }

    // set some test text at VclMultiLineEdit and make read-only - only
    // copying content to clipboard is allowed
    if (mxText)
    {
        mxText->set_size_request(400, mxText->get_height_rows(10));
        OUString aHelpId = mrParentDialog.get_help_id();
        Size aSizeCm = Application::GetDefaultDevice()->PixelToLogic(maParentDialogSize, MapMode(MapUnit::MapCM));
        maMainMarkupText = lcl_ParagraphWithImage( aHelpId, aSizeCm );
        mxText->set_text( maMainMarkupText );
        mxText->set_editable(false);
    }

    // set click handler for save button
    if (mxSave)
    {
        mxSave->connect_clicked(LINK(this, ScreenshotAnnotationDlg_Impl, saveButtonHandler));
    }
}

ScreenshotAnnotationDlg_Impl::~ScreenshotAnnotationDlg_Impl()
{
    mxVirtualBufferDevice.disposeAndClear();
}

IMPL_LINK_NOARG(ScreenshotAnnotationDlg_Impl, saveButtonHandler, weld::Button&, void)
{
    // 'save screenshot...' pressed, offer to save maParentDialogBitmap
    // as PNG image, use *.id file name as screenshot file name offering
    // get a suggestion for the filename from buildable name
    OUString aDerivedFileName = mrParentDialog.get_buildable_name();

    sfx2::FileDialogHelper aFileDlg(ui::dialogs::TemplateDescription::FILESAVE_AUTOEXTENSION,
                                                             FileDialogFlags::NONE, mpParentWindow);
    aFileDlg.SetContext(sfx2::FileDialogHelper::ScreenshotAnnotation);

    const uno::Reference< ui::dialogs::XFilePicker3 > xFilePicker = aFileDlg.GetFilePicker();

    xFilePicker->setTitle(maSaveAsText);

    if (!maLastFolderURL.isEmpty())
    {
        xFilePicker->setDisplayDirectory(maLastFolderURL);
    }

    xFilePicker->appendFilter(u"*.png"_ustr, u"*.png"_ustr);
    xFilePicker->setCurrentFilter(u"*.png"_ustr);
    xFilePicker->setDefaultName(aDerivedFileName);
    xFilePicker->setMultiSelectionMode(false);

    if (xFilePicker->execute() != ui::dialogs::ExecutableDialogResults::OK)
        return;

    maLastFolderURL = xFilePicker->getDisplayDirectory();
    const uno::Sequence< OUString > files(xFilePicker->getSelectedFiles());

    if (!files.hasElements())
        return;

    const OUString& aConfirmedName = files[0];

    if (aConfirmedName.isEmpty())
        return;

    INetURLObject aConfirmedURL(aConfirmedName);
    OUString aCurrentExtension(aConfirmedURL.getExtension());

    if (!aCurrentExtension.isEmpty() && aCurrentExtension != "png")
    {
        aConfirmedURL.removeExtension();
        aCurrentExtension.clear();
    }

    if (aCurrentExtension.isEmpty())
    {
        aConfirmedURL.setExtension(u"png");
    }

    // open stream
    SvFileStream aNew(aConfirmedURL.PathToFileName(), StreamMode::WRITE | StreamMode::TRUNC);

    if (!aNew.IsOpen())
        return;

    // prepare bitmap to save - do use the original screenshot here,
    // not the dimmed one
    RepaintToBuffer();

    // extract Bitmap
    const BitmapEx aTargetBitmap(
        mxVirtualBufferDevice->GetBitmapEx(
        Point(0, 0),
        mxVirtualBufferDevice->GetOutputSizePixel()));

    // write as PNG
    vcl::PngImageWriter aPNGWriter(aNew);
    aPNGWriter.write(aTargetBitmap);
}

weld::ScreenShotEntry* ScreenshotAnnotationDlg_Impl::CheckHit(const basegfx::B2IPoint& rPosition)
{
    weld::ScreenShotEntry* pRetval = nullptr;

    for (auto&& rCandidate : maAllChildren)
    {
        if (rCandidate.getB2IRange().isInside(rPosition))
        {
            if (pRetval)
            {
                if (pRetval->getB2IRange().isInside(rCandidate.getB2IRange().getMinimum())
                    && pRetval->getB2IRange().isInside(rCandidate.getB2IRange().getMaximum()))
                {
                    pRetval = &rCandidate;
                }
            }
            else
            {
                pRetval = &rCandidate;
            }
        }
    }

    return pRetval;
}

void ScreenshotAnnotationDlg_Impl::PaintScreenShotEntry(
    const weld::ScreenShotEntry& rEntry,
    const Color& rColor,
    double fLineWidth,
    double fTransparency)
{
    if (!(mxPicture && mxVirtualBufferDevice))
        return;

    basegfx::B2DRange aB2DRange(rEntry.getB2IRange());

    // grow in pixels to be a little bit 'outside'. This also
    // ensures that getWidth()/getHeight() ain't 0.0 (see division below)
    static const double fGrowTopLeft(1.5);
    static const double fGrowBottomRight(0.5);
    aB2DRange.expand(aB2DRange.getMinimum() - basegfx::B2DPoint(fGrowTopLeft, fGrowTopLeft));
    aB2DRange.expand(aB2DRange.getMaximum() + basegfx::B2DPoint(fGrowBottomRight, fGrowBottomRight));

    // edge rounding in pixel. Need to convert, value for
    // createPolygonFromRect is relative [0.0 .. 1.0]
    static const double fEdgeRoundPixel(8.0);
    const basegfx::B2DPolygon aPolygon(
        basegfx::utils::createPolygonFromRect(
        aB2DRange,
        fEdgeRoundPixel / aB2DRange.getWidth(),
        fEdgeRoundPixel / aB2DRange.getHeight()));

    mxVirtualBufferDevice->SetLineColor(rColor);

    // try to use transparency
    if (!mxVirtualBufferDevice->DrawPolyLineDirect(
        basegfx::B2DHomMatrix(),
        aPolygon,
        fLineWidth,
        fTransparency,
        nullptr, // MM01
        basegfx::B2DLineJoin::Round))
    {
        // no transparency, draw without
        mxVirtualBufferDevice->DrawPolyLine(
            aPolygon,
            fLineWidth);
    }
}

Point ScreenshotAnnotationDlg_Impl::GetOffsetInPicture() const
{
    const Size aPixelSizeTarget(maPicture.GetOutputSizePixel());

    return Point(
        aPixelSizeTarget.Width() > maParentDialogSize.Width() ? (aPixelSizeTarget.Width() - maParentDialogSize.Width()) >> 1 : 0,
        aPixelSizeTarget.Height() > maParentDialogSize.Height() ? (aPixelSizeTarget.Height() - maParentDialogSize.Height()) >> 1 : 0);
}

void ScreenshotAnnotationDlg_Impl::RepaintToBuffer(
    bool bUseDimmed,
    bool bPaintHilight)
{
    if (!mxVirtualBufferDevice)
        return;

    // reset with original screenshot bitmap
    mxVirtualBufferDevice->DrawBitmapEx(
        Point(0, 0),
        bUseDimmed ? maDimmedDialogBitmap : maParentDialogBitmap);

    // get various options
    const Color aHilightColor(SvtOptionsDrawinglayer::getHilightColor());
    const double fTransparence(SvtOptionsDrawinglayer::GetTransparentSelectionPercent() * 0.01);
    const bool bIsAntiAliasing(SvtOptionsDrawinglayer::IsAntiAliasing());
    const AntialiasingFlags nOldAA(mxVirtualBufferDevice->GetAntialiasing());

    if (bIsAntiAliasing)
    {
        mxVirtualBufferDevice->SetAntialiasing(AntialiasingFlags::Enable);
    }

    // paint selected entries
    for (auto&& rCandidate : maSelected)
    {
        static const double fLineWidthEntries(5.0);
        PaintScreenShotEntry(*rCandidate, COL_LIGHTRED, fLineWidthEntries, fTransparence * 0.2);
    }

    // paint highlighted entry
    if (mpHilighted && bPaintHilight)
    {
        static const double fLineWidthHilight(7.0);
        PaintScreenShotEntry(*mpHilighted, aHilightColor, fLineWidthHilight, fTransparence);
    }

    if (bIsAntiAliasing)
    {
        mxVirtualBufferDevice->SetAntialiasing(nOldAA);
    }
}

void ScreenshotAnnotationDlg_Impl::RepaintPictureElement()
{
    if (mxPicture && mxVirtualBufferDevice)
    {
        // reset image in buffer, use dimmed version and allow highlight
        RepaintToBuffer(true, true);
        mxPicture->queue_draw();
    }
}

void ScreenshotAnnotationDlg_Impl::Paint(vcl::RenderContext& rRenderContext)
{
    Point aPos(GetOffsetInPicture());
    Size aSize(mxVirtualBufferDevice->GetOutputSizePixel());
    rRenderContext.DrawOutDev(aPos, aSize, Point(), aSize, *mxVirtualBufferDevice);
}

void Picture::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle&)
{
    m_pDialog->Paint(rRenderContext);
}

bool ScreenshotAnnotationDlg_Impl::MouseMove(const MouseEvent& rMouseEvent)
{
    bool bRepaint(false);

    if (maPicture.IsMouseOver())
    {
        const weld::ScreenShotEntry* pOldHit = mpHilighted;
        const Point aOffset(GetOffsetInPicture());
        const basegfx::B2IPoint aMousePos(
            rMouseEvent.GetPosPixel().X() - aOffset.X(),
            rMouseEvent.GetPosPixel().Y() - aOffset.Y());
        const weld::ScreenShotEntry* pHit = CheckHit(aMousePos);

        if (pHit && pOldHit != pHit)
        {
            mpHilighted = const_cast<weld::ScreenShotEntry*>(pHit);
            bRepaint = true;
        }
    }
    else if (mpHilighted)
    {
        mpHilighted = nullptr;
        bRepaint = true;
    }

    if (bRepaint)
    {
        RepaintPictureElement();
    }

    return true;
}

bool Picture::MouseMove(const MouseEvent& rMouseEvent)
{
    if (rMouseEvent.IsEnterWindow())
        m_bMouseOver = true;
    if (rMouseEvent.IsLeaveWindow())
        m_bMouseOver = false;
    return m_pDialog->MouseMove(rMouseEvent);
}

bool ScreenshotAnnotationDlg_Impl::MouseButtonUp()
{
    // event in picture frame
    bool bRepaint(false);

    if (maPicture.IsMouseOver() && mpHilighted)
    {
        if (maSelected.erase(mpHilighted) == 0)
        {
            maSelected.insert(mpHilighted);
        }

        OUStringBuffer aBookmarks(maMainMarkupText);
        for (auto&& rCandidate : maSelected)
            aBookmarks.append(lcl_Bookmark(rCandidate->GetHelpId()));

        mxText->set_text( aBookmarks.makeStringAndClear() );
        bRepaint = true;
    }

    if (bRepaint)
    {
        RepaintPictureElement();
    }

    return true;
}

bool Picture::MouseButtonUp(const MouseEvent&)
{
    return m_pDialog->MouseButtonUp();
}

ScreenshotAnnotationDlg::ScreenshotAnnotationDlg(weld::Dialog& rParentDialog)
    : GenericDialogController(&rParentDialog, u"cui/ui/screenshotannotationdialog.ui"_ustr, u"ScreenshotAnnotationDialog"_ustr)
{
    m_pImpl.reset(new ScreenshotAnnotationDlg_Impl(m_xDialog.get(), *m_xBuilder, rParentDialog));
}

ScreenshotAnnotationDlg::~ScreenshotAnnotationDlg()
{
}

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