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

#include "skylabeler.h"

#include <cstdio>

#include <QPainter>
#include <QPixmap>

#include "Options.h"
#include "kstarsdata.h"   // MINZOOM
#include "skymap.h"
#include "projections/projector.h"

//---------------------------------------------------------------------------//
// A Little data container class
//---------------------------------------------------------------------------//

typedef struct LabelRun
{
    LabelRun(int s, int e) : start(s), end(e) {}
    int start;
    int end;

} LabelRun;


//----- Now for the main event ----------------------------------------------//

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


SkyLabeler* SkyLabeler::pinstance = 0;
SkyLabeler* SkyLabeler::Instance( )
{
    if ( ! pinstance ) pinstance = new SkyLabeler();
    return pinstance;
}


void SkyLabeler::setZoomFont()
{
#ifndef KSTARS_LITE
    QFont font( m_p.font() );
#else
    QFont font( m_stdFont );
#endif
    int deltaSize = 0;
    if ( Options::zoomFactor() < 2.0 * MINZOOM )
        deltaSize = 2;
    else if ( Options::zoomFactor() < 10.0 * MINZOOM )
        deltaSize = 1;

#ifndef KSTARS_LITE
    if ( deltaSize ) {
        font.setPointSize( font.pointSize() - deltaSize );
        m_p.setFont( font );
    }
#else
    if ( deltaSize ) {
        font.setPointSize( font.pointSize() - deltaSize );
    }
    if(m_drawFont.pointSize() != font.pointSize()) {
        m_drawFont = font;
    }
#endif
}

double SkyLabeler::ZoomOffset()
{
    double offset = dms::PI * Options::zoomFactor()/10800.0/3600.0;
    return 4.0 + offset * 0.5;
}

//----- Constructor ---------------------------------------------------------//

SkyLabeler::SkyLabeler() :
        m_maxY(0),
        m_size(0),
        m_fontMetrics( QFont() ),
        m_picture(-1),
        labelList( NUM_LABEL_TYPES ),
        m_proj(0)
{
    m_errors = 0;
    m_minDeltaX = 30;    // when to merge two adjacent regions
    m_marks = m_hits = m_misses = m_elements = 0;

#ifdef KSTARS_LITE
    //Painter is needed to get default font and we use it only once to have only one warning
    m_stdFont = QFont();

    //For some reason there is no point size in default font on Android
#ifdef ANDROID
    m_stdFont.setPointSize(10);
#endif

#endif
}


SkyLabeler::~SkyLabeler()
{
    for (int y = 0; y < screenRows.size(); y++ ) {
        LabelRow* row = screenRows[y];
        for ( int i = 0; i < row->size(); i++) {
            delete row->at(i);
        }
        delete row;
    }
}

bool SkyLabeler::drawGuideLabel( QPointF& o, const QString& text, double angle )
{
    // Create bounding rectangle by rotating the (height x width) rectangle
    qreal h = m_fontMetrics.height();
    qreal w = m_fontMetrics.width( text );
    qreal s = sin( angle * dms::PI / 180.0 );
    qreal c = cos( angle * dms::PI / 180.0 );

    qreal w2 = w / 2.0;

    qreal top, bot, left, right;

    // These numbers really do depend on the sign of the angle like this
    if ( angle >= 0.0 ) {
        top   = o.y()           - s * w2;
        bot   = o.y() + c *  h  + s * w2;
        left  = o.x() - c * w2  - s * h;
        right = o.x() + c * w2;
    }
    else {
        top   = o.y()           + s * w2;
        bot   = o.y() + c *  h  - s * w2;
        left  = o.x() - c * w2;
        right = o.x() + c * w2  - s * h;
    }

    // return false if label would overlap existing label
    if ( ! markRegion( left, right, top, bot) )
        return false;

    // for debugging the bounding rectangle:
    //psky.drawLine( QPointF( left,  top ), QPointF( right, top ) );
    //psky.drawLine( QPointF( right, top ), QPointF( right, bot ) );
    //psky.drawLine( QPointF( right, bot ), QPointF( left,  bot ) );
    //psky.drawLine( QPointF( left,  bot ), QPointF( left,  top ) );

    // otherwise draw the label and return true
    m_p.save();
    m_p.translate( o );

    m_p.rotate( angle );                        //rotate the coordinate system
    m_p.drawText( QPointF( -w2, h ), text );
    m_p.restore();                              //reset coordinate system

    return true;
}

