/**
 * @file llconsole.cpp
 * @brief a scrolling console output device
 *
 * $LicenseInfo:firstyear=2001&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2010, Linden Research, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License only.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 */

//#include "llviewerprecompiledheaders.h"
#include "linden_common.h"

#include "llconsole.h"

// linden library includes
#include "llmath.h"
//#include "llviewercontrol.h"
#include "llcriticaldamp.h"
#include "llfontgl.h"
#include "llgl.h"
#include "llui.h"
#include "lluiimage.h"
//#include "llviewerimage.h"
//#include "llviewerimagelist.h"
//#include "llviewerwindow.h"
#include "llsd.h"
#include "llfontgl.h"
#include "llmath.h"

//#include "llstartup.h"

// Used for LCD display
extern void AddNewDebugConsoleToLCD(const LLWString &newLine);

LLConsole* gConsole = NULL;  // Created and destroyed in LLViewerWindow.

const F32 FADE_DURATION = 2.f;

static LLDefaultChildRegistry::Register<LLConsole> r("console");

LLConsole::LLConsole(const LLConsole::Params& p)
:   LLUICtrl(p),
    LLFixedBuffer(p.max_lines),
    mLinePersistTime(p.persist_time), // seconds
    mFont(p.font),
    mConsoleWidth(0),
    mConsoleHeight(0)
{
    if (p.font_size_index.isProvided())
    {
        setFontSize(p.font_size_index);
    }
    mFadeTime = mLinePersistTime - FADE_DURATION;
    setMaxLines(LLUI::getInstance()->mSettingGroups["config"]->getS32("ConsoleMaxLines"));
}

void LLConsole::setLinePersistTime(F32 seconds)
{
    mLinePersistTime = seconds;
    mFadeTime = mLinePersistTime - FADE_DURATION;
}

void LLConsole::reshape(S32 width, S32 height, bool called_from_parent)
{
    S32 new_width = llmax(50, llmin(getRect().getWidth(), width));
    S32 new_height = llmax(mFont->getLineHeight() + 15, llmin(getRect().getHeight(), height));

    if (   mConsoleWidth == new_width
        && mConsoleHeight == new_height )
    {
        return;
    }

    mConsoleWidth = new_width;
    mConsoleHeight= new_height;

    LLUICtrl::reshape(new_width, new_height, called_from_parent);

    for(paragraph_t::iterator paragraph_it = mParagraphs.begin(); paragraph_it != mParagraphs.end(); paragraph_it++)
    {
        (*paragraph_it).updateLines((F32)getRect().getWidth(), mFont, true);
    }
}

void LLConsole::setFontSize(S32 size_index)
{
    if (-1 == size_index)
    {
        mFont = LLFontGL::getFontMonospace();
    }
    else if (0 == size_index)
    {
        mFont = LLFontGL::getFontSansSerif();
    }
    else if (1 == size_index)
    {
        mFont = LLFontGL::getFontSansSerifBig();
    }
    else
    {
        mFont = LLFontGL::getFontSansSerifHuge();
    }
    // Make sure the font exists
    if (mFont == NULL)
    {
        mFont = LLFontGL::getFontDefault();
    }

    for(paragraph_t::iterator paragraph_it = mParagraphs.begin(); paragraph_it != mParagraphs.end(); paragraph_it++)
    {
        (*paragraph_it).updateLines((F32)getRect().getWidth(), mFont, true);
    }
}

