/*
    Copyright (C) 1998 by Jorrit Tyberghein

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

/*
 * Command processor. There are now several sources of commands:
 * the console and the keyboard. This class ignores the sources but
 * just executes the commands. The respective source handlers should
 * then do whatever they need to recognize the command and send the
 * command to this class.
 */

#include <string.h>
#include "version.h"
#include "sysdef.h"
#include "csengine/sysitf.h"
#include "csengine/world.h"
#include "csengine/lghtmap.h"
#include "csengine/sector.h"
#include "csengine/polygon.h"
#include "csengine/polytext.h"
#include "csengine/cssprite.h"
#include "csengine/dumper.h"
#include "cssys/common/console.h"
#include "igraph3d.h"
#include "igraph2d.h"
#include "support/command.h"
#include "csutil/scanstr.h"
#include "csinput/csevent.h"
#include "csinput/cseventq.h"
#include "csobject/nameobj.h"
#include "csscript/objtrig.h"

// Static Command variables
Command* Command::shared_instance = NULL;
csWorld* Command::world = NULL;
csCamera* Command::camera = NULL;
IGraphics3D* Command::g3d = NULL;
csConsole* Command::console = NULL;
ISystem* Command::system = NULL;
FILE* Command::script = NULL;
// Additional command handler
Command::CmdHandler Command::ExtraHandler = NULL;

void Command::Initialize (csWorld* world, csCamera* camera, IGraphics3D* g3d, csConsole* console, ISystem* system)
{
  Command::world = world;
  Command::camera = camera;
  Command::g3d = g3d;
  Command::console = console;
  Command::system = system;
}

Command* Command::SharedInstance()
{
  if (shared_instance == 0)
    CHK (shared_instance = new Command;);
  return shared_instance;
}

bool Command::PerformLine (char* line)
{
  return perform_line (line);
}

static int value_choice (char* arg, int old_value, char** choices, int num)
{
  if (!arg) return -1;
  int i = 0;
  if (!strcasecmp (arg, "next")) return (old_value+1)%num;
  if (!strcasecmp (arg, "prev")) return (old_value-1+num)%num;
  while (choices[i])
  {
    if (!strcasecmp (choices[i], arg)) return i;
    i++;
  }
  CsPrintf (MSG_CONSOLE, "Expected one of the following:\n");
  i = 0;
  while (choices[i])
  {
    CsPrintf (MSG_CONSOLE, "    %s%s\n", choices[i], i == old_value ? " (current)" : "");
    i++;
  }
  CsPrintf (MSG_CONSOLE, "    or 'next' or 'prev'\n");
  return -1;
}

static bool yes_or_no (char* arg, bool old_value)
{
  if (!arg) return false;
  if (*arg == '0' && *(arg+1) == 0) return false;
  if (*arg == '1' && *(arg+1) == 0) return true;
  if (!strcasecmp (arg, "yes") || !strcasecmp (arg, "true") || !strcasecmp (arg, "on")) return true;
  if (!strcasecmp (arg, "no") || !strcasecmp (arg, "false") || !strcasecmp (arg, "off")) return false;
  if (!strcasecmp (arg, "toggle")) return !old_value;
  CsPrintf (MSG_CONSOLE, "Expected: yes, true, on, 1, no, false, off, 0, or toggle!\n");
  return false;
}

static char* say_on_or_off (int arg)
{
  if (arg) return "on";
  return "off";
}

/*
 * Standard processing to change/display a boolean value setting.
 */
void Command::change_boolean (char* arg, bool* value, char* what)
{
  if (arg)
  {
    // Change value
    int v = yes_or_no (arg, *value);
    if (v != -1)
    {
      *value = v;
      CsPrintf (MSG_CONSOLE, "Set %s %s\n", what, say_on_or_off (*value));
    }
  }
  else
  {
    // Show value
    CsPrintf (MSG_CONSOLE, "Current %s is %s\n", what, say_on_or_off (*value));
  }
}

bool Command::change_boolean_gfx3d (char* arg, G3D_RENDERSTATEOPTION op, char* what)
{
  bool bValue;
  long val;
  g3d->GetRenderState (op, val);
  bValue = (bool)val;
  change_boolean (arg, &bValue, what);
  return (SUCCEEDED (g3d->SetRenderState (op, bValue)));
}

