/***********************************************************************
generate-preamble.cpp v1.01 (31 March 2020)

Copyright 2020 Herman Johannes Haverkort

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

This file is part of the pftrail package. 
The package includes the following files:

* pftrail.cpp
* ifs-classics
* ifs-inventions
* generate-preamble.cpp
* colours
* postamble
* manual.pdf

New versions of the package may be available from 
http://spacefillingcurves.net or http://herman.haverkort.net.

***********************************************************************/

const char* pftrailInfo = 
"generate-preamble, part of pftrail v1.01 by Herman Haverkort.";
 
#include <string>
using std::string;
#include <sstream>
using std::stringstream;
#include <iostream>
using std::ostream;
using std::cout;
using std::cerr;
using std::endl;
#include <fstream>
using std::ifstream;
#include <cstdlib>
#include <vector>
#include <algorithm>
using std::min;
using std::max;
#include <cmath>

/*********************************************************
* Input/output headers                                   *
*********************************************************/

enum ErrorLevel 
{ 
  warning, 
  userError, 
  parsingError, 
  usageError, 
  internalError 
};

class Complaint 
// an object to which an error message can be witten with <<. 
// when the object is destroyed, the error message is produced
// and, if it is not a mere warning, the program halts.
{
    private:
  static const char* errorLabel[];
  stringstream complaint;
  ErrorLevel errorLevel;
    public:
  Complaint(ErrorLevel _errorLevel = userError);
  Complaint(ErrorLevel _errorLevel, const string& expected, const string& read);
  ~Complaint();
  template <typename T> Complaint& operator<<(const T& t);
};

class CommentedInput 
// an object that reads a file and while skipping comments.
// Comments are from // to the end of the line, or 
// between /* and */; in the latter case, /* has to be at
// the beginning of the line.
{
    private:
  ifstream source;
  stringstream buffer;
  void fillBuffer();

    public:
  CommentedInput(string& fileName);
  bool fail() const;
  template < class T > CommentedInput& operator>>(T& t);
  void getLine(string& s);
  void getLowerCaseWord(string& s);

  // The following method scans the input and stops after 
  // reading two consecutive words keyword and name 
  // (case-insensitive). Returns true iff the combination
  // was found. 
  // More precisely: after an occurrence of keyword 
  // the next word is compared to name; if it is not a 
  // match, the word is ignored. So, keyword keyword name
  // is not a match, as the first occurrence of keyword
  // "consumes" the second.
  bool find(const char* keyword, const string& name);
};

void makeLowerCase(string& s);

void usage();

void cutId(const string id, string& fileName, string& name);

void skip(CommentedInput& text, const char* s);


/*********************************************************
* Colour headers                                         *
*********************************************************/

struct Colour
{
  double red, green, blue, emission, opacity;
  Colour();
  Colour operator+(const Colour& c) const;
  Colour operator*(const double d) const;
  string phong() const;
};

CommentedInput& operator>>(CommentedInput& i, Colour& c);

ostream& operator<<(ostream& s, const Colour& c);


/*********************************************************
* main header                                            *
*********************************************************/

int main(const int argc, const char* argv[]);


/*********************************************************
**********************************************************
**                                                      **
** IMPLEMENTATION                                       **
**                                                      **
**********************************************************
*********************************************************/


/*********************************************************
* Input/Output                                           *
*********************************************************/

const char* Complaint::errorLabel[5] = 
{ 
  "Warning", 
  "Error",
  "Error while parsing colour scheme", 
  "Error", 
  "Internal error" 
};

Complaint::Complaint(ErrorLevel _errorLevel): 
  errorLevel(_errorLevel) 
{
};
  
Complaint::Complaint(
  ErrorLevel _errorLevel, 
  const string& expected, 
  const string& read
):
  errorLevel(_errorLevel)
{
  complaint << "expected \"" << expected << "\", but read \"" << read << "\".";
}

Complaint::~Complaint()
{
  cerr << errorLabel[(int) errorLevel] << ": " << complaint.str() << endl;
  if (errorLevel == usageError) usage();
  if (errorLevel > warning) exit((int) errorLevel); 
}

template <typename T> Complaint& Complaint::operator<<(const T& t)
{
  complaint << t; return *this;
}

void CommentedInput::fillBuffer()
{
  string text;
  while (text.empty())
  {
    std::getline(source, text);
    if (source.fail()) return;
    // remove long comments
    if (text.substr(0,2) == "/*")
    {
      text.erase(0,2);
      while (text.find("*/") > text.length())
      {
        std::getline(source, text);
        if (source.fail()) 
        {
          Complaint(warning) << "unclosed comment in file";
          return;
        }
      }
      text.erase(0, text.find("*/") + 2);
    }
    // remove short comments:
    text.erase(std::min(text.find("//"), text.length()));
  }
  buffer.clear();
  buffer.str(text);
}

