// --------------------------------------------------------------------
// The Ipe document.
// --------------------------------------------------------------------
/*

    This file is part of the extensible drawing editor Ipe.
    Copyright (C) 1993-2004  Otfried Cheong

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

    As a special exception, you have permission to link Ipe with the
    CGAL library and distribute executables, as long as you follow the
    requirements of the Gnu General Public License in regard to all of
    the software in the executable aside from CGAL.

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

    You should have received a copy of the GNU General Public License
    along with Ipe; if not, you can find it at
    "http://www.gnu.org/copyleft/gpl.html", or write to the Free
    Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

*/

#include "ipedoc.h"
#include "ipeiml.h"
#include "ipestyle.h"
#include "iperef.h"
#include "ipepainter.h"
#include "ipeutils.h"
#include "ipefontpool.h"
#include "ipepdfparser.h"
#include "ipepdfwriter.h"
#include "ipepswriter.h"

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

/*! \class IpeDocument
  \ingroup doc
  \brief The model for an Ipe document.

  The IpeDocument class is the engine behind the Ipe program.  It
  represents the contents of an Ipe document, and all the methods
  necessary to load, save, and edit it.  It is independent of a
  specific user interface.

  IpeDocument's cannot be copied or assigned.

  Note that the IpeDocument owns the IpeRepository that defines the
  meaning of all object's attributes.  Therefore, the document is the
  largest extent where IpeObject's make sense.  You can have several
  documents at once, but you cannot move IpeObject's (or
  IpeStyleSheet's) from one to the other. If you need to do so, you'll
  have to externalize them to XML and internalize them into the other
  document.
*/

//! Constructor clears the boolean flags.
IpeDocument::SProperties::SProperties()
{
  iFullScreen = false;
  iCropBox = false;
}

//! Construct an empty document for filling by a client.
/*! As constructed, it has no pages, A4 media, and
  only the standard style sheet. */
IpeDocument::IpeDocument()
{
  iFontPool = 0;
  iEdited = false;
  iStyleSheet = IpeStyleSheet::Standard(&iRepository);
  iProperties.iMedia = IpeRect(IpeVector::Zero, IpeVector(595,842)); // A4
}

//! Destructor.
IpeDocument::~IpeDocument()
{
  for (const_iterator it = begin(); it != end(); ++it)
    delete (*it);
  delete iStyleSheet;
  delete iFontPool;
}

//! Copy constructor is disabled: it panics.
IpeDocument::IpeDocument(const IpeDocument &)
  : IpePageSeq()
{
  assert(false);
}

//! Assignment operator is disabled: it panics.
IpeDocument &IpeDocument::operator=(const IpeDocument &)
{
  assert(false);
  return *this;
}

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

IpeString readLine(IpeDataSource &source)
{
  IpeString s;
  int ch = source.GetChar();
  while (ch != EOF && ch != '\n') {
    s += char(ch);
    ch = source.GetChar();
  }
  return s;
}

//! Determine format of file in \a source.
IpeDocument::TFormat IpeDocument::FileFormat(IpeDataSource &source)
{
  IpeString s1 = readLine(source);
  IpeString s2 = readLine(source);
  if (s1.substr(0, 5) == "<?xml" || s1.substr(0, 4) == "<ipe")
    return EXml;
  if (s1.substr(0, 4) == "%PDF")
    return EPdf;  // let's assume it contains an Ipe stream
  if (s1.substr(0, 4) == "%!PS") {
    if (s2.substr(0, 11) != "%%Creator: ")
      return EUnknown;
    IpeLex lex(s1);
    (void) lex.NextToken(); // %!PS-Adobe-Version
    bool eps = (lex.NextToken().substr(0, 4) == "EPSF");
    if (s2.substr(11, 6) == "Ipelib" || s2.substr(11, 4) == "xpdf")
      return eps ? EEps : EPs;
    if (s2.substr(11, 3) == "Ipe")
      return EIpe5;
    return EUnknown;
  }
  if (s1.substr(0, 5) == "%\\Ipe" || s1.substr(0, 6) == "%\\MIPE")
    return EIpe5;
  return EUnknown;
}

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

