#include <signal.h>
#include "libs.h"
#include "l3d.h"
#include "gui.h"
#include "glfreetype.h"
#include "player.h"
#include "space.h"
#include "planet.h"
#include "star.h"
#include "frame.h"
#include "ship_cpanel.h"
#include "sector_view.h"
#include "system_view.h"
#include "system_info_view.h"
#include "world_view.h"
#include "object_viewer_view.h"
#include "star_system.h"
#include "space_station.h"
#include "space_station_view.h"
#include "info_view.h"
#include "serializer.h"

float             L3D::timeAccel = 1.0f;
int               L3D::scrWidth;
int               L3D::scrHeight;
float             L3D::scrAspect;
SDL_Surface*      L3D::scrSurface;
sigc::signal<void, SDL_keysym*> L3D::onKeyPress;
sigc::signal<void, SDL_keysym*> L3D::onKeyRelease;
sigc::signal<void, int, int, int> L3D::onMouseButtonUp;
sigc::signal<void, int, int, int> L3D::onMouseButtonDown;
char              L3D::keyState[SDLK_LAST];
char              L3D::mouseButton[5];
int               L3D::mouseMotion[2];
enum L3D::CamType  L3D::camType;
enum L3D::MapView  L3D::mapView;
Player*           L3D::player;
View*             L3D::currentView;
WorldView*        L3D::worldView;
ObjectViewerView* L3D::objectViewerView;
SpaceStationView* L3D::spaceStationView;
InfoView*         L3D::infoView;
SectorView*       L3D::sectorView;
SystemView*       L3D::systemView;
SystemInfoView*   L3D::systemInfoView;
ShipCpanel*       L3D::cpan;
StarSystem*       L3D::selectedSystem;
StarSystem*       L3D::currentSystem;
MTRand            L3D::rng;
double            L3D::gameTime;
float             L3D::frameTime;
GLUquadric*       L3D::gluQuadric;
int               L3D::playerLocSecX;
int               L3D::playerLocSecY;
int               L3D::playerLocSysIdx;
bool              L3D::showDebugInfo;

void L3D::Init(IniConfig& config) {
  int width  = config.Int("ScrWidth");
  int height = config.Int("ScrHeight");
  const SDL_VideoInfo* info = NULL;
  if(SDL_Init(SDL_INIT_VIDEO) < 0) {
    fprintf(stderr, "Video initialization failed: %s\n", SDL_GetError());
    exit(-1);
  }

  info = SDL_GetVideoInfo();
  
  switch(config.Int("ScrDepth")) {
  case 16:
    SDL_GL_SetAttribute(SDL_GL_RED_SIZE,  5);
    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE,6);
    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
    break;
  case 32:
    SDL_GL_SetAttribute(SDL_GL_RED_SIZE,  8);
    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE,8);
    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
    break;
  default:
    fprintf(stderr, "Fatal error: Invalid screen depth in config.ini.\n");
    L3D::Quit();
  }
  SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE,  24);
  SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

  Uint32 flags = SDL_OPENGL;
  if(config.Int("StartFullscreen")) flags |= SDL_FULLSCREEN;

  if((L3D::scrSurface = SDL_SetVideoMode(width, height, info->vfmt->BitsPerPixel, flags)) == 0) {
    /* Fall back to 16-bit depth buffer.. */
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
    fprintf(stderr,"Failed to set video mode. (%s). Re-trying with 16 bit depth buffer.\n",SDL_GetError());
    if((L3D::scrSurface = SDL_SetVideoMode(width, height, info->vfmt->BitsPerPixel, flags)) == 0) {
      fprintf(stderr, "video mode set failed: %s\n", SDL_GetError());
      exit(-1);
    }
  }

  L3D::scrWidth = width;
  L3D::scrHeight = height;
  L3D::scrAspect = width / (float)height;

  L3D::rng.seed(time(NULL));

  InitOpenGL();

  dInitODE();
  GLFTInit();
  Space::Init();

  Gui::Init(scrWidth, scrHeight, 800, 600);
}

void L3D::InitOpenGL() {
  glShadeModel(GL_SMOOTH);
  glCullFace(GL_BACK);
  glFrontFace(GL_CCW);
  glEnable(GL_CULL_FACE);
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

  glClearColor(0, 0, 0, 0);
  glViewport(0, 0, scrWidth, scrHeight);

  gluQuadric = gluNewQuadric();
}

void L3D::Quit(void) {
  SDL_Quit();
  exit(0);
}

void L3D::SetTimeAccel(float s) {
  /* We don't want player spinning like crazy when hitting time accel. */
  if(s > 10) {
    player->SetAngVelocity(vector3d(0,0,0));
    dBodySetTorque(player->m_body, 0, 0, 0);
    player->SetAngThrusterState(0, 0.0f);
    player->SetAngThrusterState(1, 0.0f);
    player->SetAngThrusterState(2, 0.0f);
  }
  timeAccel = s;
}

