// SPDX-License-Identifier: GPL-2.0-or-later
/**
 * @file
 * A simple dialog for previewing icon representation.
 */
/* Authors:
 *   Jon A. Cruz
 *   Bob Jamison
 *   Other dudes from The Inkscape Organization
 *   Abhishek Sharma
 *
 * Copyright (C) 2004 Bob Jamison
 * Copyright (C) 2005,2010 Jon A. Cruz
 *
 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
 */

#include <glibmm/i18n.h>
#include <glibmm/timer.h>
#include <glibmm/main.h>

#include <gtkmm/buttonbox.h>
#include <gtkmm/checkbutton.h>
#include <gtkmm/frame.h>

#include "desktop.h"
#include "document.h"
#include "inkscape.h"
#include "page-manager.h"

#include "display/cairo-utils.h"
#include "display/drawing.h"
#include "display/drawing-context.h"

#include "object/sp-namedview.h"
#include "object/sp-root.h"

#include "icon-preview.h"

#include "ui/widget/frame.h"

extern "C" {
// takes doc, drawing, icon, and icon name to produce pixels
guchar *
sp_icon_doc_icon( SPDocument *doc, Inkscape::Drawing &drawing,
                  const gchar *name, unsigned int psize, unsigned &stride);
}

#define noICON_VERBOSE 1

namespace Inkscape {
namespace UI {
namespace Dialog {


IconPreviewPanel &IconPreviewPanel::getInstance()
{
    IconPreviewPanel *instance = new IconPreviewPanel();

    instance->refreshPreview();

    return *instance;
}

//#########################################################################
//## E V E N T S
//#########################################################################

void IconPreviewPanel::on_button_clicked(int which)
{
    if ( hot != which ) {
        buttons[hot]->set_active( false );

        hot = which;
        updateMagnify();
        queue_draw();
    }
}




//#########################################################################
//## C O N S T R U C T O R    /    D E S T R U C T O R
//#########################################################################
/**
 * Constructor
 */
IconPreviewPanel::IconPreviewPanel()
    : DialogBase("/dialogs/iconpreview", "IconPreview")
    , drawing(nullptr)
    , drawing_doc(nullptr)
    , visionkey(0)
    , timer(nullptr)
    , renderTimer(nullptr)
    , pending(false)
    , minDelay(0.1)
    , targetId()
    , hot(1)
    , selectionButton(nullptr)
    , docModConn()
    , iconBox(Gtk::ORIENTATION_VERTICAL)
{
    Inkscape::Preferences *prefs = Inkscape::Preferences::get();
    numEntries = 0;

    bool pack = prefs->getBool("/iconpreview/pack", true);

    std::vector<Glib::ustring> pref_sizes = prefs->getAllDirs("/iconpreview/sizes/default");
    std::vector<int> rawSizes;

    for (auto & pref_size : pref_sizes) {
        if (prefs->getBool(pref_size + "/show", true)) {
            int sizeVal = prefs->getInt(pref_size + "/value", -1);
            if (sizeVal > 0) {
                rawSizes.push_back(sizeVal);
            }
        }
    }

    if ( !rawSizes.empty() ) {
        numEntries = rawSizes.size();
        sizes = new int[numEntries];
        int i = 0;
        for ( std::vector<int>::iterator it = rawSizes.begin(); it != rawSizes.end(); ++it, ++i ) {
            sizes[i] = *it;
        }
    }

    if ( numEntries < 1 )
    {
        numEntries = 5;
        sizes = new int[numEntries];
        sizes[0] = 16;
        sizes[1] = 24;
        sizes[2] = 32;
        sizes[3] = 48;
        sizes[4] = 128;
    }

    pixMem = new guchar*[numEntries];
    images = new Gtk::Image*[numEntries];
    labels = new Glib::ustring*[numEntries];
    buttons = new Gtk::ToggleToolButton*[numEntries];


    for ( int i = 0; i < numEntries; i++ ) {
        char *label = g_strdup_printf(_("%d x %d"), sizes[i], sizes[i]);
        labels[i] = new Glib::ustring(label);
        g_free(label);
        pixMem[i] = nullptr;
        images[i] = nullptr;
    }


    magLabel.set_label( *labels[hot] );

    Gtk::Box* magBox = new Gtk::Box(Gtk::ORIENTATION_VERTICAL);

    UI::Widget::Frame *magFrame = Gtk::manage(new UI::Widget::Frame(_("Magnified:")));
    magFrame->add( magnified );

    magBox->pack_start( *magFrame, Gtk::PACK_EXPAND_WIDGET );
    magBox->pack_start( magLabel, Gtk::PACK_SHRINK );


    Gtk::Box *verts = new Gtk::Box(Gtk::ORIENTATION_VERTICAL);
    Gtk::Box *horiz = nullptr;
    int previous = 0;
    int avail = 0;
    for ( int i = numEntries - 1; i >= 0; --i ) {
        int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, sizes[i]);
        pixMem[i] = new guchar[sizes[i] * stride];
        memset( pixMem[i], 0x00, sizes[i] * stride );

        auto pb = Gdk::Pixbuf::create_from_data(pixMem[i], Gdk::COLORSPACE_RGB, true, 8, sizes[i], sizes[i], stride);
        images[i] = Gtk::make_managed<Gtk::Image>(pb);
        Glib::ustring label(*labels[i]);
        buttons[i] = new Gtk::ToggleToolButton(label);
        buttons[i]->set_active( i == hot );
        if ( prefs->getBool("/iconpreview/showFrames", true) ) {
            Gtk::Frame *frame = new Gtk::Frame();
            frame->set_shadow_type(Gtk::SHADOW_ETCHED_IN);
            frame->add(*images[i]);
            buttons[i]->set_icon_widget(*Gtk::manage(frame));
        } else {
            buttons[i]->set_icon_widget(*images[i]);
        }

        buttons[i]->set_tooltip_text(label);

        buttons[i]->signal_clicked().connect( sigc::bind<int>( sigc::mem_fun(*this, &IconPreviewPanel::on_button_clicked), i) );

        buttons[i]->set_halign(Gtk::ALIGN_CENTER);
        buttons[i]->set_valign(Gtk::ALIGN_CENTER);

        if ( !pack || ( (avail == 0) && (previous == 0) ) ) {
            verts->pack_end(*(buttons[i]), Gtk::PACK_SHRINK);
            previous = sizes[i];
            avail = sizes[i];
        } else {
            int pad = 12;
            if ((avail < pad) || ((sizes[i] > avail) && (sizes[i] < previous))) {
                horiz = nullptr;
            }
            if ((horiz == nullptr) && (sizes[i] <= previous)) {
                avail = previous;
            }
            if (sizes[i] <= avail) {
                if (!horiz) {
                    horiz = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
                    avail = previous;
                    verts->pack_end(*horiz, Gtk::PACK_SHRINK);
                }
                horiz->pack_start(*(buttons[i]), Gtk::PACK_EXPAND_WIDGET);
                avail -= sizes[i];
                avail -= pad; // a little extra for padding
            } else {
                horiz = nullptr;
                verts->pack_end(*(buttons[i]), Gtk::PACK_SHRINK);
            }
        }
    }