bool SkyLabeler::drawNameLabel(SkyObject* obj, const QPointF& _p)
{
    QString sLabel = obj->labelString();
    if (sLabel.isEmpty())
        return false;

    double offset = obj->labelOffset();
    QPointF p( _p.x()+offset, _p.y()+offset );

    if ( !markText( p, sLabel ) ) {
        return false;
    } else {
        m_p.drawText( p, sLabel );
        return true;
    }
}


void SkyLabeler::setFont( const QFont& font )
{
#ifndef KSTARS_LITE
    m_p.setFont( font );
#else
    m_drawFont = font;
#endif
    m_fontMetrics = QFontMetrics( font );
}

void SkyLabeler::setPen(const QPen& pen)
{
    #ifdef KSTARS_LITE
        Q_UNUSED(pen);
    #else
        m_p.setPen(pen);
    #endif
}

void SkyLabeler::shrinkFont( int delta )
{
    #ifndef KSTARS_LITE
        QFont font( m_p.font() );
    #else
        QFont font( m_drawFont );
    #endif
        font.setPointSize( font.pointSize() - delta );
    setFont( font );
}

void SkyLabeler::useStdFont()
{
    setFont( m_stdFont );
}

void SkyLabeler::resetFont()
{
    setFont( m_skyFont );
}

void SkyLabeler::getMargins( const QString& text, float *left,
                             float *right, float *top, float *bot )
{
    float height     = m_fontMetrics.height();
    float width      = m_fontMetrics.width( text );
    float sideMargin = m_fontMetrics.width("MM") + width / 2.0;

    // Create the margins within which it is okay to draw the label
    double winHeight;
    double winWidth;
#ifdef KSTARS_LITE
    winHeight = SkyMapLite::Instance()->height();
    winWidth = SkyMapLite::Instance()->width();
#else
    winHeight = m_p.window().height();
    winWidth = m_p.window().width();
#endif

    *right = winWidth - sideMargin;
    *left  = sideMargin;
    *top   = height;
    *bot   = winHeight - 2.0 * height;
}

void SkyLabeler::reset( SkyMap* skyMap )
{
    // ----- Set up Projector ---
    m_proj = skyMap->projector();
    // ----- Set up Painter -----
    if( m_p.isActive() )
        m_p.end();
    m_picture = QPicture();
    m_p.begin(&m_picture);
    //This works around BUG 10496 in Qt
    m_p.drawPoint( 0, 0 );
    m_p.drawPoint( skyMap->width() + 1, skyMap->height() + 1);
    // ----- Set up Zoom Dependent Font -----

    m_stdFont = QFont( m_p.font() );
    setZoomFont();
    m_skyFont = m_p.font();
    m_fontMetrics = QFontMetrics( m_skyFont );
    m_minDeltaX = (int) m_fontMetrics.width("MMMMM");

    // ----- Set up Zoom Dependent Offset -----
    m_offset = SkyLabeler::ZoomOffset();

    // ----- Prepare Virtual Screen -----
    m_yScale = (m_fontMetrics.height() + 1.0);

    int maxY = int( skyMap->height() / m_yScale );
    if ( maxY < 1 ) maxY = 1;                         // prevents a crash below?

    int m_maxX = skyMap->width();
    m_size = (maxY + 1) * m_maxX;

    // Resize if needed:
    if ( maxY > m_maxY ) {
        screenRows.resize( m_maxY );
        for ( int y = m_maxY; y <= maxY; y++) {
            screenRows.append( new LabelRow() );
        }
        //printf("resize: %d -> %d, size:%d\n", m_maxY, maxY, screenRows.size());
    }

    // Clear all pre-existing rows as needed

    int minMaxY = (maxY < m_maxY) ? maxY : m_maxY;

    for (int y = 0; y <= minMaxY; y++) {
        LabelRow* row = screenRows[y];
        for ( int i = 0; i < row->size(); i++) {
            delete row->at(i);
        }
        row->clear();
    }

    // never decrease m_maxY:
    if ( m_maxY < maxY ) m_maxY = maxY;

    // reset the counters
    m_marks = m_hits = m_misses = m_elements = 0;

    //----- Clear out labelList -----
    for (int i = 0; i < labelList.size(); i++) {
        labelList[ i ].clear();
    }
}