void LLConsole::draw()
{
    // Units in pixels
    static const F32 padding_horizontal = 10;
    static const F32 padding_vertical = 3;
    LLGLSUIDefault gls_ui;

    // skip lines added more than mLinePersistTime ago
    F32 cur_time = mTimer.getElapsedTimeF32();

    F32 skip_time = cur_time - mLinePersistTime;
    F32 fade_time = cur_time - mFadeTime;

    if (mParagraphs.empty())    //No text to draw.
    {
        return;
    }

    size_t num_lines{ 0 };

    paragraph_t::reverse_iterator paragraph_it;
    paragraph_it = mParagraphs.rbegin();
    auto paragraph_num=mParagraphs.size();

    while (!mParagraphs.empty() && paragraph_it != mParagraphs.rend())
    {
        num_lines += (*paragraph_it).mLines.size();
        if(num_lines > mMaxLines
            || ( (mLinePersistTime > (F32)0.f) && ((*paragraph_it).mAddTime - skip_time)/(mLinePersistTime - mFadeTime) <= (F32)0.f))
        {                           //All lines above here are done.  Lose them.
            for (size_t i = 0; i < paragraph_num; i++)
            {
                if (!mParagraphs.empty())
                    mParagraphs.pop_front();
            }
            break;
        }
        paragraph_num--;
        paragraph_it++;
    }

    if (mParagraphs.empty())
    {
        return;
    }

    // draw remaining lines
    F32 y_pos = 0.f;

    LLUIImagePtr imagep = LLUI::getUIImage("transparent");

    static LLCachedControl<F32> console_bg_opacity(*LLUI::getInstance()->mSettingGroups["config"], "ConsoleBackgroundOpacity", 0.7f);
    F32 console_opacity = llclamp(console_bg_opacity(), 0.f, 1.f);

    static LLUIColor console_color = LLUIColorTable::instance().getColor("ConsoleBackground");
    LLColor4 color = console_color;
    color.mV[VALPHA] *= console_opacity;

    F32 line_height = (F32)mFont->getLineHeight();

    for(paragraph_it = mParagraphs.rbegin(); paragraph_it != mParagraphs.rend(); paragraph_it++)
    {
        S32 target_height = llfloor( (*paragraph_it).mLines.size() * line_height + padding_vertical);
        S32 target_width =  llfloor( (*paragraph_it).mMaxWidth + padding_horizontal);

        y_pos += ((*paragraph_it).mLines.size()) * line_height;
        imagep->drawSolid(-14, (S32)(y_pos + line_height - target_height), target_width, target_height, color);

        F32 y_off=0;

        F32 alpha;

        if ((mLinePersistTime > 0.f) && ((*paragraph_it).mAddTime < fade_time))
        {
            alpha = ((*paragraph_it).mAddTime - skip_time)/(mLinePersistTime - mFadeTime);
        }
        else
        {
            alpha = 1.0f;
        }

        if( alpha > 0.f )
        {
            for (lines_t::iterator line_it=(*paragraph_it).mLines.begin();
                    line_it != (*paragraph_it).mLines.end();
                    line_it ++)
            {
                for (line_color_segments_t::iterator seg_it = (*line_it).mLineColorSegments.begin();
                        seg_it != (*line_it).mLineColorSegments.end();
                        seg_it++)
                {
                    mFont->render((*seg_it).mText, 0, (*seg_it).mXPosition - 8, y_pos -  y_off,
                        LLColor4(
                            (*seg_it).mColor.mV[VRED],
                            (*seg_it).mColor.mV[VGREEN],
                            (*seg_it).mColor.mV[VBLUE],
                            (*seg_it).mColor.mV[VALPHA]*alpha),
                        LLFontGL::LEFT,
                        LLFontGL::BASELINE,
                        LLFontGL::NORMAL,
                        LLFontGL::DROP_SHADOW,
                        S32_MAX,
                        target_width
                        );
                }
                y_off += line_height;
            }
        }
        y_pos  += padding_vertical;
    }
}

//Generate highlight color segments for this paragraph.  Pass in default color of paragraph.
void LLConsole::Paragraph::makeParagraphColorSegments (const LLColor4 &color)
{
    LLSD paragraph_color_segments;
    paragraph_color_segments[0]["text"] =wstring_to_utf8str(mParagraphText);
    LLSD color_sd = color.getValue();
    paragraph_color_segments[0]["color"]=color_sd;

    for(LLSD::array_const_iterator color_segment_it = paragraph_color_segments.beginArray();
        color_segment_it != paragraph_color_segments.endArray();
        ++color_segment_it)
    {
        LLSD color_llsd = (*color_segment_it)["color"];
        std::string color_str  = (*color_segment_it)["text"].asString();

        ParagraphColorSegment color_segment;

        color_segment.mColor.setValue(color_llsd);
        color_segment.mNumChars = static_cast<S32>(color_str.length());

        mParagraphColorSegments.push_back(color_segment);
    }
}

