/***************************************************************************
                         skylabeler.h  -  K Desktop Planetarium
                             -------------------
    begin				: 2007-07-10
    copyright			: (C) 2007 by James B. Bowlin
    email				: bowlin@mindspring.com
 ***************************************************************************/

/***************************************************************************
 *																		 *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or	 *
 *   (at your option) any later version.								   *
 *																		 *
 ***************************************************************************/

#pragma once

#include "skylabel.h"

#include <QFontMetricsF>
#include <QList>
#include <QVector>
#include <QPainter>
#include <QPicture>
#include <QFont>

class QString;
class QPointF;
class SkyMap;
class Projector;
struct LabelRun;

typedef QList<LabelRun *> LabelRow;
typedef QVector<LabelRow *> ScreenRows;

/**
 *@class SkyLabeler
 * The purpose of this class is to prevent labels from overlapping.  We do this
 * by creating a virtual (lower Y-resolution) screen. Each "pixel" of this
 * screen essentially contains a boolean value telling us whether or not there
 * is an existing label covering at least part of that pixel.  Before you draw
 * a label, call mark( QPointF, QString ) of that label.  We will check to see
 * if it would overlap any existing label.  If there is overlap we return false.
 * If there is no overlap then we mark all the pixels that cover the new label
 * and return true.
 *
 * Since we need to check for overlap for every label every time it is
 * potentially drawn on the screen, efficiency is essential.  So instead of
 * having a 2-dimensional array of boolean values we use Run Length Encoding
 * and store the virtual array in a QVector of QLists.  Each element of the
 * vector, a LabelRow, corresponds to a horizontal strip of pixels on the actual
 * screen.  How many vertical pixels are in each strip is controlled by
 * m_yDensity.  The higher the density, the fewer vertical pixels per strip and
 * hence a larger number of strips are needed to cover the screen.  As
 * m_yDensity increases so does the density of the strips.
 *
 * The information in the X-dimension is completed run length encoded. A
 * consecutive run of pixels in one strip that are covered by one or more labels
 * is stored in a LabelRun object that merely stores the start pixel and the end
 * pixel.  A LabelRow is a list of LabelRun's stored in ascending order.  This
 * saves a lot of space over an explicit array and it also makes checking for
 * overlaps faster and even makes inserting new overlaps faster on average.
 *
 * Synopsis:
 *
 *   1) Create a new SkyLabeler
 *
 *   2) every time you want to draw a new screen, reset the labeler.
 *
 *   3) Either:
 *
 *	 A) Call drawLabel() or drawOffsetLabel(), or
 *
 *	 B) Call addLabel() or addOffsetLabel()
 *
 *  4) Call draw() if addLabel() or addOffsetLabel() were used.
 *
 *
 *
 * SkyLabeler works totally on a first come, first served basis which is almost
 * the direct opposite of a z-buffer where the objects drawn last are most
 * visible.  This is why the addLabel() and draw() routines were created.
 * They allow us to time-shift the drawing of labels and thus gives us control
 * over their priority.  The drawLabel() routines are still available but are
 * not being used.  The addLabel() routines adds a label to a specific buffer.
 * Each type of label has its own buffer which lets us control the font and
 * color as well as the priority.  The priority is now manually set in the
 * draw() routine by adjusting the order in which the various buffers get
 * drawn.
 *
 * Finally, even though this code was written to be very efficient, we might
 * want to take some care in how many labels we throw at it.  Sending it
 * a large number of overlapping labels can be wasteful. Also, if one type
 * of object floods it with labels early on then there may not be any room
 * left for other types of labels.  Therefore for some types of objects (stars)
 * we may want to have a zoom dependent label threshold magnitude just like
 * we have for drawing the stars themselves.  This would throw few labels
 * at the labeler when we are zoomed at and they would mostly overlap anyway
 * and it would give us more labels when the user is zoomed in and there
 * is more room for them.  The "b" key currently causes the labeler statistics
 * to be printed.  This may be useful in figuring out the best settings for
 * the magnitude limits.  It may even be possible to have KStars do some of
 * this throttling automatically but I haven't really thought about that
 * problem yet.
 *
 * -- James B. Bowlin  2007-08-02
 */
