// Cytosim was created by Francois Nedelec. Copyright 2021 Cambridge University

#include "meca.h"
#include "modulo.h"
#include "glossary.h"
#include "simul_prop.h"
#include "print_color.h"
#include "display1.h"
#include "display2.h"
#include "display3.h"
#include "save_image_gl.h"
#include "time_date.h"
#include <unistd.h>
#include <cstdlib>
#include "filepath.h"
#include "gym_check.h"

extern void helpKeys(std::ostream&);

//------------------------------------------------------------------------------
#pragma mark -

void Player::setStyle(const unsigned style)
{
    if ( mDisplay )
    {
        //restore the previous OpenGL state
        glPopAttrib();
        delete(mDisplay);
        mDisplay = nullptr;
    }
    
    //save the current OpenGL state
    glPushAttrib(GL_ALL_ATTRIB_BITS);
    
    switch ( style )
    {
        default:
        case 1: mDisplay = new Display1(&disp);  break;
        case 2: mDisplay = new Display2(&disp);  break;
        case 3: mDisplay = new Display3(&disp);  break;
    }
    disp.style = style;

    //initialize Views associated with opened GLUT windows:
    for ( size_t n = 1; n < glApp::views.size(); ++n )
    {
        View & view = glApp::views[n];
        if ( view.window() > 0 )
        {
            view.initGL();
            view.loadViewport();
        }
    }
}


/**
 Build a message containing the label and the time.
 In live mode, it also adds 'Live' or it indicates the frame index,
 and the force generated by the mouse-controlled Single.
 */
std::string Player::buildLabel() const
{
    std::ostringstream oss;
    oss.precision(3);
    oss << std::setw(8) << std::fixed << simul.time() << "s";
    
    //display the force exerted by the mouse-controled Single:
    Single const* sh = worker.handle();
    if ( sh && sh->attached() )
        oss << "\nHandle: " << sh->force().norm() << "pN";

    if ( goLive && worker.alive() )
    {
        oss << "\nLive";
        //display ratio number-of-time-step / frame
        if ( prop.period > 1 )
            oss << " " << prop.period;
#if ( 0 )
        // display speedup compared to clock time
        static double sec = TimeDate::seconds_today();
        static double sim = 0, spd = 0;
        double SEC = TimeDate::seconds_today();
        if ( SEC > sec + 1.0 )
        {
            double SIM = simul.time();
            spd = ( SIM - sim ) / ( SEC - sec );
            sim = SIM;
            sec = SEC;
        }
        oss << std::setw(8) << std::fixed << spd << "x ";
#endif
    }
    else if ( worker.currentFrame() > 0 )
    {
        oss << "\nFrame " << worker.currentFrame();
    }

    return oss.str();
}


/**
 Compute information to be displayed in the top corner of the window.
 */
std::string Player::buildReport(std::string arg) const
{
    if ( ! arg.empty() )
    {
        Glossary glos;
        glos.define("verbose", "1");
        std::string res;
        try {
            std::stringstream ss;
            simul.poly_report(ss, arg, glos, -1);
            res = ss.str();
        }
        catch ( Exception& e )
        {
            res = e.info();
        }
        // remove extra new-line
        if ( res.size() > 1  &&  res.at(0) == '\n' )
            return res.substr(1);
        return res;
    }
    return "";
}

/**
 This text is normally displayed in the center of the window
 */
std::string Player::buildMemo(int type) const
{
    std::ostringstream oss;
    switch ( type )
    {
        case 1: return "Please, visit www.cytosim.org";
        case 2: helpKeys(oss); break;
        case 3: glApp::help(oss); break;
        case 4: writePlayParameters(oss, true); break;
        case 5: writeDisplayParameters(oss, true); break;
        default: return "";
    }
    return oss.str();
}

//------------------------------------------------------------------------------
#pragma mark - Display