#ifdef KSTARS_LITE
void SkyLabeler::reset()
{
    SkyMapLite * skyMap = SkyMapLite::Instance();
    // ----- Set up Projector ---
    m_proj = skyMap->projector();

    //m_stdFont was moved to constructor
    setZoomFont();
    m_skyFont = m_drawFont;
    m_fontMetrics = QFontMetrics( m_skyFont );
    m_minDeltaX = (int) m_fontMetrics.width("MMMMM");
    // ----- Set up Zoom Dependent Offset -----
    m_offset = ZoomOffset();

    // ----- Prepare Virtual Screen -----
    m_yScale = (m_fontMetrics.height() + 1.0);

    int maxY = int( skyMap->height() / m_yScale );
    if ( maxY < 1 ) maxY = 1;                         // prevents a crash below?

    int m_maxX = skyMap->width();
    m_size = (maxY + 1) * m_maxX;

    // Resize if needed:
    if ( maxY > m_maxY ) {
        screenRows.resize( m_maxY );
        for ( int y = m_maxY; y <= maxY; y++) {
            screenRows.append( new LabelRow() );
        }
        //printf("resize: %d -> %d, size:%d\n", m_maxY, maxY, screenRows.size());
    }

    // Clear all pre-existing rows as needed

    int minMaxY = (maxY < m_maxY) ? maxY : m_maxY;

    for (int y = 0; y <= minMaxY; y++) {
        LabelRow* row = screenRows[y];
        for ( int i = 0; i < row->size(); i++) {
            delete row->at(i);
        }
        row->clear();
    }

    // never decrease m_maxY:
    if ( m_maxY < maxY ) m_maxY = maxY;

    // reset the counters
    m_marks = m_hits = m_misses = m_elements = 0;

    //----- Clear out labelList -----
    for (int i = 0; i < labelList.size(); i++) {
        labelList[ i ].clear();
    }
}
#endif

void SkyLabeler::draw(QPainter& p)
{
    //FIXME: need a better soln. Apparently starting a painter
    //clears the picture.
    // But it's not like that's something that should be in the docs, right?
    // No, that's definitely better to leave to people to figure out on their own.
    if( m_p.isActive() ) { m_p.end(); }
    m_picture.play(&p); //can't replay while it's being painted on
                        //this is also undocumented btw.
    //m_p.begin(&m_picture);
}

// We use Run Length Encoding to hold the information instead of an array of
// chars.  This is both faster and smaller but the code is more complicated.
//
// This code is easy to break and hard to fix.

bool SkyLabeler::markText( const QPointF& p, const QString& text )
{

    qreal maxX =  p.x() + m_fontMetrics.width( text );
    qreal minY = p.y() - m_fontMetrics.height();
    return markRegion( p.x(), maxX, p.y(), minY );
}