class IpePdfStreamParser : public IpeImlParser {
public:
  explicit IpePdfStreamParser(IpePdfFile &loader, IpeDataSource &source,
			      IpeRepository *rep);
  virtual IpeBuffer PdfStream(int objNum);
private:
  IpePdfFile &iLoader;
};

IpePdfStreamParser::IpePdfStreamParser(IpePdfFile &loader,
				       IpeDataSource &source,
				       IpeRepository *rep)
  : IpeImlParser(source, rep), iLoader(loader)
{
  // nothing
}

IpeBuffer IpePdfStreamParser::PdfStream(int objNum)
{
  const IpePdfObj *obj = iLoader.Object(objNum);
  if (!obj || !obj->Dict() || obj->Dict()->Stream().size() == 0)
    return IpeBuffer();
  return obj->Dict()->Stream();
}

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

class IpePsSource : public IpeDataSource {
public:
  IpePsSource(IpeDataSource &source) : iSource(source) { /* nothing */ }
  bool SkipToXml();
  IpeString ReadLine();
  IpeBuffer Image(int index) const;
  int GetNext() const;
  inline bool Deflated() const { return iDeflated; }

  virtual int GetChar();
private:
  IpeDataSource &iSource;
  std::vector<IpeBuffer> iImages;
  bool iEos;
  bool iDeflated;
};

int IpePsSource::GetChar()
{
  int ch = iSource.GetChar();
  if (ch == '\n')
    iSource.GetChar(); // remove '%'
  return ch;
}

IpeString IpePsSource::ReadLine()
{
  IpeString s;
  int ch = iSource.GetChar();
  while (ch != EOF && ch != '\n') {
    s += char(ch);
    ch = iSource.GetChar();
  }
  iEos = (ch == EOF);
  return s;
}

IpeBuffer IpePsSource::Image(int index) const
{
  if (1 <= index && index <= int(iImages.size()))
    return iImages[index - 1];
  else
    return IpeBuffer();
}

bool IpePsSource::SkipToXml()
{
  iDeflated = false;

  IpeString s1 = ReadLine();
  IpeString s2 = ReadLine();

  if (s1.substr(0, 11) != "%!PS-Adobe-" ||
      s2.substr(0, 11) != "%%Creator: ")
    return false;

  if (s2.substr(11, 6) == "Ipelib") {
    // the 'modern' file format of Ipe 6.0 preview 17 and later
    do {
      s1 = ReadLine();
      if (s1.substr(0, 17) == "%%BeginIpeImage: ") {
	IpeLex lex(s1.substr(17));
	int num, len;
	lex >> num >> len;
	if (num != int(iImages.size() + 1))
	  return false;
	(void) ReadLine();  // skip 'image'
	IpeBuffer buf(len);
	IpeA85Source a85(iSource);
	char *p = buf.data();
	char *p1 = p + buf.size();
	while (p < p1) {
	  int ch = a85.GetChar();
	  if (ch == EOF)
	    return false;
	  *p++ = char(ch);
	}
	iImages.push_back(buf);
      }
    } while (!iEos && s1.substr(0, 13) != "%%BeginIpeXml");

    iDeflated = (s1.substr(13, 14) == ": /FlateDecode");

  } else {
    // the 'old' file format generated through pdftops
    do {
      s1 = ReadLine();
    } while (!iEos && s1.substr(0, 10) != "%%EndSetup");
  }
  if (iEos)
    return false;
  (void) iSource.GetChar(); // skip '%' before <ipe>
  return true;
}

class IpePsStreamParser : public IpeImlParser {
public:
  explicit IpePsStreamParser(IpeDataSource &source, IpePsSource &psSource, IpeRepository *rep);
  virtual IpeBuffer PdfStream(int objNum);
private:
  IpePsSource &iPsSource;
};

IpePsStreamParser::IpePsStreamParser(IpeDataSource &source, IpePsSource &psSource, IpeRepository *rep)
  : IpeImlParser(source, rep), iPsSource(psSource)
{
  // nothing
}

IpeBuffer IpePsStreamParser::PdfStream(int objNum)
{
  return iPsSource.Image(objNum);
}


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