/*
 * Standard processing to change/display a multi-value setting.
 */
void Command::change_choice (char* arg, int* value, char* what, char** choices, int num)
{
  if (arg)
  {
    // Change value
    int v = value_choice (arg, *value, choices, num);
    if (v != -1)
    {
      *value = v;
      CsPrintf (MSG_CONSOLE, "Set %s %s\n", what, choices[*value]);
    }
  }
  else
  {
    // Show value
    CsPrintf (MSG_CONSOLE, "Current %s is %s\n", what, choices[*value]);
  }
}

/*
 * Standard processing to change/display a floating point setting.
 * Return true if value changed.
 */
bool Command::change_float (char* arg, float* value, char* what, float min, float max)
{
  if (arg)
  {
    // Change value.
    float g;
    if ((*arg == '+' || *arg == '-') && *(arg+1) == *arg)
    {
      float dv;
      sscanf (arg+1, "%f", &dv);
      g = *value+dv;
    }
    else sscanf (arg, "%f", &g);
    if (g < min || g > max) CsPrintf (MSG_CONSOLE, "Bad value for %s (%f <= value <= %f)!\n", what, min, max);
    else
    {
      *value = g;
      CsPrintf (MSG_CONSOLE, "Set %s to %f\n", what, *value);
      return true;
    }
  }
  else
  {
    // Show value.
    CsPrintf (MSG_CONSOLE, "Current %s is %f\n", what, *value);
  }
  return false;
}

/*
 * Standard processing to change/display an integer setting.
 * Return true if value changed.
 */
bool Command::change_int (char* arg, int* value, char* what, int min, int max)
{
  if (arg)
  {
    // Change value.
    int g;
    if ((*arg == '+' || *arg == '-') && *(arg+1) == *arg)
    {
      int dv;
      sscanf (arg+1, "%d", &dv);
      g = *value+dv;
    }
    else sscanf (arg, "%d", &g);
    if (g < min || g > max) CsPrintf (MSG_CONSOLE, "Bad value for %s (%d <= value <= %d)!\n", what, min, max);
    else
    {
      *value = g;
      CsPrintf (MSG_CONSOLE, "Set %s to %d\n", what, *value);
      return true;
    }
  }
  else
  {
    // Show value.
    CsPrintf (MSG_CONSOLE, "Current %s is %d\n", what, *value);
  }
  return false;
}

bool Command::perform_line (char* line)
{
  char cmd[512], arg[255];
  if (*line == ';') return true;        // Comment
  if (*line == 0) return true;          // Empty line
  strcpy (cmd, line);
  char* space = strchr (cmd, ' ');
  if (space) { *space = 0; strcpy (arg, space+1); }
  else *arg = 0;
  return perform (cmd, *arg ? arg : (char*)NULL);
}

