/* bzflag
 * Copyright (c) 1993-2017 Tim Riker
 *
 * This package is free software;  you can redistribute it and/or
 * modify it under the terms of the license found in the file
 * named COPYING that should have accompanied this file.
 *
 * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */

// interface header
#include "BZWReader.h"

// implementation-specific system headers
#include <fstream>
#include <sstream>
#include <ctype.h>

// implementation-specific bzflag headers
#include "BZDBCache.h"

// implementation-specific bzfs-specific headers
#include "TeamBases.h"
#include "WorldFileObject.h"
#include "CustomGroup.h"
#include "CustomBox.h"
#include "CustomPyramid.h"
#include "CustomGate.h"
#include "CustomLink.h"
#include "CustomBase.h"
#include "CustomWeapon.h"
#include "CustomWorld.h"
#include "CustomZone.h"
#include "CustomTetra.h"
#include "CustomMesh.h"
#include "CustomArc.h"
#include "CustomCone.h"
#include "CustomSphere.h"
#include "CustomWaterLevel.h"
#include "CustomDynamicColor.h"
#include "CustomTextureMatrix.h"
#include "CustomMaterial.h"
#include "CustomPhysicsDriver.h"
#include "CustomMeshTransform.h"

// common headers
#include "ObstacleMgr.h"
#include "BaseBuilding.h"
#include "TextUtils.h"
#include "StateDatabase.h"

// bzfs specific headers
#include "bzfs.h"


BZWReader::BZWReader(std::string filename) : cURLManager(), location(filename),
					     input(NULL)
{
  static const std::string httpProtocol("http://");
  static const std::string ftpProtocol("ftp://");
  static const std::string fileProtocol("file:/");

  errorHandler = new BZWError(location);

  if ((filename.substr(0, httpProtocol.size()) == httpProtocol)
      || (filename.substr(0, ftpProtocol.size()) == ftpProtocol)
      || (filename.substr(0, fileProtocol.size()) == fileProtocol)) {
    setURL(location);
    performWait();
    input = new std::istringstream(httpData);
  } else {
    input = new std::ifstream(filename.c_str(), std::ios::in);
  }

  // .BZW is the official worldfile extension, warn for others
  if ((filename.length() < 4) ||
      (strcasecmp(filename.substr(filename.length() - 4, 4).c_str(),
		  ".bzw") != 0)) {
    errorHandler->warning(std::string(
      "world file extension is not .bzw, trying to load anyway"), 0);
  }

  if (input->peek() == EOF) {
    errorHandler->fatalError(std::string("could not find bzflag world file"), 0);
  }
}


BZWReader::~BZWReader()
{
  // clean up
  delete errorHandler;
  delete input;
}


void BZWReader::finalization(char *data, unsigned int length, bool good)
{
  if (good)
    httpData = std::string(data, length);
  else
    httpData = "";
}

void BZWReader::readToken(char *buffer, int n)
{
  int c = -1;

  // skip whitespace
  while (input->good() && (c = input->get()) != -1 && isspace(c) && c != '\n')
    ;

  // read up to whitespace or n - 1 characters into buffer
  int i = 0;
  if (c != -1 && c != '\n') {
    buffer[i++] = c;
    while (input->good() && i < n - 1 && (c = input->get()) != -1 && !isspace(c))
      buffer[i++] = (char)c;
  }

  // terminate string
  buffer[i] = 0;

  // put back last character we didn't use
  if (c != -1 && isspace(c))
    input->putback(c);
}


static bool parseNormalObject(const char* token, WorldFileObject** object)
{
  WorldFileObject* tmpObj = NULL;

  if (strcasecmp(token, "box") == 0) {
    tmpObj = new CustomBox;
  } else if (strcasecmp(token, "pyramid") == 0) {
    tmpObj = new CustomPyramid();
  } else if (strcasecmp(token, "base") == 0) {
    tmpObj = new CustomBase;
  } else if (strcasecmp(token, "link") == 0) {
    tmpObj = new CustomLink();
  } else if (strcasecmp(token, "mesh") == 0) {
    tmpObj = new CustomMesh;
  } else if (strcasecmp(token, "arc") == 0) {
    tmpObj = new CustomArc(false);
  } else if (strcasecmp(token, "meshbox") == 0) {
    tmpObj = new CustomArc(true);
  } else if (strcasecmp(token, "cone") == 0) {
    tmpObj = new CustomCone(false);
  } else if (strcasecmp(token, "meshpyr") == 0) {
    tmpObj = new CustomCone(true);
  } else if (strcasecmp(token, "sphere") == 0) {
    tmpObj = new CustomSphere;
  } else if (strcasecmp(token, "tetra") == 0) {
    tmpObj = new CustomTetra();
  } else if (strcasecmp(token, "weapon") == 0) {
    tmpObj = new CustomWeapon;
  } else if (strcasecmp(token, "zone") == 0) {
    tmpObj = new CustomZone;
  } else if (strcasecmp(token, "waterLevel") == 0) {
    tmpObj = new CustomWaterLevel;
  } else if (strcasecmp(token, "dynamicColor") == 0) {
    tmpObj = new CustomDynamicColor;
  } else if (strcasecmp(token, "textureMatrix") == 0) {
    tmpObj = new CustomTextureMatrix;
  } else if (strcasecmp(token, "material") == 0) {
    tmpObj = new CustomMaterial;
  } else if (strcasecmp(token, "physics") == 0) {
    tmpObj = new CustomPhysicsDriver;
  } else if (strcasecmp(token, "transform") == 0) {
    tmpObj = new CustomMeshTransform;
  }

  if (tmpObj != NULL) {
    *object = tmpObj;
    return true;
  } else {
    return false;
  }
}