    iconBox.pack_start(splitter);
    splitter.pack1( *magBox, true, false );
    UI::Widget::Frame *actuals = Gtk::manage(new UI::Widget::Frame (_("Actual Size:")));
    actuals->set_border_width(4);
    actuals->add(*verts);
    splitter.pack2( *actuals, false, false );


    selectionButton = new Gtk::CheckButton(C_("Icon preview window", "Sele_ction"), true);//selectionButton = (Gtk::ToggleButton*) gtk_check_button_new_with_mnemonic(_("_Selection")); // , GTK_RESPONSE_APPLY
    magBox->pack_start( *selectionButton, Gtk::PACK_SHRINK );
    selectionButton->set_tooltip_text(_("Selection only or whole document"));
    selectionButton->signal_clicked().connect( sigc::mem_fun(*this, &IconPreviewPanel::modeToggled) );

    gint val = prefs->getBool("/iconpreview/selectionOnly");
    selectionButton->set_active( val != 0 );

    pack_start(iconBox, Gtk::PACK_SHRINK);

    show_all_children();
}

IconPreviewPanel::~IconPreviewPanel()
{
    removeDrawing();
    if (timer) {
        timer->stop();
        delete timer;
        timer = nullptr;
    }
    if ( renderTimer ) {
        renderTimer->stop();
        delete renderTimer;
        renderTimer = nullptr;
    }

    docModConn.disconnect();
}

//#########################################################################
//## M E T H O D S
//#########################################################################


#if ICON_VERBOSE
static Glib::ustring getTimestr()
{
    Glib::ustring str;
    gint64 micr = g_get_monotonic_time();
    gint64 mins = ((int)round(micr / 60000000)) % 60;
    gdouble dsecs = micr / 1000000;
    gchar *ptr = g_strdup_printf(":%02u:%f", mins, dsecs);
    str = ptr;
    g_free(ptr);
    ptr = 0;
    return str;
}
#endif // ICON_VERBOSE

void IconPreviewPanel::selectionModified(Selection *selection, guint flags)
{
    if (getDesktop() && Inkscape::Preferences::get()->getBool("/iconpreview/autoRefresh", true)) {
        queueRefresh();
    }
}

void IconPreviewPanel::documentReplaced()
{
    removeDrawing();
    drawing_doc = getDocument();
    if (drawing_doc) {
        drawing = new Inkscape::Drawing();
        visionkey = SPItem::display_key_new(1);
        drawing->setRoot(drawing_doc->getRoot()->invoke_show(*drawing, visionkey, SP_ITEM_SHOW_DISPLAY));
        docDesConn = drawing_doc->connectDestroy([=]() { removeDrawing(); });
        queueRefresh();
    }
}

/// Safely delete the Inkscape::Drawing and references to it.
void IconPreviewPanel::removeDrawing()
{
    docDesConn.disconnect();
    if (!drawing) {
        return;
    }
    drawing_doc->getRoot()->invoke_hide(visionkey);
    delete drawing;
    drawing = nullptr;
    drawing_doc = nullptr;
}

void IconPreviewPanel::refreshPreview()
{
    auto document = getDocument();
    if (!timer) {
        timer = new Glib::Timer();
    }
    if (timer->elapsed() < minDelay) {
#if ICON_VERBOSE
        g_message( "%s Deferring refresh as too soon. calling queueRefresh()", getTimestr().c_str() );
#endif //ICON_VERBOSE
        // Do not refresh too quickly
        queueRefresh();
    } else if (document) {
#if ICON_VERBOSE
        g_message( "%s Refreshing preview.", getTimestr().c_str() );
#endif // ICON_VERBOSE
        bool hold = Inkscape::Preferences::get()->getBool("/iconpreview/selectionHold", true);
        SPObject *target = nullptr;
        if ( selectionButton && selectionButton->get_active() )
        {
            target = (hold && !targetId.empty()) ? document->getObjectById( targetId.c_str() ) : nullptr;
            if ( !target ) {
                targetId.clear();
                if (auto selection = getSelection()) {
                    for (auto item : selection->items()) {
                        if (gchar const *id = item->getId()) {
                            targetId = id;
                            target = item;
                        }
                    }
                }
            }
        } else {
            target = getDesktop()->getDocument()->getRoot();
        }
        if (target) {
            renderPreview(target);
        }
#if ICON_VERBOSE
        g_message( "%s  resetting timer", getTimestr().c_str() );
#endif // ICON_VERBOSE
        timer->reset();
    }
}

bool IconPreviewPanel::refreshCB()
{
    bool callAgain = true;
    if (!timer) {
        timer = new Glib::Timer();
    }
    if ( timer->elapsed() > minDelay ) {
#if ICON_VERBOSE
        g_message( "%s refreshCB() timer has progressed", getTimestr().c_str() );
#endif // ICON_VERBOSE
        callAgain = false;
        refreshPreview();
#if ICON_VERBOSE
        g_message( "%s refreshCB() setting pending false", getTimestr().c_str() );
#endif // ICON_VERBOSE
        pending = false;
    }
    return callAgain;
}

void IconPreviewPanel::queueRefresh()
{
    if (!pending) {
        pending = true;
#if ICON_VERBOSE
        g_message( "%s queueRefresh() Setting pending true", getTimestr().c_str() );
#endif // ICON_VERBOSE
        if (!timer) {
            timer = new Glib::Timer();
        }
        Glib::signal_idle().connect( sigc::mem_fun(this, &IconPreviewPanel::refreshCB), Glib::PRIORITY_DEFAULT_IDLE );
    }
}

void IconPreviewPanel::modeToggled()
{
    Inkscape::Preferences *prefs = Inkscape::Preferences::get();
    bool selectionOnly = (selectionButton && selectionButton->get_active());
    prefs->setBool("/iconpreview/selectionOnly", selectionOnly);
    if ( !selectionOnly ) {
        targetId.clear();
    }

    refreshPreview();
}

void overlayPixels(guchar *px, int width, int height, int stride,
                            unsigned r, unsigned g, unsigned b)
{
    int bytesPerPixel = 4;
    int spacing = 4;
    for ( int y = 0; y < height; y += spacing ) {
        guchar *ptr = px + y * stride;
        for ( int x = 0; x < width; x += spacing ) {
            *(ptr++) = r;
            *(ptr++) = g;
            *(ptr++) = b;
            *(ptr++) = 0xff;

            ptr += bytesPerPixel * (spacing - 1);
        }
    }

    if ( width > 1 && height > 1 ) {
        // point at the last pixel
        guchar *ptr = px + ((height-1) * stride) + ((width - 1) * bytesPerPixel);

        if ( width > 2 ) {
            px[4] = r;
            px[5] = g;
            px[6] = b;
            px[7] = 0xff;

            ptr[-12] = r;
            ptr[-11] = g;
            ptr[-10] = b;
            ptr[-9] = 0xff;
        }

        ptr[-4] = r;
        ptr[-3] = g;
        ptr[-2] = b;
        ptr[-1] = 0xff;

        px[0 + stride] = r;
        px[1 + stride] = g;
        px[2 + stride] = b;
        px[3 + stride] = 0xff;

        ptr[0 - stride] = r;
        ptr[1 - stride] = g;
        ptr[2 - stride] = b;
        ptr[3 - stride] = 0xff;

        if ( height > 2 ) {
            ptr[0 - stride * 3] = r;
            ptr[1 - stride * 3] = g;
            ptr[2 - stride * 3] = b;
            ptr[3 - stride * 3] = 0xff;
        }
    }
}

// takes doc, drawing, icon, and icon name to produce pixels
extern "C" guchar *
sp_icon_doc_icon( SPDocument *doc, Inkscape::Drawing &drawing,
                  gchar const *name, unsigned psize,
                  unsigned &stride)
{
    bool const dump = Inkscape::Preferences::get()->getBool("/debug/icons/dumpSvg");
    guchar *px = nullptr;

    if (doc) {
        SPObject *object = doc->getObjectById(name);
        if (object && SP_IS_ITEM(object)) {
            SPItem *item = SP_ITEM(object);
            // Find bbox in document
            Geom::OptRect dbox = item->documentVisualBounds();

            if ( object->parent == nullptr )
            {
                dbox = Geom::Rect(Geom::Point(0, 0),
                                Geom::Point(doc->getWidth().value("px"), doc->getHeight().value("px")));
            }

            /* This is in document coordinates, i.e. pixels */
            if ( dbox ) {
                /* Update to renderable state */
                double sf = 1.0;
                drawing.root()->setTransform(Geom::Scale(sf));
                drawing.update();
                /* Item integer bbox in points */
                // NOTE: previously, each rect coordinate was rounded using floor(c + 0.5)
                Geom::IntRect ibox = dbox->roundOutwards();

                if ( dump ) {
                    g_message( "   box    --'%s'  (%f,%f)-(%f,%f)", name, (double)ibox.left(), (double)ibox.top(), (double)ibox.right(), (double)ibox.bottom() );
                }

                /* Find button visible area */
                int width = ibox.width();
                int height = ibox.height();

                if ( dump ) {
                    g_message( "   vis    --'%s'  (%d,%d)", name, width, height );
                }

                {
                    int block = std::max(width, height);
                    if (block != static_cast<int>(psize) ) {
                        if ( dump ) {
                            g_message("      resizing" );
                        }
                        sf = (double)psize / (double)block;

                        drawing.root()->setTransform(Geom::Scale(sf));
                        drawing.update();

                        auto scaled_box = *dbox * Geom::Scale(sf);
                        ibox = scaled_box.roundOutwards();
                        if ( dump ) {
                            g_message( "   box2   --'%s'  (%f,%f)-(%f,%f)", name, (double)ibox.left(), (double)ibox.top(), (double)ibox.right(), (double)ibox.bottom() );
                        }

                        /* Find button visible area */
                        width = ibox.width();
                        height = ibox.height();
                        if ( dump ) {
                            g_message( "   vis2   --'%s'  (%d,%d)", name, width, height );
                        }
                    }
                }

                Geom::IntPoint pdim(psize, psize);
                int dx, dy;
                //dx = (psize - width) / 2;
                //dy = (psize - height) / 2;
                dx=dy=psize;
                dx=(dx-width)/2; // watch out for psize, since 'unsigned'-'signed' can cause problems if the result is negative
                dy=(dy-height)/2;
                Geom::IntRect area = Geom::IntRect::from_xywh(ibox.min() - Geom::IntPoint(dx,dy), pdim);
                /* Actual renderable area */
                Geom::IntRect ua = *Geom::intersect(ibox, area);

                if ( dump ) {
                    g_message( "   area   --'%s'  (%f,%f)-(%f,%f)", name, (double)area.left(), (double)area.top(), (double)area.right(), (double)area.bottom() );
                    g_message( "   ua     --'%s'  (%f,%f)-(%f,%f)", name, (double)ua.left(), (double)ua.top(), (double)ua.right(), (double)ua.bottom() );
                }

                stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, psize);

                /* Set up pixblock */
                px = g_new(guchar, stride * psize);
                memset(px, 0x00, stride * psize);

                /* Render */
                cairo_surface_t *s = cairo_image_surface_create_for_data(px,
                    CAIRO_FORMAT_ARGB32, psize, psize, stride);
                Inkscape::DrawingContext dc(s, ua.min());

                auto bg = doc->getPageManager().getDefaultBackgroundColor();

                cairo_t *cr = cairo_create(s);
                cairo_set_source_rgba(cr, bg[0], bg[1], bg[2], bg[3]);
                cairo_rectangle(cr, 0, 0, psize, psize);
                cairo_fill(cr);
                cairo_save(cr);
                cairo_destroy(cr);

                drawing.render(dc, ua);
                cairo_surface_destroy(s);

                // convert to GdkPixbuf format
                convert_pixels_argb32_to_pixbuf(px, psize, psize, stride);

                if ( Inkscape::Preferences::get()->getBool("/debug/icons/overlaySvg") ) {
                    overlayPixels( px, psize, psize, stride, 0x00, 0x00, 0xff );
                }
            }
        }
    }

