/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/*
 * This file is part of the libe-book project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include <cassert>

#include <libxml/xmlreader.h>

#include "libebook_utils.h"
#include "libebook_xml.h"

#include "FB2BinaryContext.h"
#include "FB2BlockContext.h"
#include "FB2Collector.h"
#include "FB2ContentCollector.h"
#include "FB2ContentMap.h"
#include "FB2ExtrasCollector.h"
#include "FB2Parser.h"
#include "FB2MetadataCollector.h"
#include "FB2MetadataContext.h"
#include "FB2TableContext.h"
#include "FB2TextContext.h"
#include "FB2Token.h"

namespace libebook
{

namespace
{

class DocumentContext : public FB2ParserContext
{
  // no copying
  DocumentContext(const DocumentContext &other);
  DocumentContext &operator=(const DocumentContext &other);

public:
  DocumentContext(FB2ContentMap &notes, FB2ContentMap &bitmaps, librevenge::RVNGTextInterface *document = 0);

private:
  virtual FB2XMLParserContext *leaveContext() const;

  virtual FB2XMLParserContext *element(const FB2TokenData &name, const FB2TokenData &ns);
  virtual void startOfElement();
  virtual void endOfElement();
  virtual void attribute(const FB2TokenData &name, const FB2TokenData *ns, const char *value);
  virtual void endOfAttributes();
  virtual void text(const char *text);

private:
  librevenge::RVNGTextInterface *const m_document;
  FB2ContentMap &m_notes;
  FB2ContentMap &m_bitmaps;
  bool m_generating;
};

class FictionBookGeneratorContext : public FB2NodeContextBase
{
  // no copying
  FictionBookGeneratorContext(const FictionBookGeneratorContext &other);
  FictionBookGeneratorContext &operator=(const FictionBookGeneratorContext &other);

public:
  FictionBookGeneratorContext(FB2ParserContext *parentContext, const FB2ContentMap &notes, const FB2ContentMap &bitmaps, librevenge::RVNGTextInterface *document);

private:
  virtual FB2XMLParserContext *element(const FB2TokenData &name, const FB2TokenData &ns);
  virtual void startOfElement();
  virtual void endOfElement();
  virtual void attribute(const FB2TokenData &name, const FB2TokenData *ns, const char *value);

private:
  librevenge::RVNGTextInterface *const m_document;
  librevenge::RVNGPropertyList m_metadata;
  FB2MetadataCollector m_metadataCollector;
  FB2ContentCollector m_contentCollector;
  bool m_bodyRead;
};

class FictionBookGathererContext : public FB2NodeContextBase
{
  // no copying
  FictionBookGathererContext(const FictionBookGathererContext &other);
  FictionBookGathererContext &operator=(const FictionBookGathererContext &other);

public:
  FictionBookGathererContext(FB2ParserContext *parentContext, FB2ContentMap &notes, FB2ContentMap &bitmaps);

private:
  virtual FB2XMLParserContext *element(const FB2TokenData &name, const FB2TokenData &ns);
  virtual void endOfElement();
  virtual void attribute(const FB2TokenData &name, const FB2TokenData *ns, const char *value);

private:
  FB2ContentMap &m_notes;
  FB2ContentMap &m_bitmaps;
  FB2ExtrasCollector m_collector;
  bool m_firstBody;
};

class StylesheetContext : public FB2NodeContextBase
{
public:
  explicit StylesheetContext(FB2ParserContext *parentContext);

private:
  virtual FB2XMLParserContext *element(const FB2TokenData &name, const FB2TokenData &ns);
  virtual void endOfElement();
  virtual void attribute(const FB2TokenData &name, const FB2TokenData *ns, const char *value);
};

}

namespace
{

FictionBookGeneratorContext::FictionBookGeneratorContext(
  FB2ParserContext *const parentContext,
  const FB2ContentMap &notes, const FB2ContentMap &bitmaps,
  librevenge::RVNGTextInterface *const document)
  : FB2NodeContextBase(parentContext)
  , m_document(document)
  , m_metadata()
  , m_metadataCollector(m_metadata)
  , m_contentCollector(m_document, m_metadata, notes, bitmaps)
  , m_bodyRead(false)
{
}

FB2XMLParserContext *FictionBookGeneratorContext::element(const FB2TokenData &name, const FB2TokenData &ns)
{
  if (FB2Token::NS_FICTIONBOOK == getFB2TokenID(ns))
  {
    switch (getFB2TokenID(name))
    {
    case FB2Token::stylesheet :
      // ignore
      break;
    case FB2Token::description :
      return new FB2DescriptionContext(this, &m_metadataCollector);
    case FB2Token::body :
    {
      if (!m_bodyRead)
      {
        m_document->startDocument(librevenge::RVNGPropertyList());
        m_document->setDocumentMetaData(m_metadata);
        m_bodyRead = true;
        return new FB2BodyContext(this, &m_contentCollector, true);
      }
    }
    break;
    default :
      break;
    }
  }

  return new FB2SkipElementContext(this);
}

void FictionBookGeneratorContext::startOfElement()
{
}

void FictionBookGeneratorContext::endOfElement()
{
  m_document->endDocument();
}

void FictionBookGeneratorContext::attribute(const FB2TokenData &, const FB2TokenData *, const char *)
{
}

FictionBookGathererContext::FictionBookGathererContext(FB2ParserContext *const parentContext, FB2ContentMap &notes, FB2ContentMap &bitmaps)
  : FB2NodeContextBase(parentContext)
  , m_notes(notes)
  , m_bitmaps(bitmaps)
  , m_collector(m_notes, m_bitmaps)
  , m_firstBody(true)
{
}

FB2XMLParserContext *FictionBookGathererContext::element(const FB2TokenData &name, const FB2TokenData &ns)
{
  if (FB2Token::NS_FICTIONBOOK == getFB2TokenID(ns))
  {
    switch (getFB2TokenID(name))
    {
    case FB2Token::body :
    {
      if (m_firstBody)
        m_firstBody = false;
      else
        return new FB2BodyContext(this, &m_collector, false);
    }
    break;
    case FB2Token::binary :
      return new FB2BinaryContext(this, &m_collector);
    default:
      break;
    }
  }

  return new FB2SkipElementContext(this);
}

void FictionBookGathererContext::endOfElement()
{
}

void FictionBookGathererContext::attribute(const FB2TokenData &, const FB2TokenData *, const char *)
{
}

StylesheetContext::StylesheetContext(FB2ParserContext *const parentContext)
  : FB2NodeContextBase(parentContext)
{
}

FB2XMLParserContext *StylesheetContext::element(const FB2TokenData &name, const FB2TokenData &ns)
{
  // TODO: implement me
  if (FB2Token::NS_FICTIONBOOK == getFB2TokenID(ns))
  {
    switch (getFB2TokenID(name))
    {
    default :
      break;
    }
  }

  return new FB2SkipElementContext(this);
}

void StylesheetContext::endOfElement()
{
  // TODO: implement me
}

void StylesheetContext::attribute(const FB2TokenData &name, const FB2TokenData *ns, const char *value)
{
  // TODO: implement me
  (void) value;
  if (FB2_NO_NAMESPACE(ns))
  {
    switch (getFB2TokenID(name))
    {
    default :
      break;
    }
  }
}

DocumentContext::DocumentContext(FB2ContentMap &notes, FB2ContentMap &bitmaps, librevenge::RVNGTextInterface *const document)
  : FB2ParserContext(0)
  , m_document(document)
  , m_notes(notes)
  , m_bitmaps(bitmaps)
  , m_generating(document != 0)
{
}

FB2XMLParserContext *DocumentContext::element(const FB2TokenData &name, const FB2TokenData &ns)
{
  if ((FB2Token::NS_FICTIONBOOK == getFB2TokenID(ns)) && (FB2Token::FictionBook == getFB2TokenID(name)))
  {
    if (m_generating)
      return new FictionBookGeneratorContext(this, m_notes, m_bitmaps, m_document);
    else
      return new FictionBookGathererContext(this, m_notes, m_bitmaps);
  }

  return 0;
}

FB2XMLParserContext *DocumentContext::leaveContext() const
{
  return 0;
}

void DocumentContext::startOfElement()
{
}

void DocumentContext::endOfElement()
{
}

void DocumentContext::attribute(const FB2TokenData &, const FB2TokenData *, const char *)
{
}

void DocumentContext::endOfAttributes()
{
}

void DocumentContext::text(const char *)
{
}

}

namespace
{

void processAttribute(FB2XMLParserContext *const context, const xmlTextReaderPtr reader)
{
  const FB2TokenData *const name = getFB2Token(xmlTextReaderConstLocalName(reader));
  const xmlChar *const nsUri = xmlTextReaderConstNamespaceUri(reader);
  const FB2TokenData *const ns = nsUri ? getFB2Token(nsUri) : 0;
  if (name && (FB2Token::NS_XMLNS != getFB2TokenID(ns))) // ignore unknown attributes and namespace decls
    context->attribute(*name, ns, reinterpret_cast<const char *>(xmlTextReaderConstValue(reader)));
}

FB2XMLParserContext *processNode(FB2XMLParserContext *const context, const xmlTextReaderPtr reader)
{
  FB2XMLParserContext *newContext = context;
  switch (xmlTextReaderNodeType(reader))
  {
  case XML_READER_TYPE_ELEMENT :
  {
    const xmlChar *name_str = xmlTextReaderConstLocalName(reader);
    const xmlChar *ns_str = xmlTextReaderConstNamespaceUri(reader);

    const FB2TokenData *name = name_str ? getFB2Token(name_str) : 0;
    const FB2TokenData *ns = ns_str ? getFB2Token(ns_str) : 0;

    if (!name || !ns)
      // TODO: unknown elements should not be skipped entirely, but
      // their content should be processed as if they did not exist.
      // Unfortunately this would be quite hard (if not impossible) to
      // do in the current parser framework
      newContext = new FB2SkipElementContext(dynamic_cast<FB2ParserContext *>(context));
    else
      newContext = context->element(*name, *ns);

    if (newContext)
    {
      newContext->startOfElement();
      const bool isEmpty = xmlTextReaderIsEmptyElement(reader);

      if (xmlTextReaderHasAttributes(reader))
      {
        int ret = xmlTextReaderMoveToFirstAttribute(reader);
        while (1 == ret)
        {
          processAttribute(newContext, reader);
          ret = xmlTextReaderMoveToNextAttribute(reader);
        }
        if (0 > ret) // some error while reading
        {
          delete newContext;
          newContext = 0;
        }
      }
      if (newContext)
        newContext->endOfAttributes();

      if (newContext && isEmpty)
      {
        newContext->endOfElement();
        newContext = newContext->leaveContext();
      }
    }

    break;
  }
  case XML_READER_TYPE_ATTRIBUTE :
    assert(false && "How did i ever got there?");
    processAttribute(context, reader);
    break;
  case XML_READER_TYPE_END_ELEMENT :
  {
    context->endOfElement();
    newContext = context->leaveContext();
    break;
  }
  case XML_READER_TYPE_TEXT :
  {
    xmlChar *const text = xmlTextReaderReadString(reader);
    context->text(reinterpret_cast<char *>(text));
    xmlFree(text);
    break;
  }
  default :
    // ignore other types of XML content
    break;
  }

  return newContext;
}

}

FB2Parser::FB2Parser(librevenge::RVNGInputStream *input)
  : m_input(input)
{
  assert(m_input);
}

bool FB2Parser::parse(FB2XMLParserContext *const context) const
{
  m_input->seek(0, librevenge::RVNG_SEEK_SET);

  const xmlTextReaderPtr reader = xmlReaderForIO(ebookXMLReadFromStream, ebookXMLCloseStream, m_input, "", 0, 0);
  if (!reader)
    return false;

  int ret = xmlTextReaderRead(reader);
  FB2XMLParserContext *currentContext = context;
  while ((1 == ret) && currentContext)
  {
    currentContext = processNode(currentContext, reader);
    if (currentContext)
      ret = xmlTextReaderRead(reader);
  }

  xmlTextReaderClose(reader);
  xmlFreeTextReader(reader);

  // we processed all input and it was valid
  return (!currentContext || (currentContext == context)) && m_input->isEnd();
}

bool FB2Parser::parse(librevenge::RVNGTextInterface *const document) const
{
  FB2ContentMap notes;
  FB2ContentMap bitmaps;

  {
    // in the 1st pass we gather notes and bitmaps
    DocumentContext context(notes, bitmaps);
    if (!parse(&context))
      return false;
  }

  DocumentContext context(notes, bitmaps, document);
  return parse(&context);
}

}

/* vim:set shiftwidth=2 softtabstop=2 expandtab: */
