/*****************************************************************************
 * $LastChangedDate: 2010-01-24 20:49:54 -0500 (Sun, 24 Jan 2010) $
 * @file
 * @author  Jim E. Brooks  http://www.palomino3d.org
 * @brief   GUI class.
 *
 * NOTE: !!FLTK DOES _NOT_ COPY STRINGS ARGS!!
 *
 * This allows easily changing a widget's text
 * but precludes using C++ strings!
 *
 * LEGAL:   COPYRIGHT (C) 2008 JIM E. BROOKS
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE VERSION 2 (GPL 2).
 *****************************************************************************/

#define GUI_GUI_CC 1
#include "base/module.hh"
#include "base/conf_bin.hh"
#include "base/timer.hh"
using namespace base;
#include "glue/module.hh"
#include "glue/conf_bin_ext.hh"
using namespace glue;
#include "view/module.hh"
#include "view/events.hh"
using namespace view;
#include "world/module.hh"
#include "world/conf.hh"
using namespace world;
#include "program/module.hh"
using namespace program;
#include "gui/module.hh"
#include "gui/gui.hh"

#if OS_WINDOWS
#pragma comment(lib,"ws2_32.lib")  // solves linker error on Win32
#endif
#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Tabs.H>
#include <FL/Fl_Help_Dialog.H>
#include <FL/Fl_Input_Choice.H>
#include <FL/Fl_Float_Input.H>