CommentedInput::CommentedInput(string& fileName):
  source(fileName.c_str())
{};

bool CommentedInput::fail() const
{
  return buffer.fail() || source.fail();
}

template < class T > CommentedInput& CommentedInput::operator>>(T& t)
{
  if (source.fail()) return *this;
  buffer >> t;
  while (buffer.fail() && buffer.eof() && !source.fail()) 
  {
    fillBuffer();
    buffer >> t;
  }
  return *this;
}

void CommentedInput::getLine(string& s)
{
  if (source.fail()) return;
  std::getline(buffer, s);
  if (s.empty()) 
  {
    fillBuffer();
    std::getline(buffer, s);
  }
}

void CommentedInput::getLowerCaseWord(string& s)
{
  *this >> s; makeLowerCase(s);
}

bool CommentedInput::find(const char* keyword, const string& name)
{
  string lowerCaseKeyword(keyword); makeLowerCase(lowerCaseKeyword);
  string lowerCaseName(name); makeLowerCase(lowerCaseName);
  string inputWord;
  while (!fail() && inputWord != lowerCaseName)
  {
    while (!fail() && inputWord != lowerCaseKeyword) 
      getLowerCaseWord(inputWord);
    if (fail()) return false;
    getLowerCaseWord(inputWord);
  }
  return (!fail() && inputWord == lowerCaseName);
}

void makeLowerCase(string& s)
{
  for (int i = 0; i < s.length(); ++i) s[i] = tolower(s[i]);
}

void usage()
{
  cerr << pftrailInfo << endl
       << "Usage: generate-preamble <resolution> <file>[:<name>]" << endl
       << "Output: COLLADA file preamble including camera, light, effects and "
       << "materials libraries. Concatenate preamble, pftrail output, and postamble "
       << "to produce a COLLADA file that can be imported in Blender." << endl
       << endl
       << "<resolution>  number of colours in the desired colour gradient" << endl
       << "              for the trail surface" << endl
       << "<file>        name of a file with pftrail colour schemes" << endl
       << "<name>        name of the colour scheme, if the file contains" << endl
       << "              more than one" << endl
       << endl;
  exit(-1);
}

void cutId(const string id, string& fileName, string& name)
{
  fileName = id;
  name.erase();
  int cut = fileName.find(':');
  if (cut >= fileName.length()) return;
  name = fileName.substr(cut+1);
  fileName.erase(cut);
}

void skip(CommentedInput& text, const char* s)
{
  string aword;
  text.getLowerCaseWord(aword);
  if (text.fail() || aword != s) Complaint(parsingError, s, aword);
}


/*********************************************************
* Colour implementation                                  *
*********************************************************/

Colour::Colour() 
{};

Colour Colour::operator+(const Colour& c) const
{
  Colour sum;
  sum.red      = red      + c.red;
  sum.green    = green    + c.green;
  sum.blue     = blue     + c.blue;
  sum.emission = emission + c.emission;
  sum.opacity  = opacity  + c.opacity;
  return sum;
}

Colour Colour::operator*(const double d) const
{
  Colour scaled;
  scaled.red      = d * red;
  scaled.green    = d * green;
  scaled.blue     = d * blue;
  scaled.emission = d * emission;
  scaled.opacity  = d * opacity;
  return scaled; 
}

string Colour::phong() const 
{
  stringstream s;
  s
<< "    <profile_COMMON>" << endl
<< "      <technique sid=\"phong1\">" << endl
<< "        <phong>" << endl
<< "          <emission>      <color>"
<< red   * emission << ' ' 
<< green * emission << ' ' 
<< blue  * emission << ' ' 
<< "1.0</color> </emission>" << endl
<< "          <ambient>       <color>0.0 0.0 0.0 1.0</color> </ambient>" << endl
<< "          <diffuse>       <color>"
<< red   << ' ' 
<< green << ' ' 
<< blue  << ' '
<< "1.0</color> </diffuse>" << endl 
<< "          <specular>      <color>0.0 0.0 0.0 1.0</color> </specular>" << endl
<< "          <shininess>     <float>0</float>               </shininess>" << endl
<< "          <reflective>    <color>1.0 1.0 1.0 1.0</color> </reflective>" << endl
<< "          <reflectivity>  <float>0.02</float>            </reflectivity>" << endl
<< "          <transparent>   <color>1.0 1.0 1.0 1.0</color> </transparent>" << endl
<< "          <transparency>  <float>"
<< opacity
<< "</float> </transparency>" << endl
<< "        </phong>" << endl
<< "      </technique>" << endl
<< "    </profile_COMMON>" << endl
  ;
  return s.str();
}