IpeDocument *DoParse(IpeDocument *self, IpeImlParser &parser, int &reason)
{
  int requires;
  if (!parser.ParseDocument(*self, requires) || requires > IPELIB_VERSION) {
    delete self;
    self = 0;
    if (requires > IPELIB_VERSION)
      reason = -requires;
    else
      reason = parser.ParsePosition();
  }
  return self;
}

IpeDocument *DoParseXml(IpeDataSource &source, int &reason)
{
  IpeDocument *self = new IpeDocument;
  IpeImlParser parser(source, self->Repository());
  return DoParse(self, parser, reason);
}

IpeDocument *DoParsePs(IpeDataSource &source, int &reason)
{
  IpePsSource psSource(source);
  reason = -3; // could not find Xml stream
  if (!psSource.SkipToXml())
    return 0;

  IpeDocument *self = new IpeDocument;
  if (psSource.Deflated()) {
    IpeA85Source a85(psSource);
    IpeInflateSource source(a85);
    IpePsStreamParser parser(source, psSource, self->Repository());
    return DoParse(self, parser, reason);
  } else {
    IpePsStreamParser parser(psSource, psSource, self->Repository());
    return DoParse(self, parser, reason);
  }
}

IpeDocument *DoParsePdf(IpeDataSource &source, int &reason)
{
  IpePdfFile loader;
  reason = -2; // could not parse
  if (!loader.Parse(source))
    return 0;

  reason = -3; // not an Ipe document
  const IpePdfObj *obj = loader.Object(1);
  if (!obj || !obj->Dict())
    return 0;
  const IpePdfObj *type = obj->Dict()->Get("Type", 0);
  if (!type || !type->Name() || type->Name()->Value() != "Ipe")
    return 0;

  IpeBuffer buffer = obj->Dict()->Stream();
  IpeBufferSource xml(buffer);

  IpeDocument *self = new IpeDocument;
  if (obj->Dict()->Deflated()) {
    IpeInflateSource xml1(xml);
    IpePdfStreamParser parser(loader, xml1, self->Repository());
    return DoParse(self, parser, reason);
  } else {
    IpePdfStreamParser parser(loader, xml, self->Repository());
    return DoParse(self, parser, reason);
  }
}

//! Construct a document from an input stream.
/*! Returns 0 if the stream couldn't be parsed, and a reason
  explaining that in \a reason.  If \a reason is positive, it is a
  file (stream) offset where parsing failed.  If \a reason is
  negative, it is an error code. If it is smaller than -60000, then it
  is the negative value of the Ipe version that created that file (and
  which exceeds the version of this Ipe).
*/
IpeDocument *IpeDocument::New(IpeDataSource &source, TFormat format,
			      int &reason)
{
  if (format == EXml)
    return DoParseXml(source, reason);

  if (format == EPdf)
    return DoParsePdf(source, reason);

  if (format == EPs || format == EEps)
    return DoParsePs(source, reason);

  return 0;
}

IpeDocument *IpeDocument::New(const char *fname, int &reason)
{
  reason = -1;
  std::FILE *fd = std::fopen(fname, "rb");
  if (!fd)
    return 0;
  IpeFileSource source(fd);
  TFormat format = FileFormat(source);
  std::rewind(fd);
  IpeDocument *self = New(source, format, reason);
  std::fclose(fd);
  return self;
}

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

//! Save in a stream.
/*! Returns true if sucessful.

  Does not reset IsEdited().
*/
bool IpeDocument::Save(IpeTellStream &stream, IpeString creator,
		       TFormat format, uint flags,
		       int compressLevel) const
{
  if (format == EXml) {
    SaveAsXml(stream, creator);
    return true;
  }

  if (format == EPdf) {
    IpePdfWriter writer(stream, this, (flags & ENoShading), compressLevel);
    writer.EmbedFonts(iFontPool);
    writer.CreatePages(flags & ELastView);
    writer.CreateBookmarks();
    if (!(flags & EExport)) {
      IpeString xmlData;
      IpeStringStream stream(xmlData);
      if (compressLevel > 0) {
	IpeDeflateStream dfStream(stream, compressLevel);
	// all bitmaps have been embedded and carry correct object number
	SaveAsXml(dfStream, creator, true);
	dfStream.Close();
	writer.CreateXmlStream(xmlData, true);
      } else {
	SaveAsXml(stream, creator, true);
	writer.CreateXmlStream(xmlData, false);
      }
    }
    writer.CreateTrailer(creator);
    return true;
  }

  if (format == EPs || format == EEps) {
    IpePsWriter writer(stream, this, (format == EEps));
    if (!writer.CreateHeader(creator))
      return false;
    writer.CreatePages(flags & ELastView);
    if (!(flags & EExport))
      writer.CreateXml(creator, compressLevel);
    writer.CreateTrailer();
    return true;
  }

  return false;
}