namespace gui {

INTERN const char LOCATION_NAME_0[] = "Nepal";              // 0
INTERN const char LOCATION_NAME_1[] = "Himalayas";          // 1
INTERN const char LOCATION_NAME_2[] = "Oregon";             // 2
INTERN const char LOCATION_NAME_3[] = "Sahara";             // 3
INTERN const char LOCATION_NAME_4[] = "San Francisco";      // 4
INTERN const char LOCATION_NAME_5[] = "New York";           // 5
INTERN const char LOCATION_NAME_6[] = "Chicago";            // 6
INTERN const char LOCATION_NAME_7[] = "Grand Canyon";       // 7
INTERN const char LOCATION_NAME_8[] = "Florida";            // 8
INTERN const char LOCATION_NAME_9[] = "Antarctica";         // 9
INTERN const char LOCATION_NAME_10[] = "London";            // 10
INTERN const char LOCATION_NAME_11[] = "Mediterrean Sea";   // 11
INTERN const char LOCATION_NAME_12[] = "Japan";             // 12
INTERN const char LOCATION_NAME_13[] = "Australia";         // 13
INTERN const char LOCATION_NAME_14[] = "South America";     // 14

INTERN const char AIRCRAFT_NAME_0[]  = "Spitfire";          // 0
INTERN const char AIRCRAFT_NAME_1[]  = "F4U Corsair";       // 1
INTERN const char AIRCRAFT_NAME_2[]  = "P-51 Mustang";      // 2
INTERN const char AIRCRAFT_NAME_3[]  = "F-82 Twin Mustang"; // 3
INTERN const char AIRCRAFT_NAME_4[]  = "F-86 Sabre";        // 4
INTERN const char AIRCRAFT_NAME_5[]  = "A-4 Skyhawk";       // 5
INTERN const char AIRCRAFT_NAME_6[]  = "F-14 Tomcat";       // 6
INTERN const char AIRCRAFT_NAME_7[]  = "F-15 Eagle";        // 7
INTERN const char AIRCRAFT_NAME_8[]  = "F-16 Falcon";       // 8
INTERN const char AIRCRAFT_NAME_9[]  = "F-18 Hornet";       // 9
INTERN const char AIRCRAFT_NAME_10[] = "SR-71 Blackbird";   // 10
INTERN const char AIRCRAFT_NAME_11[] = "Mirage 2000";       // 11
INTERN const char AIRCRAFT_NAME_12[] = "SU-37 Flanker";     // 12
INTERN const char AIRCRAFT_NAME_13[] = "Space Shuttle";     // 13

#if 0
INTERN const string DEFAULT_MISSION_NAME                = "takeoff";  // -- CHANGE LUA TOO --
INTERN const int    INPUT_VALUE_TAKEOFF_OR_LAND         = 0;
#else
INTERN const string DEFAULT_MISSION_NAME                = "land";     // -- CHANGE LUA TOO --
INTERN const int    INPUT_VALUE_TAKEOFF_OR_LAND         = 1;
#endif
INTERN const string DEFAULT_AIRCRAFT_NAME_FLAT_PLANET   = "F-14 Tomcat";
INTERN const string DEFAULT_AIRCRAFT_NAME_SPHERE_PLANET = "Space Shuttle";
INTERN const string DEFAULT_AIRCRAFT_NAME               = DEFAULT_AIRCRAFT_NAME_FLAT_PLANET;
INTERN const uint   DEFAULT_AIRCRAFT_IDX_FLAT_PLANET    = 6;
INTERN const uint   DEFAULT_AIRCRAFT_IDX_SPHERE_PLANET  = 13;
INTERN const string DEFAULT_LOCATION_NAME               = "Florida";
INTERN const uint   DEFAULT_LOCATION_IDX                = 8;

INTERN const Milliseconds GUI_READ_CONF_TICK_FREQ( 500 );
INTERN int  sScoreCrashesInt = 0;
INTERN int  sScoreLandingsInt = 0;
INTERN int  sScoreTargetsDestroyedInt = 0;
INTERN char sScoreCrashesText[256]  = "Crashes = 0";
INTERN char sScoreLandingsText[256] = "Landings = 0";
INTERN char sScoreTargetsDestroyedText[256] = "Targets destroyed = 0";

////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////  Gui  //////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

Gui* Gui::msInstance;  // there can be only one

/*****************************************************************************
 * ctor/dtor.
 *****************************************************************************/
Gui::Gui( void )
:   mFrameListener(new FrameListener),
    mWindow(NULL),
    mLatWidget(NULL), mLonWidget(NULL), mAltWidget(NULL),
    mScoreCrashesWidget(NULL), mScoreLandingsWidget(NULL), mScoreTargetsDestroyedWidget(NULL),
    mReadConfBinTimestamp(0),
    mMissionName(DEFAULT_MISSION_NAME),
    mAircraftName(DEFAULT_AIRCRAFT_NAME),
    mLocationName(DEFAULT_LOCATION_NAME)
{
    // Register listener to be called AFTER rendering a frame.
    // Let OSG finish its first frame else framebuffer may be corrupted
    // (2008/06 Ubuntu X11 bug).
    EVENT_VIEW_POST_DRAW_ALL_VIEWS.Listen( mFrameListener.PTR() );
}

Gui::~Gui()
{
    EVENT_VIEW_POST_DRAW_ALL_VIEWS.Unlisten( mFrameListener.PTR() );
}

/*****************************************************************************
 * Reset fields.
 * Assign internal strings with values to be shown in GUI fields.
 *****************************************************************************/
void
Gui::Reset( void )
{
    mMissionName  = DEFAULT_MISSION_NAME;
    mAircraftName = WORLD_CONF.mFlatWorld ? DEFAULT_AIRCRAFT_NAME_FLAT_PLANET
                                          : DEFAULT_AIRCRAFT_NAME_SPHERE_PLANET;
    mLocationName = DEFAULT_LOCATION_NAME;
}

/*****************************************************************************
 * Shows the GUI.
 * This is called every time window is closed/re-opened.
 * Make the window unless it exists.
 * This may have to remake it as the user can close it.
 *****************************************************************************/
void
Gui::Show( void )
{
    // NOP if window exists (keep current fields).
    if ( mWindow != NULL )
        return;

    // Reset fields.
    Reset();

    // SUBTLE:
    // Making a window both scrollable and resizable in FTLK is very tricky!
    // FLTK owns the window objects (so the unassigned new is ok).

    const int w = 600;
    const int h = 500;
    const int page_x = 10;
    const int page_y = 50;
    const int page_w = w - 20;
    const int page_h = h - 20;
          int y = 150;
    const int heightBetweenWidgets = 40;

    mWindow = new Fl_Double_Window( w, h, "Palomino GUI" );
    mWindow->callback( reinterpret_cast<Fl_Callback*>(DestroyWindowCallback) );

    Fl_Box* hiddenBox = new Fl_Box( FL_NO_BOX, 0, 0, w, h, 0 );
    mWindow->resizable( hiddenBox );

    Fl_Tabs* tabs = new Fl_Tabs( 10, 10, w-20, h-20 );

    tabs->begin();

    Fl_Group* group = new Fl_Group( page_x, page_y, page_w, page_h, "Missions" );

    if ( GET_WORLD().IfFlat() )
    {
        group->begin();
        Fl_Input_Choice* in = new Fl_Input_Choice( 200, 100, 100, 30, "Select mission:" );
        in->add( "takeoff" );
        in->add( "land" );
        in->value( INPUT_VALUE_TAKEOFF_OR_LAND );
        in->callback( MissionCallback, reinterpret_cast<void*>(in) );
    }
    else  // OSSIM
    {
        group->begin();
        Fl_Input_Choice* in = new Fl_Input_Choice( 200, y, 150, 30, "Select location:" );
        in->add( LOCATION_NAME_0 );     // 0
        in->add( LOCATION_NAME_1 );     // 1
        in->add( LOCATION_NAME_2 );     // 2
        in->add( LOCATION_NAME_3 );     // 3
        in->add( LOCATION_NAME_4 );     // 4
        in->add( LOCATION_NAME_5 );     // 5
        in->add( LOCATION_NAME_6 );     // 6
        in->add( LOCATION_NAME_7 );     // 7
        in->add( LOCATION_NAME_8 );     // 8
        in->add( LOCATION_NAME_9 );     // 9
        in->add( LOCATION_NAME_10 );    // 10
        in->add( LOCATION_NAME_11 );    // 11
        in->add( LOCATION_NAME_12 );    // 12
        in->add( LOCATION_NAME_13 );    // 13
        in->add( LOCATION_NAME_14 );    // 14
        in->value( DEFAULT_LOCATION_IDX );
        in->callback( LocationCallback, reinterpret_cast<void*>(in) );

        {
        y += heightBetweenWidgets;
        const int w = 45;

        group->begin();
        mLatWidget = new Fl_Float_Input( 200    , y, w, 30, "Or Lat,Lon,Alt:" );

        group->begin();
        mLonWidget = new Fl_Float_Input( 200+w  , y, w, 30, "" );

        group->begin();
        mAltWidget = new Fl_Float_Input( 200+w*2, y, w, 30, "" );
        }
    }

    {
        y += heightBetweenWidgets;
        Fl_Input_Choice* in = new Fl_Input_Choice( 200, y, 150, 30, "Select aircraft:" );
        in->add( AIRCRAFT_NAME_0 );     // 0
        in->add( AIRCRAFT_NAME_1 );     // 1
        in->add( AIRCRAFT_NAME_2 );     // 2
        in->add( AIRCRAFT_NAME_3 );     // 3
        in->add( AIRCRAFT_NAME_4 );     // 4
        in->add( AIRCRAFT_NAME_5 );     // 5
        in->add( AIRCRAFT_NAME_6 );     // 6
        in->add( AIRCRAFT_NAME_7 );     // 7
        in->add( AIRCRAFT_NAME_8 );     // 8
        in->add( AIRCRAFT_NAME_9 );     // 9
        in->add( AIRCRAFT_NAME_10 );    // 10
        in->add( AIRCRAFT_NAME_11 );    // 11
        in->add( AIRCRAFT_NAME_12 );    // 12
        in->add( AIRCRAFT_NAME_13 );    // 13
        in->value( WORLD_CONF.mFlatWorld ? DEFAULT_AIRCRAFT_IDX_FLAT_PLANET
                                         : DEFAULT_AIRCRAFT_IDX_SPHERE_PLANET );
        in->callback( AircraftCallback, reinterpret_cast<void*>(in) );
    }

    {
        y += heightBetweenWidgets;
        Fl_Button* button = new Fl_Button( 200, y, 150, 30, "Restart" );
        button->callback( reinterpret_cast<Fl_Callback*>( RestartCallback ) );
    }

    // The default dark-blue color of tab is determined by "selection color"
    // (URL link color) of Help_View.  The color change here will
    // affect the URL links of FTLK 1.1.7 but not 1.1.9.
    {
    tabs->begin();
    Fl_Help_View* help = new Fl_Help_View( page_x, page_y, page_w, page_h, "Help" );
    help->selection_color( FL_DARK_CYAN );
    help->load( "doc/html/manual.html" );
    }

    // Score page.
    {
    tabs->begin();
    const int yi = 30;
    Fl_Group* group = new Fl_Group( page_x, page_y, page_w, page_h, "Score" );

    group->begin();
    mScoreCrashesWidget = new Fl_Box( 50, 100 + yi*0, 200, yi, sScoreCrashesText );

    group->begin();
    mScoreLandingsWidget = new Fl_Box( 50, 100 + yi*1, 200, yi, sScoreLandingsText );

    group->begin();
    mScoreTargetsDestroyedWidget = new Fl_Box( 50, 100 + yi*2, 200, yi, sScoreTargetsDestroyedText );
    }

    mWindow->show();
}

/*****************************************************************************
 * GUI event loop.
 *****************************************************************************/
void
Gui::FrameListener::operator()( Void )
{
    GET_GUI().ProcessEvents();
}

void
Gui::ProcessEvents( void )
{
    // Call F1::check() regardless if window is destroyed or not.
    // But do not update other parts of the GUI that assume window exists!

    Fl::check();

    if ( mWindow != NULL )
    {
        // For speed, read ConfBin only a few times a second.
        const Milliseconds elapsedTime = GET_TIMER().GetElapsedTime();
        if ( elapsedTime - mReadConfBinTimestamp > GUI_READ_CONF_TICK_FREQ )
        {
            mReadConfBinTimestamp = elapsedTime;  // update timestamp
            ReadConfBin();
        }
    }
}

/*****************************************************************************
 * Update from data in binary configuration.
 *****************************************************************************/
void
Gui::ReadConfBin( void )
{
    // Read player's score/stats from the binary configuration (~/.palomino.cfg).
    // Show this info in notebook page of GUI.

#define GUI_SCORE_LINE_INT( WIDGET, INTERN_VAR, INTERN_STRING, FORMAT, KEY )    \
{                                                                               \
    int var = 0;                                                                \
    confBin.Read( KEY, var );                                                   \
    /* Changed? */                                                              \
    if ( INTERN_VAR != var )                                                    \
    {                                                                           \
        INTERN_VAR = var;   /* remember new value */                            \
        sprintf( INTERN_STRING, FORMAT, var );                                  \
      /*WIDGET->redraw();*/  /* causes font artifacts */                        \
        redraw = true;  /* need to redraw GUI to show new info */               \
    }                                                                           \
}
    ConfBin& confBin = glue::GetConfBin();
    bool redraw = false;
    GUI_SCORE_LINE_INT( mScoreCrashesWidget, sScoreCrashesInt, sScoreCrashesText,
                        "Crashes = %d", "game_score_crashes" );
    GUI_SCORE_LINE_INT( mScoreLandingsWidget, sScoreLandingsInt, sScoreLandingsText,
                        "Landings = %d", "game_score_landings" );
    GUI_SCORE_LINE_INT( mScoreTargetsDestroyedWidget, sScoreTargetsDestroyedInt, sScoreTargetsDestroyedText,
                        "Targets destroyed = %d", "game_score_targets_destroyed" );
    if ( redraw )  // redraw window if new info appeared in binary conf
        mWindow->redraw();
}

/*****************************************************************************
 * Callbacks.
 * GET_GUI() has to be called since these are CLASS_METHOD.
 *****************************************************************************/
void
Gui::DestroyWindowCallback( Fl_Widget*, void* )
{
    if ( GET_GUI().mWindow != NULL )
    {
        // Destroy window.
        Fl::delete_widget( GET_GUI().mWindow );

        // Nullify pointer to allow window to be re-created.
        GET_GUI().mWindow = NULL;
    }
}

#define SET_GUI_CMD_ARG( MEMBER_NAME )                                      \
{                                                                           \
    Fl_Input_Choice* in = reinterpret_cast<Fl_Input_Choice*>( data );       \
    if ( in != NULL and in->value() != NULL )                               \
    {                                                                       \
        /* Call GET_GUI() since this is a class method. */                  \
        GET_GUI().MEMBER_NAME = in->value();                                \
    }                                                                       \
    else                                                                    \
    {                                                                       \
        throw Exception( "[Gui] FLTK InputChoice failed" );                 \
    }                                                                       \
}

void
Gui::MissionCallback( Fl_Widget*, void* data )
{
    SET_GUI_CMD_ARG( mMissionName );
}

void
Gui::AircraftCallback( Fl_Widget*, void* data )
{
    SET_GUI_CMD_ARG( mAircraftName );
}

void
Gui::LocationCallback( Fl_Widget*, void* data )
{
    SET_GUI_CMD_ARG( mLocationName );
}

string
Gui::GetWidgetVal( SafePtr<Fl_Float_Input> widget )
{
    if ( (widget != NULL) and (widget->value() != NULL) )
        return widget->value();
    else
        return string();
}

void
Gui::RestartCallback( Fl_Widget*, void* )
{
    const string lat = GetWidgetVal( GET_GUI().mLatWidget );
    const string lon = GetWidgetVal( GET_GUI().mLonWidget );
    const string alt = GetWidgetVal( GET_GUI().mAltWidget );

    // Separate with commas.
    string cmd = "RESTART"
               + string(",") + GET_GUI().mMissionName
               + string(",") + GET_GUI().mAircraftName
               + string(",") + GET_GUI().mLocationName
               + string(",") + lat
               + string(",") + lon
               + string(",") + alt;
    GET_PROGRAM().EnqueueCommand( cmd );
}

} // namespace gui
