/*
    Copyright (C) 1998 by Jorrit Tyberghein
    Metaballs Demo (C) 1999 by Denis Dmitriev

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

    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
    Library General Public License for more details.

    You should have received a copy of the GNU Library General Public
    License along with this library; if not, write to the Free
    Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#include <stdarg.h>
#include <string.h>

#include "sysdef.h"

#include "itxtmgr.h"
#include "igraph3d.h"
#include "csutil/inifile.h"
#include "csengine/world.h"
#include "csengine/texture.h"
#include "csparser/csloader.h"
#include "apps/metademo/meta.h"
#include "cssys/common/system.h"
#include "csengine/pol2d.h"
#include "csengine/polygon.h"

static MetaDemo* Sys = NULL;

#define FRAME_WIDTH Sys->FrameWidth
#define FRAME_HEIGHT Sys->FrameHeight

#define MAP_RESOLUTION  256
static float asin_table[2*MAP_RESOLUTION+1];

static char texture_name[256];

static int text_fg;

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

void cleanup()
{
  CHK (delete Sys);
  CHK (delete config);
}

void debug_dump ()
{
}

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

MetaDemo::MetaDemo ()
{
  tm=NULL;
  world=NULL;

  CHK (poly = new G3DPolygonDPFX ());
  memset(poly,0,sizeof(*poly));
  poly->num=3;

  alpha=frame=num_frames=0;
  d_alpha=config->GetFloat("MetaDemo","INITIAL_SPEED",0.03);

  num_meta_balls=config->GetInt("MetaDemo","NUM_META_BALLS",3);
  
  iso_level=config->GetFloat("MetaDemo","ISO_LEVEL",1.0);
  max_triangles=config->GetInt("MetaDemo","MAX_TRIANGLES",2000);

  env_map_mult=config->GetFloat("MetaDemo","ENV_MAP_MULTIPLIER",1.0);
  env_mapping=config->GetYesNo("MetaDemo","USE_TRUE_ENV_MAP",true)?
    TRUE_ENV_MAP:FAKE_ENV_MAP;

  CHK(meta_balls=new MetaBall[num_meta_balls]);
  CHK(triangles_array=new Triangle[max_triangles]);

  charge=config->GetFloat("MetaDemo","CHARGE",3.5);

  InitTables();
}

MetaDemo::~MetaDemo ()
{
  char buf[256];

  sprintf(buf,"%.2f",d_alpha);
  config->SetStr("MetaDemo","INITIAL_SPEED",buf);
  sprintf(buf,"%d",num_meta_balls);
  config->SetStr("MetaDemo","NUM_META_BALLS",buf);
  sprintf(buf,"%.2f",env_map_mult);
  config->SetStr("MetaDemo","ENV_MAP_MULTIPLIER",buf);
  sprintf(buf,"%s",env_mapping==TRUE_ENV_MAP?"Yes":"No");
  config->SetStr("MetaDemo","USE_TRUE_ENV_MAP",buf);

  /*
   * These values can't be changed yet from MetaDemo, but could be
   *  in the future. So I save them too
   */
  sprintf(buf,"%g",iso_level);
  config->SetStr("MetaDemo","ISO_LEVEL",buf);
  sprintf(buf,"%d",max_triangles);
  config->SetStr("MetaDemo","MAX_TRIANGLES",buf);
  sprintf(buf,"%.2f",charge);
  config->SetStr("MetaDemo","CHARGE",buf);
  
  config->Save("metademo.cfg");

  CHK (delete world);

  CHK (delete[] triangles_array);
  CHK (delete[] meta_balls);
  CHK (delete poly);
}

void MetaDemo::Help()
{
  SysSystemDriver::Help();
  Printf(MSG_STDOUT,"  -texture <name>    "
    "texture to map onto meta balls (default=stone4.gif)\n");
}

void MetaDemo::SetSystemDefaults()
{
  SysSystemDriver::SetSystemDefaults();
  strcpy(texture_name,"stone4.gif");
}

bool MetaDemo::ParseArg(int argc, char* argv[], int& i)
{
  bool okay = true;
  if (strcasecmp("-texture", argv[i]) == 0)
  {
    if (++i < argc)
    {
      strncpy(texture_name, argv[i], sizeof(texture_name) - 1);
      texture_name[sizeof(texture_name) - 1] = '\0';
    }
  }
  else
    okay = SysSystemDriver::ParseArg(argc, argv, i);
  return okay;
}

float MetaDemo::map(float x)
{
  return asin_table[(int)(MAP_RESOLUTION*(1+x))];
}

void MetaDemo::InitTables(void)
{
  for(int i=-MAP_RESOLUTION,j=0;i<=MAP_RESOLUTION;i++,j++)
  {
    float c=1.0*i/MAP_RESOLUTION;

    switch(env_mapping)
    {
      case TRUE_ENV_MAP:
        asin_table[j]=env_map_mult*(0.5+asin(c)/M_PI);
        break;
      case FAKE_ENV_MAP:
        asin_table[j]=0.5*env_map_mult*(1+c);
        break;
    }
  }
}