bool BZWReader::readWorldStream(std::vector<WorldFileObject*>& wlist,
				GroupDefinition* groupDef)
{
  // make sure input is valid
  if (input->peek() == EOF) {
    errorHandler->fatalError(std::string("unexpected EOF"), 0);
    return false;
  }

  int line = 1;
  char buffer[1024];
  WorldFileObject* object = NULL;
  WorldFileObject* newObject = NULL;
  WorldFileObject* const fakeObject = (WorldFileObject*)((char*)NULL + 1);
  GroupDefinition* const worldDef = const_cast<GroupDefinition*>(OBSTACLEMGR.getWorld());
  GroupDefinition* const startDef = groupDef;

  std::string customObject;
  std::vector<std::string>	customLines;

  bool gotWorld = false;

  while (!input->eof() && !input->fail() && input->good())
  {
    // watch out for starting a new object when one is already in progress
    if (newObject) {
      if (object) {
	errorHandler->warning(
	  std::string("discarding incomplete object"), line);
	if (object != fakeObject) {
	  delete object;
	}
	else if (customObject.size())
	{
		customObject = "";
		customLines.clear();
	}
      }
      object = newObject;
      newObject = NULL;
    }

    // read first token but do not skip newlines
    readToken(buffer, sizeof(buffer));
    if (strcmp(buffer, "") == 0) {
      // ignore blank line
    } else if (buffer[0] == '#') {
      // ignore comment

    } else if (strcasecmp(buffer, "end") == 0) {
      if (object) {
	if (object != fakeObject) {
	  line += object->getLineCount();

	  if (object->usesManager()) {
	    object->writeToManager();
	    delete object;
	  } else if (object->usesGroupDef()) {
	    object->writeToGroupDef(groupDef);
	    delete object;
	  } else {
	    wlist.push_back(object);
	  }
	} else if (customObject.size())	{
	  bz_CustomMapObjectInfo data;
	  data.name = bz_ApiString(customObject);
	  for (unsigned int i = 0; i < customLines.size(); i++)
	    data.data.push_back(customLines[i]);
	  customObjectMap[customObject]->MapObject(bz_ApiString(customObject),&data);
	  object = NULL;
	}
	object = NULL;
      } else {
	errorHandler->fatalError(
	  std::string("unexpected \"end\" token"), line);
	return false;
      }

    } else if (parseNormalObject(buffer, &newObject)) {
      // newObject has already been assigned

    } else if (strcasecmp(buffer, "define") == 0) {
      if (groupDef != worldDef) {
	errorHandler->warning(
	  std::string("group definitions can not be nested \"") +
	  std::string(buffer) + std::string("\" - skipping"), line);
      } else {
	readToken(buffer, sizeof(buffer));
	if (strlen(buffer) > 0) {
	  if (OBSTACLEMGR.findGroupDef(buffer) != NULL) {
	    errorHandler->warning(
	      std::string("duplicate group definition \"") +
	      std::string(buffer) + std::string("\" - using newest"), line);
	  }
	  groupDef = new GroupDefinition(buffer);
	} else {
	  errorHandler->warning(
	    std::string("missing group definition name"), line);
	}
      }

    } else if (strcasecmp(buffer, "enddef") == 0) {
      if (groupDef == worldDef) {
	errorHandler->warning(
	  std::string("enddef without define - skipping"), line);
      } else {
	OBSTACLEMGR.addGroupDef(groupDef);
	groupDef = worldDef;
      }

    } else if (strcasecmp(buffer, "group") == 0) {
      readToken(buffer, sizeof(buffer));
      if (strlen(buffer) <= 0) {
	errorHandler->warning(
	  std::string("missing group definition reference"), line);
      }
      newObject = new CustomGroup(buffer);

    } else if (strcasecmp(buffer, "teleporter") == 0) {
      readToken(buffer, sizeof(buffer));
      newObject = new CustomGate(buffer);

    } else if (strcasecmp(buffer, "options") == 0) {
      newObject = fakeObject;

    } else if (strcasecmp(buffer, "include") == 0) {
      // NOTE: intentionally undocumented  (at the moment)
      readToken(buffer, sizeof(buffer));
      std::string incName = buffer;
      if (object == NULL) {
	// FIXME - check for recursion
	//       - better filename handling ("", spaces, and / vs. \\)
	//       - make relative names work from the base file location
	logDebugMessage(1,"%s: (line %i): including \"%s\"\n",
		location.c_str(), line, incName.c_str());
	BZWReader incFile(incName);
	std::vector<WorldFileObject*> incWlist;
	if (incFile.readWorldStream(incWlist, groupDef)) {
	  // add the included objects
	  for (unsigned int i = 0; i < incWlist.size(); i++) {
	    wlist.push_back(incWlist[i]);
	  }
	} else {
	  // empty the failed list
	  emptyWorldFileObjectList(incWlist);
	  errorHandler->fatalError(
	    TextUtils::format("including \"%s\"", incName.c_str()), line);
	  return false;
	}
      }
      else {
	errorHandler->warning(
	  TextUtils::format("including \"%s\" within an obstacle, skipping",
			    incName.c_str()), line);
      }

    } else if (strcasecmp(buffer, "world") == 0) {
      if (!gotWorld) {
	newObject = new CustomWorld();
	gotWorld = true;
      } else {
	errorHandler->warning(
	  std::string("multiple \"world\" sections found"), line);
      }

    } else if (object) {
      if (object != fakeObject) {
	if (!object->read(buffer, *input)) {
	  // unknown token
	  errorHandler->warning(
	    std::string("unknown object parameter \"") +
	    std::string(buffer) + std::string("\" - skipping"), line);
	  // delete object;
	  // return false;
	}
      } else if (customObject.size()) {
	std::string thisline = buffer;
	thisline += " ";

	while (input->good() && input->peek() != '\n') {
	  input->get(buffer, sizeof(buffer));
	  thisline += buffer;
	}

	thisline = TextUtils::replace_all(thisline,std::string("\r"),std::string(""));
	thisline = TextUtils::replace_all(thisline,std::string("\n"),std::string(""));
	customLines.push_back(thisline);
      }

    } else { // filling the current object
      // unknown token
      if (customObjectMap.find(TextUtils::toupper(std::string(buffer))) != customObjectMap.end()) {
	customObject = TextUtils::toupper(std::string(buffer));
	object = fakeObject;
	customLines.clear();
      }	else {
	errorHandler->warning(
	  std::string("invalid object type \"") +
	  std::string(buffer) + std::string("\" - skipping"), line);
	if (object != fakeObject)
	  delete object;
      }
      // return false;
    }

    // discard remainder of line
    while (input->good() && input->peek() != '\n')
      input->get(buffer, sizeof(buffer));
    input->getline(buffer, sizeof(buffer));
    ++line;
  }

  bool retval = true;
  if (object) {
    errorHandler->fatalError(std::string("missing \"end\" parameter"), line);
    if (object != fakeObject) {
      delete object;
    }
    retval = false;
  }
  if (groupDef != startDef) {
    errorHandler->fatalError(std::string("missing \"enddef\" parameter"), line);
    if (startDef == worldDef) {
      delete groupDef;
    }
    retval = false;
  }
  return retval;
}