    return px;
} // end of sp_icon_doc_icon()


void IconPreviewPanel::renderPreview( SPObject* obj )
{
    SPDocument * doc = obj->document;
    gchar const * id = obj->getId();
    if ( !renderTimer ) {
        renderTimer = new Glib::Timer();
    }
    renderTimer->reset();

#if ICON_VERBOSE
    g_message("%s setting up to render '%s' as the icon", getTimestr().c_str(), id );
#endif // ICON_VERBOSE

    for ( int i = 0; i < numEntries; i++ ) {
        unsigned unused;
        int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, sizes[i]);
        guchar *px = sp_icon_doc_icon(doc, *drawing, id, sizes[i], unused);
//         g_message( " size %d %s", sizes[i], (px ? "worked" : "failed") );
        if ( px ) {
            memcpy( pixMem[i], px, sizes[i] * stride );
            g_free( px );
            px = nullptr;
        } else {
            memset( pixMem[i], 0, sizes[i] * stride );
        }
        images[i]->set(images[i]->get_pixbuf());
        // images[i]->queue_draw();
    }
    updateMagnify();

    renderTimer->stop();
    minDelay = std::max( 0.1, renderTimer->elapsed() * 3.0 );
#if ICON_VERBOSE
    g_message("  render took %f seconds.", renderTimer->elapsed());
#endif // ICON_VERBOSE
}

void IconPreviewPanel::updateMagnify()
{
    Glib::RefPtr<Gdk::Pixbuf> buf = images[hot]->get_pixbuf()->scale_simple( 128, 128, Gdk::INTERP_NEAREST );
    magLabel.set_label( *labels[hot] );
    magnified.set( buf );
    // magnified.queue_draw();
    // magnified.get_parent()->queue_draw();
}

} //namespace Dialogs
} //namespace UI
} //namespace Inkscape

/*
  Local Variables:
  mode:c++
  c-file-style:"stroustrup"
  c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
  indent-tabs-mode:nil
  fill-column:99
  End:
*/
// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