void MetaDemo::InitApp ()
{
  // Open the main system. This will open all the previously loaded
  // COM drivers.
  if(!Open("Metaballs Crystal Space Application"))
  {
    Printf(MSG_FATAL_ERROR,"Error opening system!\n");
    cleanup();
    exit(1);
  }

  ITextureManager* txtmgr;
  piG3D->GetTextureManager (&txtmgr);
  txtmgr->SetVerbose (true);

  // Initialize our world.
  world->Initialize (GetISystemFromSystem (this), piG3D, config);

  CSLoader::LoadLibrary (world, "standard", "standard.zip");
  tm=CSLoader::LoadTexture (world, "stone", texture_name);

  world->Prepare (piG3D);
  txtmgr->AllocPalette();

  txtmgr->FindRGB(255,255,255,text_fg);

  h_height=FRAME_HEIGHT/2;
  h_width=FRAME_WIDTH/2;
}

void MetaDemo::eatkeypress (int key, bool shift, bool alt, bool ctrl, float /*elapsed_time*/)
{
  (void)shift; (void)alt; (void)ctrl;

  switch (key)
  {
    case CSKEY_LEFT:  d_alpha-=0.01; break;
    case CSKEY_RIGHT: d_alpha+=0.01; break;
    case CSKEY_ESC:   System->Shutdown=true; break;
    case CSKEY_UP:    env_map_mult+=0.01; InitTables(); break;
    case CSKEY_DOWN:  env_map_mult-=0.01; InitTables(); break;
    case CSKEY_TAB:   env_mapping=!env_mapping; InitTables(); break;

    case CSKEY_PGUP:
      delete[] meta_balls;

      num_meta_balls++;
      meta_balls=new MetaBall[num_meta_balls];

      break;

    case CSKEY_PGDN:
      if(num_meta_balls)
      {
        delete[] meta_balls;

        num_meta_balls--;
        meta_balls=new MetaBall[num_meta_balls];
      }
      break;
  }
}

void LitVertex(const csVector3 &n, G3DTexturedVertex &c)
{
  if(n.z>0)
    c.r=c.g=c.b=0;
  else
  {
    float l=n.z*n.z;
    c.r=c.g=c.b=l;
  }
}

void MetaDemo::DrawSomething(void)
{
  int i,j;

  for(i=0;i<num_meta_balls;i++)
  {
    float m=fmod((i+1)/3.0,1.5)+0.5;

    csVector3 &c=meta_balls[i].center;
    c.x=4*m*sin(m*alpha+i*M_PI/4);
    c.y=3*m*cos(1.4*m*alpha+m*M_PI/6);
    c.z=11+2*sin(m*alpha*1.3214);
  }
  
  CalculateMetaBalls();

  for(i=0;i<triangles_tesselated;i++)
  {
    Triangle& t=triangles_array[i];

    for(j=0;j<3;j++)
    {
      int m=2-j;

      // Projecting.
      poly->vertices[j].sx=h_width+h_height*t.p[m].x/(1+t.p[m].z);
      poly->vertices[j].sy=h_height*(1+t.p[m].y/(1+t.p[m].z));

      // Computing normal at point.
      csVector3 n(0,0,0);
      for(int k=0;k<num_meta_balls;k++)
      {
        csVector3 rv(t.p[m].x-meta_balls[k].center.x,
          t.p[m].y-meta_balls[k].center.y,
          t.p[m].z-meta_balls[k].center.z);

        float r=rv.Norm();
        float c=charge/(r*r*r);

        n+=rv*c;
      }

      // Lighting
      if(n.z>0)
        break;

      n=n.Unit();
      LitVertex(n,poly->vertices[j]);

      // Environment mapping.
      poly->vertices[j].u=map(n.x);
      poly->vertices[j].v=map(n.y);
      poly->vertices[j].z=1/t.p[m].z;
    }

    // We really want to draw this triangle
    if(j==3)
      piG3D->DrawPolygonFX(*poly);
  }
}

void MetaDemo::Write(int align,int x,int y,int fg,int bg,char *str,...)
{
  va_list arg;
  char b[256],*buf=b;

  va_start(arg,str);
  int l=vsprintf(buf,str,arg);
  va_end(arg);

  if(align!=ALIGN_LEFT)
  {
    int id,rb=0;
    piG2D->GetFontID(id);

    if(align==ALIGN_CENTER)
    {
      int where;
      sscanf(buf,"%d%n",&rb,&where);
      buf+=where+1;
      l-=where+1;
    }

    int w=0;
    for(int i=0;i<l;i++)
    {
      w += 8;
      //if(FontList[id].IndividualWidth)
        //w+=FontList[id].IndividualWidth[buf[i]];
      //else
        //w+=FontList[id].Width;
    }

    switch(align)
    {
      case ALIGN_RIGHT:  x-=w; break;
      case ALIGN_CENTER: x=(x+rb-w)/2; break;
    }
  }

  piG2D->Write(x,y,fg,bg,buf);
}