WorldInfo* BZWReader::defineWorldFromFile()
{
  // create world object
  WorldInfo *myWorld = new WorldInfo;
  if (!myWorld) {
    errorHandler->fatalError(std::string("WorldInfo failed to initialize"), 0);
    return NULL;
  }

  // read file
  std::vector<WorldFileObject*> list;
  GroupDefinition* worldDef = const_cast<GroupDefinition*>(OBSTACLEMGR.getWorld());
  if (!readWorldStream(list, worldDef)) {
    emptyWorldFileObjectList(list);
    errorHandler->fatalError(std::string("world file failed to load."), 0);
    delete myWorld;
    return NULL;
  }

  if (!BZDB.isTrue("noWalls"))
    makeWalls();

  // generate group instances
  OBSTACLEMGR.makeWorld();

  // make local bases
  unsigned int i;
  const ObstacleList& baseList = OBSTACLEMGR.getBases();
  for (i = 0; i < baseList.size(); i++) {
    const BaseBuilding* base = (const BaseBuilding*) baseList[i];
    TeamColor color = (TeamColor)base->getTeam();
    if (bases.find(color) == bases.end()) {
      bases[color] = TeamBases((TeamColor)color);
    }
    bases[color].addBase(base->getPosition(), base->getSize(),
			 base->getRotation());
  }

  // add objects
  const unsigned int n = list.size();
  for (i = 0; i < n; ++i) {
    list[i]->writeToWorld(myWorld);
  }

  // clean up
  emptyWorldFileObjectList(list);
  myWorld->finishWorld();

  return myWorld;
}


// Local Variables: ***
// mode: C++ ***
// tab-width: 8 ***
// c-basic-offset: 2 ***
// indent-tabs-mode: t ***
// End: ***
// ex: shiftwidth=2 tabstop=8