void Player::autoFocus(View& view, Simul const& sim, unsigned mode) const
{
    real mat[9] = { 1, 0, 0, 0, 1, 0, 0, 0, 1 };
    
    if ( mode & 1 )
    {
        Vector M, G, P;
        FiberSet::infoPosition(sim.fibers.collect(), M, G, P);
        view.move_shift(Vector3(G));
        //std::clog << "auto center: " << G << '\n';
    }
    
    if ( mode & 2 )
    {
        // align with mean nematic direction
        static Vector3 dir(1, 0, 0);
        ObjectList objs = sim.fibers.collect();
        real S = FiberSet::infoNematic(objs, mat);
        Vector3 vec(mat);

        Space const* spc = sim.spaces.master();
        if ( spc && spc->prop->shape== "sphere" )
        {
            real T = FiberSet::infoOrthoNematic(objs, mat, spc);
            flashText("Nematic order %5.3f %5.3f", S, T);
        }
        else
            flashText("Nematic order %5.3f", S);
        if ( S > 0.1 )
        {
            // align `vec` to `dir`:
            vec *= std::copysign(1.0, dot(dir, vec));
            // switch between immediate alignment (default) versus gradual:
            if ( goLive && ( mode & 16 ))
            {
                // time-average the direction vector:
                dir = normalize( dir * 20 + vec );
                if ( glApp::mouseS ) // only track if mouse button is up!
                    view.align_with(dir, false, 0.01);
            }
            else
            {
                dir = vec;
                view.align_with(dir, false, 1.0);
            }
        }
    }

    if ( mode & 4 )
    {
        // align with the main axis of the cloud of fiber-vertices
        real sum = 0;
        real avg[3] = { 0 };
        real mom[9] = { 0 };
        FiberSet::infoComponents(sim.fibers.collect(), sum, avg, mom, mat);
        view.rotation.setFromMatrix3(mat);
        view.rotation.conjugate(); // inverse rotation
        //std::clog << "auto quat: " << view.rotation << '\n';
    }

    if ( mode & 8 )
    {
        // track mean position of Solid
        size_t cnt = 0;
        Vector pos(0,0,0);
        for ( Solid const* B=sim.solids.first(); B; B=B->next() )
        {
            ++cnt;
            pos += B->position();
        }
        if ( cnt > 0 )
            view.move_shift(Vector3(pos/cnt));
    }
}


//set pixel size, unit-size and direction:
void Player::setPixelSize(View& view)
{
    float mag = view.magnify;
    float pix = view.pixelSize() / mag;
    /*
     if `disp.point_value` is set, widths of lines and point sizes are understood to
     be specified in 'real' units, while by default, they are understood in pixels.
     */
    if ( disp.point_value > 0 )
        mag = disp.point_value / pix;
    
    mDisplay->setParameters(pix, mag, view.depthAxis());
    mDisplay->setStencil(view.stencil);

    for ( Property * p : mPointDisp )
        static_cast<PointDisp*>(p)->setPixels(pix, mag, disp.style==2);
}


void Player::prepareDisplay(View& view)
{    
    CHECK_GL_ERROR("before prepareDisplay");
    //-------- automatic adjustment of viewing area:

    if ( view.auto_scale > 0 )
        view.set_scale(2*simul.spaces.maxExtension());

    //-------- auto-track:
    
    if ( view.track_fibers )
        autoFocus(view, simul, view.track_fibers);
    
    //-------- texts:
    
    view.setLabel(buildLabel());
    view.setMemo(buildMemo(view.draw_memo));
    view.setMessage(buildReport(prop.report));

    //-------- set pixel size, unit-size and direction:

    setPixelSize(view);
    
    //-------- adjust reference color used by gle::bright_color
    gym::background_color = view.back_color;

    CHECK_GL_ERROR("in prepareDisplay");

    try {
        
        if ( mDisplay->prop->fold )
            simul.foldPositions();

        if ( simul.fresh_ )
        {
            mDisplay->prepareDrawing(simul, mFiberDisp, mPointDisp);
            //std::clog << " allDisp.size() = " << allDisp.size() << '\n';
            simul.fresh_ = 0;
        }
    }
    catch(Exception & e)
    {
        print_blue(stderr, e.brief());
        std::cerr << e.info() << '\n';
    }
}

//------------------------------------------------------------------------------
void Player::drawCytosim()
{
#if 0
    static double sec = TimeDate::milliseconds();
    double now = TimeDate::milliseconds();
    fprintf(stderr, "drawCytosim(%8.3f) %.2f msec\n", simul.time(), now-sec);
    sec = now;
#endif
    try {
        // draw main elements:
        if ( modulo && disp.tile )
            mDisplay->drawTiled(simul, disp.tile);
        else
            mDisplay->drawSimul(simul);

        mDisplay->drawMisc(simul);
    }
    catch(Exception & e)
    {
        print_blue(stderr, e.brief());
        std::cerr << e.info() << " (display)\n";
    }
}


void Player::readDisplayString(View& view, std::string const& str)
{
    //std::clog << "readDisplayString " << str << '\n';
    try
    {
        Glossary glos(str);
        disp.read(glos);
        prop.read(glos);
        view.read(glos);
    }
    catch( Exception & e )
    {
        print_blue(stderr, e.brief());
        std::cerr << e.info() << " (simul:display)\n";
    }
}


/**
 Display the full Scene for export, skipping some features
 */