bool IpeDocument::Save(const char *fname, IpeString creator,
		       TFormat format, uint flags,
		       int compressLevel) const
{
  std::FILE *fd = std::fopen(fname, "wb");
  if (!fd)
    return false;
  IpeFileStream stream(fd);
  bool result = Save(stream, creator, format, flags, compressLevel);
  std::fclose(fd);
  return result;
}

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

//! Save in XML format into an IpeStream.
/*! You can set \a creator to set the \c creator attribute in the
  \c <ipe> tag. */
void IpeDocument::SaveAsXml(IpeStream &stream, IpeString creator,
			    bool usePdfBitmaps) const
{
  stream << "<ipe version=\"" << IPELIB_VERSION << "\"";
  if (!creator.empty())
    stream << " creator=\"" << creator << "\"";
  stream << " media=\"" << iProperties.iMedia.Min() << " "
	 << iProperties.iMedia.Max() << "\"";
  stream << ">\n";
  IpeString info;
  IpeStringStream infoStr(info);
  infoStr << "<info";
  if (!iProperties.iCreated.empty())
    infoStr << " created=\"" << iProperties.iCreated << "\"";
  if (!iProperties.iModified.empty())
    infoStr << " modified=\"" << iProperties.iModified << "\"";
  if (!iProperties.iTitle.empty()) {
    infoStr << " title=\"";
    infoStr.PutXmlString(iProperties.iTitle);
    infoStr << "\"";
  }
  if (!iProperties.iAuthor.empty()) {
    infoStr << " author=\"";
    infoStr.PutXmlString(iProperties.iAuthor);
    infoStr << "\"";
  }
  if (!iProperties.iSubject.empty()) {
    infoStr << " subject=\"";
    infoStr.PutXmlString(iProperties.iSubject);
    infoStr << "\"";
  }
  if (!iProperties.iKeywords.empty()) {
    infoStr << " keywords=\"";
    infoStr.PutXmlString(iProperties.iKeywords);
    infoStr << "\"";
  }
  if (iProperties.iFullScreen) {
    infoStr << " pagemode=\"fullscreen\"";
  }
  if (iProperties.iCropBox) {
    infoStr << " bbox=\"cropbox\"";
  }
  infoStr << "/>\n";
  if (info.size() > 10)
    stream << info;

  if (!iProperties.iPreamble.empty()) {
    stream << "<preamble>";
    stream.PutXmlString(iProperties.iPreamble);
    stream << "</preamble>\n";
  }
  if (iStyleSheet)
    iStyleSheet->SaveCascadeAsXml(stream);
  // save bitmaps
  IpeBitmapFinder bm;
  for (const_iterator it = begin(); it != end(); ++it)
    bm.ScanPage(*it);
  if (!bm.iBitmaps.empty()) {
    int id = 1;
    IpeBitmap prev;
    std::sort(bm.iBitmaps.begin(), bm.iBitmaps.end());
    for (std::vector<IpeBitmap>::iterator it = bm.iBitmaps.begin();
	 it != bm.iBitmaps.end(); ++it) {
      if (!it->Equal(prev)) {
	if (usePdfBitmaps) {
	  it->SaveAsXml(stream, it->ObjNum(), it->ObjNum());
	} else {
	  it->SaveAsXml(stream, id);
	  it->SetObjNum(id);
	}
      } else
	it->SetObjNum(prev.ObjNum()); // noop if prev == it
      prev = *it;
      ++id;
    }
  }
  // save pages
  IpePainter painter(iStyleSheet);
  for (const_iterator it = begin(); it != end(); ++it)
    (*it)->SaveAsXml(painter, stream);
  stream << "</ipe>\n";
}

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