void MetaDemo::WriteShadow(int align,int x,int y,int fg,char *str,...)
{
  char buf[256];

  va_list arg;
  va_start(arg,str);
  vsprintf(buf,str,arg);
  va_end(arg);

  Write(align,x+1,y-1,0,-1,buf);
  Write(align,x,y,fg,-1,buf);
}

void MetaDemo::NextFrame (long elapsed_time, long current_time)
{
  SysSystemDriver::NextFrame(elapsed_time,current_time);

  // Handle all events.
  csEvent *Event;
  while((Event=EventQueue->Get()))
  {
    switch(Event->Type)
    {
      case csevKeyDown:
        eatkeypress(Event->Key.Code,Event->Key.ShiftKeys&CSMASK_SHIFT,
            Event->Key.ShiftKeys&CSMASK_ALT,Event->Key.ShiftKeys&CSMASK_CTRL,
	          elapsed_time);
        break;
      case csevBroadcast:
        break;
      case csevMouseDown:
        break;
      case csevMouseMove:
        break;
      case csevMouseUp:
        break;
    }
    delete Event;
  }

  static time_t time0;
  static time_t time_first=SysGetTime();
  time_t time1 = SysGetTime ();

  if (count <= 0)
  {
    if (time0 != (time_t)-1)
    {
      if (time1 != time0)
        timeFPS=30000.0f/(float)(time1-time0);
    }
    count = 30;
    time0 = SysGetTime ();
  }
  count--;
  num_frames++;

  alpha+=d_alpha;

  // Tell 3D driver we're going to display 3D things.
  if (piG3D->BeginDraw (CSDRAW_CLEARSCREEN|CSDRAW_3DGRAPHICS|CSDRAW_CLEARZBUFFER) != S_OK)
    return;

//-------------------------------------------------------------------
  ITextureHandle *th=tm->GetTextureHandle();

  poly->txt_handle = th;

  piG3D->StartPolygonFX(th, CS_FX_COPY | CS_FX_GOURAUD);

  piG3D->SetRenderState (G3DRENDERSTATE_ZBUFFERTESTENABLE, true);
  piG3D->SetRenderState (G3DRENDERSTATE_ZBUFFERFILLENABLE, true);

  DrawSomething();

  piG3D->FinishPolygonFX();
//-------------------------------------------------------------------

  // Start drawing 2D graphics.
  if (piG3D->BeginDraw (CSDRAW_2DGRAPHICS) == S_OK)
  {
    if (timeFPS>0)
    {
      WriteShadow(ALIGN_LEFT,10,FRAME_HEIGHT-48,text_fg,"%d balls",num_meta_balls);
      WriteShadow(ALIGN_LEFT,10,FRAME_HEIGHT-38,text_fg,"rate: %.2f",timeFPS);

      liveFPS=1000.0*num_frames/(time1-time_first);
      WriteShadow(ALIGN_LEFT,10,FRAME_HEIGHT-28,text_fg,"mean rate: %.2f",liveFPS);
      WriteShadow(ALIGN_LEFT,10,FRAME_HEIGHT-18,text_fg,"triangles: %d",
        triangles_tesselated);

      WriteShadow(ALIGN_RIGHT,FRAME_WIDTH-10,FRAME_HEIGHT-48,text_fg,"%d x %d",
        FRAME_WIDTH,FRAME_HEIGHT);
      WriteShadow(ALIGN_RIGHT,FRAME_WIDTH-10,FRAME_HEIGHT-28,text_fg,"%s enviro"
        "nment mapping",env_mapping==TRUE_ENV_MAP?"True":"Fake");
      WriteShadow(ALIGN_RIGHT,FRAME_WIDTH-10,FRAME_HEIGHT-18,text_fg,"%.2f -- e"
        "nv. mapping multiplier",env_map_mult);
      WriteShadow(ALIGN_RIGHT,FRAME_WIDTH-10,FRAME_HEIGHT-38,text_fg,"%.2f -- s"
        "peed",d_alpha);

      WriteShadow(ALIGN_CENTER,10,10,text_fg,"%d.Metaballs Demo based on Crystal Space",FRAME_WIDTH-10);
    }
  }

  // Drawing code ends here.
  piG3D->FinishDraw ();
  // Print the final output.
  piG3D->Print (NULL);
}

/*---------------------------------------------------------------------*
 * Main function
 *---------------------------------------------------------------------*/
int main (int argc, char* argv[])
{
  srand (time (NULL));

  // Open our configuration file.
  CHK (config = new csIniFile ("metademo.cfg"));

  // Create our main class.
  CHK (Sys = new MetaDemo());

  // Create our world. The world is the representation of
  // the 3D engine.
  CHK (Sys->world = new csWorld ());

  // Initialize the main system. This will load all needed
  // COM drivers (3D, 2D, network, sound, ...) and initialize them.
  if (!Sys->Initialize (argc, argv, Sys->world->GetEngineConfigCOM ()))
  {
    Sys->Printf (MSG_FATAL_ERROR, "Error initializing system!\n");
    cleanup ();
    exit (1);
  }

  // Our own initialization routine.
  Sys->InitApp ();

  // Main loop.
  Sys->Loop ();

  // Cleanup.
  cleanup ();

  return 0;
}