CommentedInput& operator>>(CommentedInput& i, Colour& c)
{
  return i >> c.red >> c.green >> c.blue >> c.emission >> c.opacity;
}

ostream& operator<<(ostream& s, const Colour& c)
{
  return s << c.red << ' ' << c.green << ' ' << c.blue << ' ' << c.emission << ' ' << c.opacity;
}


/*********************************************************
* main                                                   *
*********************************************************/

int main(const int argc, const char* argv[])
{
  if (argc < 3) Complaint(usageError) << "too few arguments on command line.";
   
  // get desired number of output colours from command line
  int outputSize = atoi(argv[1]);
  if (outputSize < 1) 
    Complaint(usageError) 
      << "Invalid number of output colours on command line (" << outputSize << ").";

  // find colour scheme in file
  string colourSchemeFileName, colourSchemeName;
  cutId(argv[2], colourSchemeFileName, colourSchemeName);
  CommentedInput colourSchemeFile(colourSchemeFileName);
  if (colourSchemeFile.fail()) 
    Complaint(userError) << "cannot read file \"" << colourSchemeFileName << "\".";
  if (
    !colourSchemeName.empty() && 
    !colourSchemeFile.find("scheme", colourSchemeName)
  )
    Complaint(userError) << "Colour scheme \"" << colourSchemeName 
      << "\" not found in file \"" << colourSchemeFileName << "\".";

  // skip any (alternative) names of the colour scheme:
  string aword;
  while (!colourSchemeFile.fail())
  {
    colourSchemeFile.getLowerCaseWord(aword);
    if (aword == "scheme") colourSchemeFile >> aword; else break;
  }
 
  // read trail colours
  if (aword != "trail") Complaint(parsingError, "trail", aword);
  std::vector< Colour > inputTrailColours;
  while (true)
  {
    inputTrailColours.push_back(Colour());
    colourSchemeFile >> inputTrailColours.back();
    if (colourSchemeFile.fail())
      Complaint(parsingError) << "could not read trail colour.";
    colourSchemeFile >> aword;
    if (aword == ",") continue;
    if (aword == ";") break;
    Complaint(parsingError, ",\" or \".", aword);
  }
  if (inputTrailColours.size() == 1) 
    inputTrailColours.push_back(inputTrailColours[0]);  
  int inputSize = inputTrailColours.size();

  // read other colours
  skip(colourSchemeFile, "lowland"); 
  Colour lowlandColour; colourSchemeFile >> lowlandColour;
  if (colourSchemeFile.fail()) 
    Complaint(parsingError) << "could not read lowland colour.";
  skip(colourSchemeFile, ";");

  skip(colourSchemeFile, "highland"); 
  Colour highlandColour; colourSchemeFile >> highlandColour;
  if (colourSchemeFile.fail()) 
    Complaint(parsingError) << "could not read highland colour.";
  skip(colourSchemeFile, ";");

  skip(colourSchemeFile, "walls"); 
  Colour wallsColour; colourSchemeFile >> wallsColour;
  if (colourSchemeFile.fail()) 
    Complaint(parsingError) << "could not read wall colour.";
  skip(colourSchemeFile, ";");

  skip(colourSchemeFile, "parapets"); 
  Colour parapetsColour; colourSchemeFile >> parapetsColour;
  if (colourSchemeFile.fail()) 
    Complaint(parsingError) << "could not read parapet colour.";
  skip(colourSchemeFile, ".");

  // generate output trail colour table
  std::vector< Colour > trailColours;
  if (outputSize == 1) 
    trailColours.push_back(inputTrailColours.back());
  else
    for (int i = 0; i < outputSize; ++i)
    {
      double scaledIndex = (double) i / (double) (outputSize-1) * (inputSize - 1);
      int firstColour = floor(scaledIndex);
      firstColour = std::max(0, firstColour);
      firstColour = std::min((int) inputSize-2, firstColour);
      int secondColour = firstColour + 1;
      double firstFraction = secondColour - scaledIndex;
      double secondFraction = scaledIndex - firstColour;
      trailColours.push_back(
        inputTrailColours[firstColour] * firstFraction + 
        inputTrailColours[secondColour] * secondFraction
      );
    } 
   
  // generate COLLADA preamble
  cout
<< "<?xml version=\"1.0\" encoding=\"utf-8\"?>" << endl
<< "<COLLADA xmlns=\"http://www.collada.org/2005/11/COLLADASchema\" version=\"1.4.1\">" << endl
<< "" << endl
<< "<!-- " << endl
<< "Preamble for path-filling trail model generated by " << pftrailInfo << endl
<< endl
<< "Input colours:" << endl
<< endl
<< "trail    " << inputTrailColours[0];
  for (int i = 1; i < inputTrailColours.size(); ++i)
    cout << " ," << endl << "         " << inputTrailColours[i];
  cout
<< " ;" << endl
<< "lowland  " << lowlandColour << " ;" << endl
<< "highland " << highlandColour << " ;" << endl
<< "walls    " << wallsColour << " ;" << endl
<< "parapets " << parapetsColour << " ." << endl
<< "-->" << endl
<< "<asset>" << endl
<< "  <up_axis>Y_UP</up_axis>" << endl
<< "</asset>" << endl
<< "" << endl
<< "<library_cameras>" << endl
<< "  <camera id=\"TheCamera\">" << endl
<< "    <optics>" << endl
<< "      <technique_common>" << endl
<< "        <perspective>" << endl
<< "          <yfov>37</yfov>" << endl
<< "          <aspect_ratio>1</aspect_ratio>" << endl
<< "          <znear>0.1</znear>" << endl
<< "          <zfar>100</zfar>" << endl
<< "        </perspective>" << endl
<< "      </technique_common>" << endl
<< "    </optics>" << endl
<< "  </camera>" << endl
<< "</library_cameras>" << endl
<< "" << endl
<< "<library_lights>" << endl
<< "  <light id=\"TopLight\">" << endl
<< "    <technique_common>" << endl
<< "      <point>" << endl
<< "        <color>1 1 1</color>" << endl
<< "      </point>" << endl
<< "    </technique_common>" << endl
<< "    <extra>" << endl
<< "      <technique profile=\"blender\">" << endl
<< "        <red sid=\"red\" type=\"float\">1</red>" << endl
<< "        <green sid=\"green\" type=\"float\">1</green>" << endl
<< "        <blue sid=\"blue\" type=\"float\">1</blue>" << endl
<< "        <energy sid=\"blender_energy\" type=\"float\">300</energy>" << endl
<< "        <area_size sid=\"area_size\" type=\"float\">1.0</area_size>" << endl
<< "      </technique>" << endl
<< "    </extra>" << endl
<< "  </light>" << endl
<< "</library_lights>" << endl
<< "" << endl
<< "<library_effects>" << endl
<< "  <effect id=\"LowerPlanePhong\">" << endl
<< lowlandColour.phong()
<< "  </effect>" << endl
<< "  <effect id=\"UpperPlanePhong\">" << endl
<< highlandColour.phong()
<< "  </effect>" << endl
<< "  <effect id=\"WallPhong\">" << endl
<< wallsColour.phong()
<< "  </effect>" << endl
<< "  <effect id=\"ParapetPhong\">" << endl
<< parapetsColour.phong()
<< "  </effect>" << endl
  ;
  for (int i = 0; i < outputSize; ++i)
    cout
<< "  <effect id=\"Path" << i << "Phong\">" << endl
<< trailColours[i].phong()
<< "  </effect>" << endl
    ;
  cout
<< "</library_effects>" << endl
<< "" << endl
<< "<library_materials>" << endl
<< "  <material id=\"LowerPlaneMaterial\">" << endl
<< "    <instance_effect url=\"#LowerPlanePhong\"/>" << endl
<< "  </material>" << endl
<< "  <material id=\"UpperPlaneMaterial\">" << endl
<< "    <instance_effect url=\"#UpperPlanePhong\"/>" << endl
<< "  </material>" << endl
<< "  <material id=\"WallMaterial\">" << endl
<< "    <instance_effect url=\"#WallPhong\"/>" << endl
<< "  </material>" << endl
<< "  <material id=\"ParapetMaterial\">" << endl
<< "    <instance_effect url=\"#ParapetPhong\"/>" << endl
<< "  </material>" << endl
  ;
  for (int i = 0; i < outputSize; ++i)
    cout
<< "  <material id=\"PathMaterial" << i << "\">" << endl
<< "    <instance_effect url=\"#Path" << i << "Phong\"/>" << endl
<< "  </material>" << endl
    ;
  cout
<< "</library_materials>" << endl
<< "<!-- End of generate-preamble output. Append pftrail output here. -->" << endl
  ;
}