void L3D::SetCamType(enum CamType c) {
  camType = c;
  mapView = MAP_NOMAP;
  SetView(worldView);
}

void L3D::SetMapView(enum MapView v) {
  mapView = v;
  if(v == MAP_SECTOR)
    SetView(sectorView);
  else
    SetView(systemView);
}

void L3D::SetView(View* v) {
  if(currentView) currentView->HideAll();
  currentView = v;
  currentView->ShowAll();
}

void L3D::HandleEvents(void) {
  SDL_Event event;

  L3D::mouseMotion[0] = L3D::mouseMotion[1] = 0;
  while(SDL_PollEvent(&event)) {
    Gui::HandleSDLEvent(&event);
    switch(event.type) {
    case SDL_KEYDOWN:
      if(KeyState(SDLK_LCTRL) && (event.key.keysym.sym == SDLK_q)) L3D::Quit();
      if(event.key.keysym.sym == SDLK_i) L3D::showDebugInfo = !L3D::showDebugInfo;
#ifdef DEBUG
      if(event.key.keysym.sym == SDLK_F12) {
        /* Add test object. */
        Ship* body = new Ship(ShipType::LADYBIRD);
        body->SetLabel("A friend");
        body->SetFrame(L3D::player->GetFrame());
        body->SetPosition(L3D::player->GetPosition()+vector3d(0,0,-1000));
        Space::AddBody(body);
      }
#endif
      if(event.key.keysym.sym == SDLK_F11) SDL_WM_ToggleFullScreen(L3D::scrSurface);
      if(event.key.keysym.sym == SDLK_F10) L3D::SetView(L3D::objectViewerView);
      if(event.key.keysym.sym == SDLK_F9)  Serializer::Write::Game("quicksave.sav");
      L3D::keyState[event.key.keysym.sym] = 1;
      L3D::onKeyPress.emit(&event.key.keysym);
      break;
    case SDL_KEYUP:
      L3D::keyState[event.key.keysym.sym] = 0;
      L3D::onKeyRelease.emit(&event.key.keysym);
      break;
    case SDL_MOUSEBUTTONDOWN:
      L3D::mouseButton[event.button.button] = 1;
      L3D::onMouseButtonDown.emit(event.button.button,
                                 event.button.x, event.button.y);
      break;
    case SDL_MOUSEBUTTONUP:
      L3D::mouseButton[event.button.button] = 1;
      L3D::onMouseButtonUp.emit(event.button.button,
                               event.button.x, event.button.y);
      break;
    case SDL_MOUSEMOTION:
      L3D::mouseMotion[0] += event.motion.xrel;
      L3D::mouseMotion[1] += event.motion.yrel;
      //SDL_GetRelativeMouseState(&L3D::mouseMotion[0], &L3D::mouseMotion[1]);
      break;
    case SDL_QUIT:
      L3D::Quit();
      break;
    }
  }
}

static void draw_intro(float _time) {
  static float lightCol[4] = { 1, 1, 1, 0 };
  static float lightDir[4] = { 0, 1, 0, 0 };

  static ObjParams params  = {
    { 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0.0f, 0.0f, 0.1f}, { 0.0f, 0.0f, 0.0f },
    /* pColor[3] */
    {
      { { 1.0f, 1.0f, 1.0f }, { 0, 0, 0 }, { 0, 0, 0 }, 0 },
      { { 0.8f, 0.6f, 0.5f }, { 0, 0, 0 }, { 0, 0, 0 }, 0 },
      { { 0.5f, 0.5f, 0.5f }, { 0, 0, 0 }, { 0, 0, 0 }, 0 },
    },
    { "LEPHISTO" },
  };
  glRotatef(_time*10, 1, 0, 0);
  L3D::worldView->DrawBgStars();
  glPushAttrib(GL_ALL_ATTRIB_BITS);
  sbreSetViewport(L3D::GetScrWidth(), L3D::GetScrHeight(), L3D::GetScrWidth()*0.5, 1.0f, 1000.0f, 0.0f, 1.0f);
  sbreSetDirLight(lightCol, lightDir);
  matrix4x4d rot = matrix4x4d::RotateYMatrix(_time) * matrix4x4d::RotateZMatrix(0.6*_time) *
               matrix4x4d::RotateXMatrix(_time*.7);
  Matrix m;
  Vector p;
  m.x1 = rot[0]; m.x2 = rot[4]; m.x3 = rot[ 8];
  m.y1 = rot[1]; m.y2 = rot[5]; m.y3 = rot[ 9];
  m.z1 = rot[2]; m.z2 = rot[6]; m.z3 = rot[10];
  p.x = 0; p.y = 0; p.z = 80;
  sbreRenderModel(&p, &m, 61, &params);
  glPopAttrib();
}