bool SkyLabeler::markRegion( qreal left, qreal right, qreal top, qreal bot )
{
    if ( m_maxY < 1 ) {
        if ( ! m_errors++ )
            qDebug() << QString("Someone forgot to reset the SkyLabeler!");
        return true;
    }

    // setup x coordinates of rectangular region
    int minX = int( left );
    int maxX = int( right );
    if (maxX < minX) {
        maxX = minX;
        minX = int( right );
    }

    // setup y coordinates
    int maxY = int( bot / m_yScale );
    int minY = int( top / m_yScale );

    if ( maxY < 0 ) maxY = 0;
    if ( maxY > m_maxY ) maxY = m_maxY;
    if ( minY < 0 ) minY = 0;
    if ( minY > m_maxY ) minY = m_maxY;

    if ( maxY < minY ) {
        int temp = maxY;
        maxY = minY;
        minY = temp;
    }

    // check to see if we overlap any existing label
    // We must check all rows before we start marking
    for (int y = minY; y <= maxY; y++ ) {
        LabelRow* row = screenRows[ y ];
        int i;
        for ( i = 0; i < row->size(); i++) {
            if ( row->at( i )->end < minX ) continue;  // skip past these
            if ( row->at( i )->start > maxX ) break;
            m_misses++;
            return false;
        }
    }

    m_hits++;
    m_marks += (maxX - minX + 1) * (maxY - minY + 1);

    // Okay, there was no overlap so let's insert the current rectangle into
    // screenRows.

    for ( int y = minY; y <= maxY; y++ ) {
        LabelRow* row = screenRows[ y ];

        // Simplest case: an empty row
        if ( row->size() < 1 ) {
            row->append( new LabelRun( minX, maxX ) );
            m_elements++;
            continue;
        }

        // Find out our place in the universe (or row).
        // H'mm.  Maybe we could cache these numbers above.
        int i;
        for ( i = 0; i < row->size(); i++ ) {
            if ( row->at(i)->end >= minX ) break;
        }

        // i now points to first label PAST ours

        // if we are first, append or merge at start of list
        if ( i == 0 ) {
            if ( row->at(0)->start - maxX < m_minDeltaX ) {
                row->at(0)->start = minX;
            }
            else {
                row->insert( 0, new LabelRun(minX, maxX) );
                m_elements++;
            }
            continue;
        }

        // if we are past the last label, merge or append at end
        else if ( i == row->size() ) {
            if ( minX - row->at(i-1)->end < m_minDeltaX ) {
                row->at(i-1)->end = maxX;
            }
            else {
                row->append( new LabelRun(minX, maxX) );
                m_elements++;
            }
            continue;
        }

        // if we got here, we must insert or merge the new label
        //  between [i-1] and [i]

        bool mergeHead = ( minX - row->at(i-1)->end < m_minDeltaX );
        bool mergeTail = ( row->at(i)->start - maxX < m_minDeltaX );

        // double merge => combine all 3 into one
        if ( mergeHead && mergeTail ) {
            row->at(i-1)->end = row->at(i)->end;
            delete row->at( i );
            row->removeAt( i );
            m_elements--;
        }

        // Merge label with [i-1]
        else if ( mergeHead ) {
            row->at(i-1)->end = maxX;
        }

        // Merge label with [i]
        else if ( mergeTail ) {
            row->at(i)->start = minX;
        }

        // insert between the two
        else {
            row->insert( i, new LabelRun( minX, maxX) );
            m_elements++;
        }
    }

    return true;
}


void SkyLabeler::addLabel( SkyObject *obj, SkyLabeler::label_t type )
{
    bool visible = false;
    QPointF p = m_proj->toScreen(obj, true, &visible);
    if ( !visible || !m_proj->onScreen(p) || obj->translatedName().isEmpty() ) return;
    labelList[ (int)type ].append( SkyLabel( p, obj ) );
}

#ifdef KSTARS_LITE
    void SkyLabeler::addLabel(SkyObject *obj, QPointF pos,  label_t type) {
        labelList[ (int)type ].append( SkyLabel( pos, obj ) );
    }
#endif

void SkyLabeler::drawQueuedLabels()
{
    KStarsData* data = KStarsData::Instance();

    resetFont();
    m_p.setPen( QColor( data->colorScheme()->colorNamed( "PNameColor" ) ) );
    drawQueuedLabelsType( PLANET_LABEL );

    if ( labelList[ SATURN_MOON_LABEL ].size() > 0 ) {
        shrinkFont( 2 );
        drawQueuedLabelsType( SATURN_MOON_LABEL );
        resetFont();
    }

    if ( labelList[ JUPITER_MOON_LABEL ].size() > 0 ) {
        shrinkFont( 2 );
        drawQueuedLabelsType( JUPITER_MOON_LABEL );
        resetFont();
    }

    // No colors for these fellas? Just following planets along?
    drawQueuedLabelsType( ASTEROID_LABEL );
    drawQueuedLabelsType( COMET_LABEL );

    m_p.setPen( QColor( data->colorScheme()->colorNamed( "SatLabelColor" ) ) );
    drawQueuedLabelsType( SATELLITE_LABEL );

    // Whelp we're here and we don't have a Rude Label color?
    // Will just set it to Planet color since this is how it used to be!!
    m_p.setPen( QColor( data->colorScheme()->colorNamed( "PNameColor" ) ) );
    LabelList list = labelList[ RUDE_LABEL ];
    for ( int i = 0; i < list.size(); i ++ ) {
        drawRudeNameLabel( list.at(i).obj, list.at(i).o );
    }

}