class SkyLabeler
{
  protected:
    SkyLabeler();
    SkyLabeler(SkyLabeler &skyLabler);

  public:
    enum label_t
    {
        STAR_LABEL,
        ASTEROID_LABEL,
        COMET_LABEL,
        PLANET_LABEL,
        JUPITER_MOON_LABEL,
        SATURN_MOON_LABEL,
        DEEP_SKY_LABEL,
        CONSTEL_NAME_LABEL,
        SATELLITE_LABEL,
        RUDE_LABEL, ///Rude labels block other labels FIXME: find a better solution
        NUM_LABEL_TYPES
    };

    //----- Static Methods ----------------------------------------------//

    static SkyLabeler *Instance();

    /**
         * @short returns the zoom dependent label offset.  This is used in this
         * class and in SkyObject.  It is important that the offsets be the same
         * so highlighted labels are always drawn exactly on top of the normally
         * drawn labels.
         */
    static double ZoomOffset();

    /**
         * @short static version of addLabel() below.
         */
    inline static void AddLabel(SkyObject *obj, label_t type) { pinstance->addLabel(obj, type); }

    //--------------------------------------------------------------------//
    ~SkyLabeler();

    /**
         * @short clears the virtual screen (if needed) and resizes the virtual
         * screen (if needed) to match skyMap.  A font must be specified which
         * is taken to be the average or normal font that will be used.  The
         * size of the horizontal strips will be (slightly) optimized for this
         * font.  We also adjust the font size in psky to smaller fonts if the
         * screen is zoomed out.  You can mimic this setting with the static
         * method SkyLabeler::setZoomFont( psky ).
         */
    void reset(SkyMap *skyMap);

/**
         * @short KStars Lite version of the function above
         */
#ifdef KSTARS_LITE
    void reset();
#endif

    /**
         * @short Draws labels using the given painter
         * @param p the painter to draw labels with
         */
    void draw(QPainter &p);

    //----- Font Setting -----//

    /**
         * @short adjusts the font in psky to be smaller if we are zoomed out.
         */
    void setZoomFont();

    /**
         * @short sets the pen used for drawing labels on the sky.
         */
    void setPen(const QPen &pen);

    /**
         * @short tells the labeler the font you will be using so it can figure
         * out the height and width of the labels.  Also sets this font in the
         * psky since this is almost always what is wanted.
         */
    void setFont(const QFont &font);

    /**
         * @short decreases the size of the font in psky and in the SkyLabeler
         * by the delta points.  Negative deltas will increase the font size.
         */
    void shrinkFont(int delta);

    /**
         * @short sets the font in SkyLabeler and in psky to the font psky
         * had originally when reset() was called.  Used by ConstellationNames.
         */
    void useStdFont();

    /**
         * @short sets the font in SkyLabeler and in psky back to the zoom
         * dependent value that was set in reset().  Also used in
         * ConstellationLines.
         */
    void resetFont();

    /**
         * @short returns the fontMetricsF we have already created.
         */
    QFontMetricsF &fontMetrics() { return m_fontMetrics; }

    //----- Drawing/Adding Labels -----//

    /**
         *@short sets four margins for help in keeping labels entirely on the
         * screen.
         */
    void getMargins(const QString &text, float *left, float *right, float *top, float *bot);

    /**
         * @short Tries to draw the text at the position and angle specified. If
         * the label would overlap an existing label it is not drawn and we
         * return false, otherwise the label is drawn, its position is marked
         * and we return true.
         */
    bool drawGuideLabel(QPointF &o, const QString &text, double angle);

    /**
         * @short Tries to draw a label for an object.
         * @param obj the object to draw the label for
         * @param _p the position of that object
         * @return true if the label was drawn
         * //FIXME: should this just take an object pointer and do its own projection?
         */
    bool drawNameLabel(SkyObject *obj, const QPointF &_p);