void L3D::Start(void) {

  player = new Player(ShipType::SLEEK);
  player->SetLabel("Me");
  Space::AddBody(player);

  cpan = new ShipCpanel();

  sectorView        = new SectorView();
  systemView        = new SystemView();
  systemInfoView    = new SystemInfoView();
  worldView         = new WorldView();
  objectViewerView  = new ObjectViewerView();
  spaceStationView  = new SpaceStationView();
  infoView          = new InfoView();

  Gui::Fixed* splash = new Gui::Fixed(Gui::Screen::GetWidth(), Gui::Screen::GetHeight());
  Gui::Screen::AddBaseWidget(splash, 0, 0);
  splash->SetTransparency(true);

  const float w = Gui::Screen::GetWidth() / 2;
  const float h = Gui::Screen::GetHeight() / 2;
  const int OPTS = 4;
  Gui::ToggleButton* opts[OPTS];
  opts[0] = new Gui::ToggleButton(); opts[0]->SetShortcut(SDLK_1, KMOD_NONE);
  opts[1] = new Gui::ToggleButton(); opts[1]->SetShortcut(SDLK_2, KMOD_NONE);
  opts[2] = new Gui::ToggleButton(); opts[2]->SetShortcut(SDLK_3, KMOD_NONE);
  opts[3] = new Gui::ToggleButton(); opts[3]->SetShortcut(SDLK_4, KMOD_NONE);
  splash->Add(opts[0], w, h+64);
  splash->Add(new Gui::Label("New game starting on Earth"), w+32, h+64);
  splash->Add(opts[1], w, h+32);
  splash->Add(new Gui::Label("New game starting on debug point"), w+32, h+32);
  splash->Add(opts[2], w, h);
  splash->Add(new Gui::Label("Load quicksave"), w+32, h);
  splash->Add(opts[3], w, h-32);
  splash->Add(new Gui::Label("Quit"), w+32, h-32);

  splash->ShowAll();

  int choice = 0;
  Uint32 last_time = SDL_GetTicks();
  float _time = 0;
  do {
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glClearColor(0,0,0,0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    L3D::HandleEvents();
    SDL_ShowCursor(1);
    SDL_WM_GrabInput(SDL_GRAB_OFF);

    draw_intro(_time);
    Gui::Draw();
    glFlush();
    SDL_GL_SwapBuffers();

    L3D::frameTime = 0.001*(SDL_GetTicks() - last_time);
    _time += L3D::frameTime;
    last_time = SDL_GetTicks();

    /* Poll ui instead of using callbacks. :D */
    for(int i = 0; i < OPTS; i++) if(opts[i]->GetPressed()) choice = i+1;
  } while(!choice);
  splash->HideAll();

  if(choice == 1) {
    /* Earth start point. */
    StarSystem s(0,0,0);
    HyperspaceTo(&s);
    //Frame* pframe = *(++(++(Space::rootFrame->m_children.begin())));
    //player->SetFrame(pframe);
    /* TODO: There isn't a sensible way to find stations for a planet. */
    SpaceStation* station = 0;
    for(Space::bodiesIter_t i = Space::bodies.begin(); i!=Space::bodies.end(); i++) {
      if((*i)->IsType(Object::SPACESTATION)) { station = (SpaceStation*)*i; break; }
    }
    assert(station);
    player->SetFrame(station->GetFrame());
    player->SetDockedWith(station, 0);
    MainLoop();
  } else if(choice == 2) {
    /* Debug start point. */
    StarSystem s(0,0,1);
    HyperspaceTo(&s);
    Frame* pframe = *(Space::rootFrame->m_children.begin());
    player->SetFrame(pframe);
    player->SetPosition(vector3d(0,0,EARTH_RADIUS));
    MainLoop();
  } else if(choice == 3) {
    /* Load quicksave. */
    Serializer::Read::Game("quicksave.sav");
  }
}

void L3D::MainLoop(void) {
  cpan->ShowAll();

  SetView(worldView);

  Uint32 last_stats = SDL_GetTicks();
  int frame_stat = 0;
  int phys_stat = 0;
  char fps_readout[32];
  Uint32 time_before_frame = SDL_GetTicks();
  Uint32 last_phys_update = time_before_frame;

  for(;;) {
    frame_stat++;
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    currentView->Draw3D();
    /*
     * TODO: HandleEvents at the moment must be after view->Draw3D and before
     * Gui::Draw so that labels drawn to screen can have mouse events correctly
     * detected. Gui::Draw wipes memory of label positions.
     */
    L3D::HandleEvents();
    /* Hide cursor for ship control. */
    if(L3D::MouseButtonState(3)) {
      SDL_ShowCursor(0);
      SDL_WM_GrabInput(SDL_GRAB_ON);
    } else {
      SDL_ShowCursor(1);
      SDL_WM_GrabInput(SDL_GRAB_OFF);
    }

    Gui::Draw();
#ifdef DEBUG
    if(L3D::showDebugInfo) {
      Gui::Screen::EnterOrtho();
      glColor3f(1, 1, 1);
      glTranslatef(0, Gui::Screen::GetHeight()-20, 0);
      Gui::Screen::RenderString(fps_readout);
      Gui::Screen::LeaveOrtho();
    }
#endif

    glFlush();
    SDL_GL_SwapBuffers();
    //if(glGetError()) printf("GL: %s\n", gluErrorString(glGetError()));

    L3D::frameTime = 0.001*(SDL_GetTicks() - time_before_frame);

    time_before_frame = SDL_GetTicks();
    /* Fixed 62.5hz physics. */
    while(time_before_frame - last_phys_update > 16) {
      last_phys_update += 16;
      const float step = L3D::GetTimeStep();
      if(step) Space::TimeStep(step);
      gameTime += step;
      phys_stat++;
    }
    currentView->Update();

    if(SDL_GetTicks() - last_stats > 1000) {
      snprintf(fps_readout, sizeof(fps_readout), "%d fps, %d phys updates", frame_stat, phys_stat);
      frame_stat = 0;
      phys_stat = 0;
      last_stats += 1000;
    }
  }
}

StarSystem* L3D::GetSelectedSystem(void) {
  int sector_x, sector_y, system_idx;
  L3D::sectorView->GetSelectedSystem(&sector_x, &sector_y, &system_idx);
  if(system_idx == -1) {
    selectedSystem = 0;
    return NULL;
  }
  if(selectedSystem) {
    if(!selectedSystem->IsSystem(sector_x, sector_y, system_idx)) {
      delete selectedSystem;
      selectedSystem = 0;
    }
  }
  if(!selectedSystem) {
    selectedSystem = new StarSystem(sector_x, sector_y, system_idx);
  }
  return selectedSystem;
}

void L3D::HyperspaceTo(StarSystem* dest) {
  int sec_x, sec_y, sys_idx;
  dest->GetPos(&sec_x, &sec_y, &sys_idx);

  if(currentSystem) delete currentSystem;
  currentSystem = new StarSystem(sec_x, sec_y, sys_idx);

  Space::Clear();
  Space::BuildSystem();
  float ang = rng.Double(M_PI);
  L3D::player->SetPosition(vector3d(sin(ang)*AU, cos(ang)*AU,0));
  L3D::player->SetVelocity(vector3d(0.0));
  dest->GetPos(&L3D::playerLocSecX, &L3D::playerLocSecY, &L3D::playerLocSysIdx);
}

void L3D::Serialize(void) {
  using namespace Serializer::Write;
  StarSystem::Serialize(selectedSystem);
  wr_double(gameTime);
  StarSystem::Serialize(currentSystem);
  Space::Serialize();
}

void L3D::Unserialize(void) {
  using namespace Serializer::Read;
  selectedSystem = StarSystem::Unserialize();
  gameTime = rd_double();
  currentSystem = StarSystem::Unserialize();
  Space::Unserialize();
}

IniConfig::IniConfig(const char* filename) {
  FILE* f = fopen(filename, "r");
  if(!f) {
    fprintf(stderr, "Could not open '%s'.\n", filename);
    L3D::Quit();
  }
  char buf[1024];
  while(fgets(buf, sizeof(buf), f)) {
    if(buf[0] == '#') continue;
    char* sep = strchr(buf, '=');
    char* kend = sep;
    if(!sep) continue;
    *sep = 0;
    /* Strip whitespace. */
    while(isspace(*(--kend))) *kend = 0;
    while(isspace(*(++sep)))  *sep  = 0;
    /* Snip \r, \n. */
    char* vend = sep;
    while(*(++vend)) if((*vend == '\r') || (*vend == '\n')) { *vend = 0; break; }
    std::string key = std::string(buf);
    std::string val = std::string(sep);
    (*this)[key] = val;
  }
  fclose(f);
}

void sigsegv_handler(int signum) {
  if(signum == SIGSEGV) {
    printf("Segfault! All is lost! Abondon ship\n");
    SDL_Quit();
    abort();
  }
}

int main(int argc, char** argv) {
  printf("Lephisto3D's super high tech demo!\n");
  
  signal(SIGSEGV, sigsegv_handler);
  IniConfig cfg("config.ini");

  L3D::Init(cfg);
  L3D::Start();
  L3D::Quit();
  return 0;
}