//! Return true if document has been edited since last save.
bool IpeDocument::IsEdited() const
{
  if (iEdited)
    return true;
  for (const_iterator it = begin(); it != end(); ++it) {
    if ((*it)->IsEdited())
      return true;
  }
  return false;
}

//! Set whether document has been edited.
/*! Methods that can modify the document already set the flag, so you
  only need to call this when inserting or deleting pages, etc.  When
  modifying an IpePage, rather call IpePage::SetEdited(true).

  Clients need to manually reset the edited flag when they save the
  document, or after constructing it during loading.

  Calling this with \c edited == \c false will call
  IpePage::SetEdited(false) for all pages.
*/

void IpeDocument::SetEdited(bool edited)
{
  iEdited = edited;
  if (!edited) {
    for (iterator it = begin(); it != end(); ++it)
      (*it)->SetEdited(false);
  }
}

//! Set document properties.
void IpeDocument::SetProperties(const SProperties &props)
{
  iProperties = props;
  iEdited = true;
}

//! Replace the style sheet cascade.
/*! The previous contents is not deleted (because this function is
  often used to insert style sheets into the cascade).

  Sets the edited flag. */
void IpeDocument::SetStyleSheet(IpeStyleSheet *sheet)
{
  iStyleSheet = sheet;
  iEdited = true;
}

//! Check all symbolic attributes in the document.
/*!  This function verifies that all symbolic attributes in the
  document are defined in the style sheet. It appends to \a seq all
  symbolic attributes (in no particular order, but with not
  duplicates) that are NOT defined.

  Returns \c true if there are no undefined symbolic attributes in the
  document.
*/
bool IpeDocument::CheckStyle(IpeAttributeSeq &seq) const
{
  for (const_iterator it = begin(); it != end(); ++it) {
    const IpePage *page = *it;
    for (IpePage::const_iterator it1 = page->begin(); it1 != page->end();
	 ++it1) {
      it1->Object()->CheckStyle(StyleSheet(), seq);
    }
  }
  return (seq.size() == 0);
}

bool IpeDocument::HasTrueTypeFonts() const
{
  if (iFontPool) {
    for (IpeFontPool::const_iterator it = iFontPool->begin();
	 it != iFontPool->end(); ++it) {
      if (it->iType == IpeFont::ETrueType)
	return true;
    }
  }
  return false;
}

//! Update the font pool (after running Pdflatex).
/*! Takes ownership of the font pool. */
void IpeDocument::SetFontPool(IpeFontPool *fontPool)
{
  delete iFontPool;
  iFontPool = fontPool;
}

//! Load a style sheet and add at top of cascade.
bool IpeDocument::AddStyleSheet(IpeDataSource &source)
{
  IpeImlParser parser(source, Repository());
  IpeStyleSheet *sheet = parser.ParseStyleSheet();
  if (sheet) {
    sheet->SetCascade(GetStyleSheet());
    SetStyleSheet(sheet);
    return true;
  } else
    return false;
}

//! Return total number of views in all pages.
int IpeDocument::TotalViews() const
{
  int views = 0;
  for (const_iterator it = begin(); it != end(); ++it) {
    int nviews = (*it)->CountViews();
    views += (nviews > 0) ? nviews : 1;
  }
  return views;
}

//! Create a new empty page with standard settings.
IpePage *IpeDocument::NewPage(int gridSize)
{
  // Create one empty page
  IpePage *page = new IpePage;
  IpeAttribute bga =
    Repository()->MakeSymbol(IpeAttribute::ETemplate, "background");
  IpeView view;
  if (StyleSheet()->FindTemplate(bga)) {
    IpeLayer bglayer("background");
    bglayer.SetLocked(true);
    page->AddLayer(bglayer);
    IpeObject *obj = new IpeReference(StyleSheet(), bga);
    page->push_back(IpePgObject(IpePgObject::ENone, 0, obj));
    view.AddLayer("background");
  }
  page->AddLayer(IpeLayer("alpha"));
  view.AddLayer("alpha");
  view.SetActive("alpha");
  page->AddView(view);
  page->SetGridSize(gridSize);
  return page;
}

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