    /**
         *@short draw the object's name label on the map, without checking for
         *overlap with other labels.
         *@param obj reference to the QPainter on which to draw (either the sky pixmap or printer device)
         *@param _p The screen position for the label (in pixels; typically as found by SkyMap::toScreen())
         */
    void drawRudeNameLabel(SkyObject *obj, const QPointF &_p);

    /**
         * @short queues the label in the "type" buffer for later drawing.
         */
    void addLabel(SkyObject *obj, label_t type);

#ifdef KSTARS_LITE
    /**
         * @short queues the label in the "type" buffer for later drawing. Doesn't calculate the position of
         * SkyObject but uses pos as a position of label.
         */
    void addLabel(SkyObject *obj, QPointF pos, label_t type);
#endif
    /**
         *@short draws the labels stored in all the buffers.  You can change the
         * priority by editing the .cpp file and changing the order in which
         * buffers are drawn.  You can also change the fonts and colors there
         * too.
         */
    void drawQueuedLabels();

    /**
         * @short a convenience routine that draws all the labels from a single
         * buffer. Currently this is only called from within draw() above.
         */
    void drawQueuedLabelsType(SkyLabeler::label_t type);

    //----- Marking Regions -----//

    /**
         * @short tells the labeler the location and text of a label you want
         * to draw.  Returns true if there is room for the label and returns
         * false otherwise.  If it returns true, the location of the label is
         * marked on the virtual screen so future labels won't overlap it.
         *
         * It is usually easier to use drawLabel() or drawLabelOffest() instead
         * which both call mark() internally.
         */
    bool markText(const QPointF &p, const QString &text);

    /**
         * @short Works just like markText() above but for an arbitrary
         * rectangular region bounded by top, bot, left, and right.
         */
    bool markRegion(qreal left, qreal right, qreal top, qreal bot);

    //----- Diagnostics and Information -----//

    /**
         * @short diagnostic. the *percentage* of pixels that have been filled.
         * Expect return values between 0.0 and 100.0.  A fillRatio above 20
         * is pretty busy and crowded.  I think a fillRatio of about 10 looks
         * good.  The fillRatio will be lowered of the screen is zoomed out
         * so are big blank spaces around the celestial sphere.
         */
    float fillRatio();

    /**
         * @short diagnostic, the number of times mark() returned true divided by
         * the total number of times mark was called multiplied by 100.  Expect
         * return values between 0.0 an 100.  A hit ratio of 100 means no labels
         * would have overlapped.  A ratio of zero means no labels got drawn
         * (which should never happen).  A hitRatio around 50 might be a good
         * target to shoot for.  Expect it to be lower when fully zoomed out and
         * higher when zoomed in.
         */
    float hitRatio();

    /**
         * @short diagnostic, prints some brief statistics to the console.
         * Currently this is connected to the "b" key in SkyMapEvents.
         */
    void printInfo();

    inline QFont stdFont() { return m_stdFont; }
    inline QFont skyFont() { return m_skyFont; }
#ifdef KSTARS_LITE
    inline QFont drawFont() { return m_drawFont; }
#endif

    int hits() { return m_hits; }
    int marks() { return m_marks; }

  private:
    ScreenRows screenRows;
    int m_maxX { 0 };
    int m_maxY { 0 };
    int m_size { 0 };
    /// When to merge two adjacent regions
    int m_minDeltaX { 30 };
    int m_marks { 0 };
    int m_hits { 0 };
    int m_misses { 0 };
    int m_elements { 0 };
    int m_errors { 0 };
    qreal m_yScale { 0 };
    double m_offset { 0 };
    QFont m_stdFont, m_skyFont;
    QFontMetricsF m_fontMetrics;
//In KStars Lite this font should be used wherever font of m_p was changed or used
#ifdef KSTARS_LITE
    QFont m_drawFont;
#endif
    QPainter m_p;
    QPicture m_picture;
    QVector<LabelList> labelList;
    const Projector *m_proj { nullptr };
    static SkyLabeler *pinstance;
};