void SkyLabeler::drawQueuedLabelsType( SkyLabeler::label_t type )
{
    LabelList list = labelList[ type ];
    for ( int i = 0; i < list.size(); i ++ ) {
        drawNameLabel( list.at(i).obj, list.at(i).o );
    }
}

//Rude name labels don't check for collisions with other labels,
//these get drawn no matter what.  Transient labels are rude labels.
//To mitigate confusion from possibly "underlapping" labels, paint a
//semi-transparent background.
void SkyLabeler::drawRudeNameLabel( SkyObject *obj, const QPointF &p )
{
    QString sLabel = obj->labelString();
    double offset = obj->labelOffset();
    QRectF rect = m_p.fontMetrics().boundingRect( sLabel );
    rect.moveTo( p.x()+offset, p.y()+offset );

    //Interestingly, the fontMetric boundingRect isn't where you might think...
    //We need to tweak rect to get the BG rectangle rect2
    QRectF rect2 = rect;
    rect2.moveTop( rect.top() - 0.6*rect.height() );
    rect2.setHeight( 0.8*rect.height() );

    //FIXME: Implement label background options
    QColor color( KStarsData::Instance()->colorScheme()->colorNamed( "SkyColor" ) );
    color.setAlpha( m_p.pen().color().alpha() ); //same transparency for the text and the background
    m_p.fillRect( rect2, QBrush( color ) );
    m_p.drawText( rect.topLeft(), sLabel );
}


//----- Diagnostic and information routines -----

float SkyLabeler::fillRatio()
{
    if ( m_size == 0 ) return 0.0;
    return 100.0 * float(m_marks) / float(m_size);
}

float SkyLabeler::hitRatio()
{
    if (m_hits == 0 ) return 0.0;
    return 100.0 * float(m_hits) / ( float(m_hits + m_misses) );
}

void SkyLabeler::printInfo()
{
    printf("SkyLabeler:\n");
    printf("  fillRatio=%.1f%%\n", fillRatio() );
    printf("  hits=%d  misses=%d  ratio=%.1f%%\n", m_hits, m_misses, hitRatio());
    printf("  yScale=%.1f maxY=%d\n", m_yScale, m_maxY );

    printf("  screenRows=%d elements=%d virtualSize=%.1f Kbytes\n",
           screenRows.size(), m_elements, float(m_size) / 1024.0 );

    return;

    static const char *labelName[NUM_LABEL_TYPES];

    labelName[         STAR_LABEL ] = "Star";
    labelName[     ASTEROID_LABEL ] = "Asteroid";
    labelName[        COMET_LABEL ] = "Comet";
    labelName[       PLANET_LABEL ] = "Planet";
    labelName[ JUPITER_MOON_LABEL ] = "Jupiter Moon";
    labelName[  SATURN_MOON_LABEL ] = "Saturn Moon";
    labelName[     DEEP_SKY_LABEL ] = "Deep Sky Object";
    labelName[ CONSTEL_NAME_LABEL ] = "Constellation Name";

    for ( int i = 0; i < NUM_LABEL_TYPES; i++ ) {
        printf("  %20ss: %d\n", labelName[ i ], labelList[ i ].size() );
    }

    // Check for errors in the data structure
    for (int y = 0; y <= m_maxY; y++) {
        LabelRow* row = screenRows[y];
        int size = row->size();
        if ( size < 2 ) continue;

        bool error = false;
        for (int i = 1; i < size; i++) {
            if ( row->at(i-1)->end > row->at(i)->start ) error = true;
        }
        if ( ! error ) continue;

        printf("ERROR: %3d: ", y );
        for (int i=0; i < row->size(); i++) {
            printf("(%d, %d) ", row->at(i)->start, row->at(i)->end );
        }
        printf("\n");
    }
}