bool Command::perform (char* cmd, char* arg)
{
  if (ExtraHandler)
  {
    static bool inside = false;
    if (!inside)
    {
      inside = true;
      bool ret = ExtraHandler (cmd, arg);
      inside = false;
      if (ret) return true;
    }
  }

  if (!strcasecmp (cmd, "quit"))
    csWorld::isys->Shutdown ();
  else if (!strcasecmp (cmd, "help"))
  {
    CsPrintf (MSG_CONSOLE, "-*- General commands -*-\n");
    CsPrintf (MSG_CONSOLE, " about, version, quit, help\n");
    CsPrintf (MSG_CONSOLE, " dblbuff, debug, maxpol, cachedump, cacheclr\n");
//  CsPrintf (MSG_CONSOLE, " coorddump, coordsave, coordload,\n");
    CsPrintf (MSG_CONSOLE, " coordset, console, facenorth,\n");
    CsPrintf (MSG_CONSOLE, " facesouth, faceeast, facewest,\n");
    CsPrintf (MSG_CONSOLE, " faceup, facedown, turn, activate,\n");
    CsPrintf (MSG_CONSOLE, " cls, exec, dnl, dump, cmessage,\n");
    CsPrintf (MSG_CONSOLE, " dmessage, lighting, texture,\n");
    CsPrintf (MSG_CONSOLE, " portals, transp, mipmap,\n");
    CsPrintf (MSG_CONSOLE, " gamma, fov, gouraud, dmipmap1,\n");
    CsPrintf (MSG_CONSOLE, " dmipmap2, dmipmap3, ilace, mmx\n");
    CsPrintf (MSG_CONSOLE, " inter, texelflt, subcache\n");
    CsPrintf (MSG_CONSOLE, " dyncache, lm_grid, lm_only, cosfact\n");
    CsPrintf (MSG_CONSOLE, " extension, lod, coorddump\n");
  }
  else if (!strcasecmp (cmd, "about"))
  {
    CsPrintf (MSG_CONSOLE, "Crystal Space version %s (%s).\n", VERSION, RELEASE_DATE);
  }
  else if (!strcasecmp (cmd, "version"))
    CsPrintf (MSG_CONSOLE, "%s\n", VERSION);
  else if (!strcasecmp (cmd, "extension"))
  {
    IGraphics2D* g2d;
    g3d->Get2dDriver (&g2d);
    if (g2d->PerformExtension (arg) != S_OK)
      CsPrintf (MSG_CONSOLE, "Extension '%s' not supported!\n", arg);
  }
  else if (!strcasecmp (cmd, "maxpol"))
  {
    long val;
    g3d->GetRenderState (G3DRENDERSTATE_MAXPOLYGONSTODRAW, val);
    int ival = (int)val;
    change_int (arg, &ival, "maximum polygons", 0, 2000000000);
    g3d->SetRenderState (G3DRENDERSTATE_MAXPOLYGONSTODRAW, (long)ival);
  }
  else if (!strcasecmp (cmd, "cmessage"))
  {
    if (arg) CsPrintf (MSG_CONSOLE, arg);
    else CsPrintf (MSG_CONSOLE, "Argument expected!\n");
  }
  else if (!strcasecmp (cmd, "dmessage"))
  {
    if (arg) CsPrintf (MSG_DEBUG_0, arg);
    else CsPrintf (MSG_CONSOLE, "Argument expected!\n");
  }
  else if (!strcasecmp (cmd, "cosfact"))
    change_float (arg, &csPolyTexture::cfg_cosinus_factor, "cosinus factor", -1, 1);
  else if (!strcasecmp (cmd, "lod"))
    change_float (arg, &csSprite3D::cfg_lod_detail, "LOD detail", -1, 1);
  else if (!strcasecmp (cmd, "dnl"))
    CsPrintf (MSG_DEBUG_0, "\n");
  else if (!strcasecmp (cmd, "exec"))
  {
    if (arg)
    {
      if (start_script (arg))
        console->Show ();
    }
    else CsPrintf (MSG_CONSOLE, "Please specify the name of the script!\n");
  }
  else if (!strcasecmp (cmd, "dump"))
  {
    Dumper::dump (camera);
    Dumper::dump (world);
  }
  else if (!strcasecmp (cmd, "transp"))
    change_boolean_gfx3d (arg, G3DRENDERSTATE_TRANSPARENCYENABLE, "transp");
  else if (!strcasecmp (cmd, "portals"))
    change_boolean (arg, &csSector::do_portals, "portals");
  //@@@
  //else if (!strcasecmp (cmd, "lm_grid"))
    //change_boolean (arg, &Textures::do_lightmapgrid, "lightmap grid");
  //else if (!strcasecmp (cmd, "lm_only"))
    //change_boolean (arg, &Textures::do_lightmaponly, "lightmap only");
  else if (!strcasecmp (cmd, "texture"))
    change_boolean_gfx3d (arg, G3DRENDERSTATE_TEXTUREMAPPINGENABLE, "texture mapping");
  else if (!strcasecmp (cmd, "texelflt"))
    change_boolean_gfx3d (arg, G3DRENDERSTATE_FILTERINGENABLE, "texel filtering");
  else if (!strcasecmp (cmd, "things"))
    change_boolean (arg, &csSector::do_things, "things");
  else if (!strcasecmp (cmd, "lighting"))
    change_boolean_gfx3d (arg, G3DRENDERSTATE_LIGHTINGENABLE, "lighting");
  else if (!strcasecmp (cmd, "gouraud"))
  {
    if (!change_boolean_gfx3d (arg, G3DRENDERSTATE_GOURAUDENABLE, "Gouraud shading"))
      CsPrintf (MSG_CONSOLE, "Gouraud shading toggling is not supported by 3D driver\n");
  }
  else if (!strcasecmp (cmd, "ilace"))
  {
    if (!change_boolean_gfx3d (arg, G3DRENDERSTATE_INTERLACINGENABLE, "interlaced mode"))
      CsPrintf (MSG_CONSOLE, "Interlaced mode not supported by 3D driver\n");
  }
  else if (!strcasecmp (cmd, "mmx"))
  {
    if (!change_boolean_gfx3d (arg, G3DRENDERSTATE_MMXENABLE, "mmx usage"))
      CsPrintf (MSG_CONSOLE, "MMX support is not present in this version\n");
  }
  else if (!strcasecmp (cmd, "cls"))
    console->Clear ();
  else if (!strcasecmp (cmd, "console"))
  {
    bool active = console->IsActive ();
    change_boolean (arg, &active, "console");
    if (active != console->IsActive ())
    {
      if (active)
        console->Show ();
      else
        console->Hide ();
    }
  }
  else if (!strcasecmp (cmd, "mipmap"))
  {
    long old;
    int nValue;
    char* choices[6] = { "on", "off", "1", "2", "3", NULL };
    g3d->GetRenderState( G3DRENDERSTATE_MIPMAPENABLE, old );
    nValue = old;
    change_choice (arg, &nValue, "mipmapping", choices, 5);
    g3d->SetRenderState( G3DRENDERSTATE_MIPMAPENABLE, (long)nValue );
  }
  else if (!strcasecmp (cmd, "inter"))
  {
    long old;
    int nValue;
    char* choices[5] = { "smart", "step32", "step16", "step8", NULL };
    g3d->GetRenderState (G3DRENDERSTATE_INTERPOLATIONSTEP, old);
    nValue = old;
    change_choice (arg, &nValue, "interpolation steps", choices, 4);
    g3d->SetRenderState (G3DRENDERSTATE_INTERPOLATIONSTEP, (long)nValue);
  }
  else if (!strcasecmp (cmd, "cachedump"))
    g3d->DumpCache ();
  else if (!strcasecmp (cmd, "cacheclr"))
  {
    CsPrintf (MSG_CONSOLE, "Refresh (clear) the texture cache.\n");
    g3d->ClearCache ();
  }
  else if (!strcasecmp (cmd, "turn"))
    camera->Rotate (VEC_ROT_RIGHT, M_PI);
  else if (!strcasecmp (cmd, "activate"))
  {
    csVector3 where = camera->This2Other(3*VEC_FORWARD);
    csPolygon3D* p = camera->GetHit (where);
    if (p)
    {
      CsPrintf (MSG_CONSOLE, "Activate polygon '%s' ", 
                             csNameObject::GetName(*p));
      csPolygonSet* ob = (csPolygonSet*)(p->GetParent ());
      CsPrintf (MSG_CONSOLE, "in set '%s'\n", csNameObject::GetName(*ob));
      csObjectTrigger::DoActivateTriggers(*ob);
    }
  }
  else if (!strcasecmp (cmd, "coordset"))
  {
    if (!arg)
    {
      CsPrintf (MSG_CONSOLE, "Expected argument!\n");
      return false;
    }
    char sect[100];
    float x, y, z;
    if (ScanStr (arg, "%s,%f,%f,%f", sect, &x, &y, &z) != 4)
    {
      CsPrintf (MSG_CONSOLE, "Expected sector,x,y,z. Got something else!\n");
      return false;
    }
    csSector* s = (csSector*)world->sectors.FindByName (sect);
    if (!s)
    {
      CsPrintf (MSG_CONSOLE, "Can't find this sector!\n");
      return false;
    }
    camera->SetSector (s);
    camera->SetPosition (csVector3(x,y,z));
  }
  else if (!strcasecmp (cmd, "facenorth"))
   camera->SetO2T (csMatrix3() /* identity */ );
  else if (!strcasecmp (cmd, "facesouth"))
   camera->SetO2T ( csMatrix3 ( -1,  0,  0,
                               0,  1,  0,
                               0,  0, -1 ) );
  else if (!strcasecmp (cmd, "facewest"))
   camera->SetO2T ( csMatrix3 (  0,  0,  1,
                               0,  1,  0,
                              -1,  0,  0 ) );
  else if (!strcasecmp (cmd, "faceeast"))
   camera->SetO2T ( csMatrix3 (  0,  0, -1,
                               0,  1,  0,
                              1,  0,  0 ) );
  else if (!strcasecmp (cmd, "facedown"))
   camera->SetO2T ( csMatrix3 (  1,  0,  0,
                               0,  0,  1,
                               1, -1,  0 ) );
  else if (!strcasecmp (cmd, "faceup"))
   camera->SetO2T ( csMatrix3 (  1,  0,  0,
                               0,  0, -1,
                               1,  1,  0 ) );
  else if (!strcasecmp (cmd, "coorddump"))
    Dumper::dump (camera);
  else if (!strcasecmp (cmd, "gamma"))
  {
#if 0
//@@@GAMMA is a setting for the 3D rasterizer and should move there
    if (change_float (arg, &Textures::Gamma, "gamma", 0.01, 20.0))
    {
      Textures* tex = world->GetTextures ();
      tex->alloc_palette ();
      CHK (System->EventQueue->Put (new csEvent (SysGetTime (), csevBroadcast, cscmdPaletteChanged)));
    }
#endif
  }
  else if (!strcasecmp (cmd, "fov"))
  {
    float fov = (float)(csCamera::aspect);
    if (change_float (arg, &fov, "fov", 0.01, 2000.0))
    {
      csCamera::aspect = (int)fov;
      csCamera::inv_aspect = 1./csCamera::aspect;
    }
  }
  else if (!strcasecmp (cmd, "subcache"))
  {
    int s = csPolyTexture::subtex_size;
    change_int (arg, &s, "subcache", 0, 32768);
    if (s != 0)
    {
      int ss = s;
      while (!(ss&1)) ss >>= 1;
      if (ss != 1)
      {
        CsPrintf (MSG_CONSOLE, "Expected a power of two or else zero to disable this feature!\n");
        return true;
      }
    }
    csPolyTexture::subtex_size = s;
    g3d->ClearCache ();
  }
  else if (!strcasecmp (cmd, "dyncache"))
    change_boolean (arg, &csPolyTexture::subtex_dynlight, "dynlight cache opt");
  //@@@
  //else if (!strcasecmp (cmd, "dmipmap1"))
    //change_float (arg, &Polygon3D::zdist_mipmap1, "mipmap distance 1", 0.0, 1000.0);
  //else if (!strcasecmp (cmd, "dmipmap2"))
    //change_float (arg, &Polygon3D::zdist_mipmap2, "mipmap distance 2", 0.0, 1000.0);
  //else if (!strcasecmp (cmd, "dmipmap3"))
    //change_float (arg, &Polygon3D::zdist_mipmap3, "mipmap distance 3", 0.0, 1000.0);
  else if (!strcasecmp (cmd, "dblbuff"))
  {
    bool state;    
    IGraphics2D* g2d;
    g3d->Get2dDriver (&g2d);
    g2d->GetDoubleBufferState(state);

    if (arg)
    {
      bool newstate = yes_or_no (arg, state);
      if (newstate != (bool)state)
        if (g2d->DoubleBuffer (newstate) != S_OK)
          CsPrintf (MSG_CONSOLE, "Switching double buffering is not supported in current video mode!\n");
    }
    else
      CsPrintf (MSG_CONSOLE, "Current dblbuff is %s\n", say_on_or_off (state));
  }
  else return false;
  return true;
}

bool Command::start_script (char* scr)
{
  FILE* fp;
  system->FOpen (scr, "r", &fp);
  if (!fp)
  {
    CsPrintf (MSG_CONSOLE, "Could not open script file '%s'!\n", scr);
    return false;
  }

  // Replace possible running script with this one.
  if (script) system->FClose (script);
  script = fp;

  return true;
}

bool Command::get_script_line (char* buf, int max_size)
{
  if (!script) return false;
  if (fgets (buf, max_size, script))
  {
    char* p = strchr (buf, '\n');
    if (p) *p = 0;
    return true;
  }
  system->FClose (script);
  script = NULL;
  return false;
}