//Called when a paragraph is added to the console or window is resized.
void LLConsole::Paragraph::updateLines(F32 screen_width, const LLFontGL* font, bool force_resize)
{
    if ( !force_resize )
    {
        if ( mMaxWidth >= 0.0f
         &&  mMaxWidth < screen_width )
        {
            return;                 //No resize required.
        }
    }

    screen_width = screen_width - 30;   //Margin for small windows.

    if (    mParagraphText.empty()
        || mParagraphColorSegments.empty()
        || font == NULL)
    {
        return;                 //Not enough info to complete.
    }

    mLines.clear();             //Chuck everything.
    mMaxWidth = 0.0f;

    paragraph_color_segments_t::iterator current_color = mParagraphColorSegments.begin();
    U32 current_color_length = (*current_color).mNumChars;

    S32 paragraph_offset = 0;           //Offset into the paragraph text.

    // Wrap lines that are longer than the view is wide.
    while( paragraph_offset < (S32)mParagraphText.length() &&
           mParagraphText[paragraph_offset] != 0)
    {
        S32 skip_chars; // skip '\n'
        // Figure out if a word-wrapped line fits here.
        LLWString::size_type line_end = mParagraphText.find_first_of(llwchar('\n'), paragraph_offset);
        if (line_end != LLWString::npos)
        {
            skip_chars = 1; // skip '\n'
        }
        else
        {
            line_end = mParagraphText.size();
            skip_chars = 0;
        }

        U32 drawable = font->maxDrawableChars(mParagraphText.c_str()+paragraph_offset, screen_width, static_cast<S32>(line_end) - paragraph_offset, LLFontGL::WORD_BOUNDARY_IF_POSSIBLE);

        if (drawable != 0)
        {
            F32 x_position = 0;                     //Screen X position of text.

            mMaxWidth = llmax( mMaxWidth, (F32)font->getWidth( mParagraphText.substr( paragraph_offset, drawable ).c_str() ) );
            Line line;

            U32 left_to_draw = drawable;
            U32 drawn = 0;

            while (left_to_draw >= current_color_length
                && current_color != mParagraphColorSegments.end() )
            {
                LLWString color_text = mParagraphText.substr( paragraph_offset + drawn, current_color_length );
                line.mLineColorSegments.push_back( LineColorSegment( color_text,            //Append segment to line.
                                                (*current_color).mColor,
                                                x_position ) );

                x_position += font->getWidth( color_text.c_str() ); //Set up next screen position.

                drawn += current_color_length;
                left_to_draw -= current_color_length;

                current_color++;                            //Goto next paragraph color record.

                if (current_color != mParagraphColorSegments.end())
                {
                    current_color_length = (*current_color).mNumChars;
                }
            }

            if (left_to_draw > 0 && current_color != mParagraphColorSegments.end() )
            {
                    LLWString color_text = mParagraphText.substr( paragraph_offset + drawn, left_to_draw );

                    line.mLineColorSegments.push_back( LineColorSegment( color_text,        //Append segment to line.
                                                    (*current_color).mColor,
                                                    x_position ) );

                    current_color_length -= left_to_draw;
            }
            mLines.push_back(line);                             //Append line to paragraph line list.
        }
        paragraph_offset += (drawable + skip_chars);
    }
}

//Pass in the string and the default color for this block of text.
LLConsole::Paragraph::Paragraph (LLWString str, const LLColor4 &color, F32 add_time, const LLFontGL* font, F32 screen_width)
:   mParagraphText(str), mAddTime(add_time), mMaxWidth(-1)
{
    makeParagraphColorSegments(color);
    updateLines( screen_width, font );
}

// called once per frame regardless of console visibility
// static
void LLConsole::updateClass()
{
    for (auto& con : instance_snapshot())
    {
        con.update();
    }
}

void LLConsole::update()
{
    {
        LLCoros::LockType lock(mMutex);

        while (!mLines.empty())
        {
            mParagraphs.push_back(
                Paragraph(  mLines.front(),
                            LLColor4::white,
                            mTimer.getElapsedTimeF32(),
                            mFont,
                            (F32)getRect().getWidth()));
            mLines.pop_front();
        }
    }

    // remove old paragraphs which can't possibly be visible any more.  ::draw() will do something similar but more conservative - we do this here because ::draw() isn't guaranteed to ever be called!  (i.e. the console isn't visible)
    while ((S32)mParagraphs.size() > llmax((S32)0, (S32)(mMaxLines)))
    {
            mParagraphs.pop_front();
    }
}