void Player::drawSystem(View& view)
{
    CHECK_GL_ERROR("drawSystem");
    view.openDisplay();
    drawCytosim();
    view.drawTextAndScale(simul.text_);
    view.closeDisplay();

    if ( prop.save_images > 0 )
    {
        double t = simul.time();
        if ( prop.saved_image_time != t )
        {
            prop.saved_image_time = t;
            size_t frame_index = worker.currentFrame();
            if ( goLive )
                frame_index = prop.image_index++;
            saveView(view, frame_index, prop.downsample);
            // exit if this was the last image requested:
            if ( --prop.save_images == 0 && ( prop.auto_exit & 2 ))
            {
                printf("\n");
                exit(EXIT_SUCCESS);
            }
        }
    }
}


//------------------------------------------------------------------------------
#pragma mark - Export Image

/**
 Export image from the current OpenGL back buffer,
 in the format specified by 'PlayerProp::image_format',
 in the current working directory
 */
int Player::saveView(View const& view, const char* filename, const char* format, int downsample) const
{
    GLint vp[4] = { 0 };
    view.copyViewport(vp);
    //printf("Player::viewport %ix%i\n", vp[2], vp[3]);
    int err = SaveImage::saveImage(filename, format, vp, downsample);
    if ( err == 0 && simul.prop.verbose > 0 )
    {
        printf("\r saved %ix%i snapshot %s    ", vp[2]/downsample, vp[3]/downsample, filename);
        fflush(stdout);
    }
    return err;
}


/**
 Attempts to build an image file name from the elements
 */
void fixFileName(char str[], size_t len, const char format[], size_t indx)
{
    static int virgin = true;
    // remove file extension:
    char* ptr = strchr(str, '.');
    if ( ptr ) *ptr = 0;
    // check for an integer printf() pattern:
    ptr = strchr(str, '%');
    // add a number if a file was already saved:
    if ( !ptr && !virgin )
        ptr = str + strlen(str);
    // insert number:
    if ( ptr )
        ptr += snprintf(ptr, len-(ptr-str), "%04lu", indx);
    else
        ptr = str + strlen(str);
    // add file extension:
    if ( ptr - str + strlen(format) < len )
    {
        *ptr++ = '.';
        strcpy(ptr, format);
    }
    virgin = false;
}
    
/**
 Save image from the current OpenGL back buffer,
 in the format specified by `PlayerProp::image_format`,
 in the folder specified in `PlayerProp::image_dir`.
 The file name is derived from `PlayerProp::image_name` by including `indx`.
 */
int Player::saveView(View& view, size_t indx, int downsample) const
{
    char str[1024] = { 0 };
    char const* name = prop.image_name.c_str();
    char const* format = prop.image_format.c_str();
    strncpy(str, name, sizeof(str));
    fixFileName(str, sizeof(str), format, indx);
    int cwd = FilePath::change_dir(prop.image_dir, true);
    int err = saveView(view, str, format, downsample);
    FilePath::change_dir(cwd);
    
    if ( prop.auto_pilot )
    {
        view.zoom_in(prop.auto_zoom);
        view.rotate_by(prop.auto_rotate);
    }
    return err;
}


//------------------------------------------------------------------------------

static void drawFunc(int mag, void * arg)
{
    static_cast<Player*>(arg)->drawCytosim();
    CHECK_GL_ERROR("in drawFunc");
}


/**
 save an image where the resolution is magnified by a factor `mag`.
 This requires access to the simulation world.
 */
int Player::saveScene(const int mag, const char* name, const char* format, const int downsample)
{
    View & view = glApp::currentView();
    const int W = view.width(), H = view.height();
    worker.lock();
    float m = view.magnify;
    view.magnify = mag;
    //std::clog << "saveMagnifiedImage " << W << "x" << H << " mag=" << mag << '\n';
    prepareDisplay(view);
    view.openDisplay();
    int err = SaveImage::saveMagnifiedImage(mag, name, format, W, H, drawFunc, this, downsample);
    if ( err )
        err = SaveImage::saveCompositeImage(mag, name, format, W, H, view.pixelSize(), drawFunc, this, downsample);
    if ( !err )
        printf("saved %ix%i snapshot %s\n", mag*W/downsample, mag*H/downsample, name);
    view.closeDisplay();
    view.magnify = m;
    
    worker.unlock();
    return err;
}


/**
 save an image where the resolution is magnified by a factor `mag`.
 This requires access to the simulation world.
 */
int Player::saveScene(const int mag, const char* root, unsigned indx, const int downsample)
{
    char str[1024] = { 0 };
    char const* format = prop.image_format.c_str();
    snprintf(str, sizeof(str), "%s%04i.%s", root, indx, format);
    int cwd = FilePath::change_dir(prop.image_dir, true);
    int err = saveScene(mag, str, format, downsample);
    FilePath::change_dir(cwd);
    glApp::postRedisplay();
    return err;
}

