// -*-c++-*-

// fixg2sxd - a utility to convert fig to sxd format

// Copyright (C) 2003-2022 Alexander Bürger, acfb@users.sourceforge.net

// This program 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.
//
// This program 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 this program; if not, write to the Free Software
// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

#include "xfigobjects.h"
#include "colors.h"
#include "misc.h"
#include "xmlwrite.h"
#include "zipwrite.h"

#include <cstdlib>
#include <clocale>
#include <string>
#include <cstring>
#include <ctime>
#include <strings.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>

#define SOME_STYLES_ONLY_IN_CONTENT 1

using namespace std;

static int papersize;
static enum { Portrait, Landscape } orientation = Portrait;

static const int MIN_STYLEBASE = 4, MAX_STYLEBASE=255;

static const char* papersizes[15] = {
    "Letter", "Legal", "Ledger", "Tabloid",
    "A", "B", "C", "D", "E", "A4", "A3", "A2", "A1", "A0", "B5"
};

// all sizes in cm
static float paperwidths[15] =  {
    21.59 /*Letter*/, 21.59 /*Legal*/, 10, 27.958/*Tabloid*/,
    10, 10, 10, 10, 10, 21.0/*A4*/, 29.7, 42.0, 59.4, 84.1
};
static float paperheights[15] = {
    27.94 /*Letter*/, 35.565/*Legal*/, 10, 43.127/*Tabloid*/,
    10, 10, 10, 10, 10, 29.7/*A4*/, 42.0, 59.4, 84.1, 118.9
};

static vector<XfigObject*> xfigobjects;

static const char* xmlheader =
    "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";

static const char* xmlnamespaces =
    " xmlns:draw=\"http://openoffice.org/2000/drawing\""
    " xmlns:style=\"http://openoffice.org/2000/style\""
    " xmlns:office=\"http://openoffice.org/2000/office\""
    " xmlns:text=\"http://openoffice.org/2000/text\""
    " xmlns:svg=\"http://www.w3.org/2000/svg\""
    " xmlns:fo=\"http://www.w3.org/1999/XSL/Format\""
    " xmlns:dc=\"http://purl.org/dc/elements/1.1/\""
    " xmlns:meta=\"http://openoffice.org/2000/meta\""
    " xmlns:xlink=\"http://www.w3.org/1999/xlink\"";

static void write_file( ostream& sxdfile )
{
    float pw = paperwidths[papersize];
    float ph = paperheights[papersize];
    if( orientation == Landscape )
        swap( pw, ph );

    Node styles("office:styles");
    Node autostyles("office:automatic-styles");

    Node& linebase = styles.subnode("style:style");
    linebase["style:name"] << LineFillStyle::base;
    linebase["style:family"] << "graphics";
    linebase["style:parent-style-name"] << "standard";

    Node& textbase = styles.subnode("style:style");
    textbase["style:name"] << TextStyle::base;
    textbase["style:parent-style-name"] << "standard";
    textbase["style:family"] << "graphics"; // paragraph
    textbase["style:class"] << "text";
    Node& textbaseprop = textbase.subnode("style:properties");
    textbaseprop["fo:font-family"] << "Helvetica";
    textbaseprop["fo:font-weight"] << "normal";
    textbaseprop["fo:font-style"] << "normal";
    textbaseprop["fo:color"] << "#000000";
    textbaseprop["fo:font-size"] << "10pt";
    textbaseprop["fo:padding-left"] << "0cm";
    textbaseprop["fo:padding-right"] << "0cm";
    textbaseprop["fo:padding-top"] << "0cm";
    textbaseprop["fo:padding-bottom"] << "0cm";
    textbaseprop["draw:fill"] << "none";
    textbaseprop["draw:stroke"] << "none";

#ifndef SOME_STYLES_ONLY_IN_CONTENT
    // does not work here as of OOo 2.4.0
    Node& narrow = autostyles.subnode("style:style");
    narrow["style:name"] << "P1";
    narrow["style:family"] << "paragraph";
    Node& narrowprop = narrow.subnode("style:properties");
    narrowprop["style:text-scale"] << "90%";
#endif

    for( set<TextStyle>::iterator i=textstyles.begin();
         i!=textstyles.end(); ++i )
        i->write( styles );
    for( arrowset::iterator i=arrows.begin();
         i!=arrows.end(); ++i )
        i->write( styles );
    for( set<LineFillStyle>::iterator i=linefillstyles.begin();
         i!=linefillstyles.end(); ++i )
        i->write( styles );

    Node& pm = autostyles.subnode("style:page-master");
    pm["style:name"] << "PM1";
    Node& pmprop = pm.subnode( "style:properties" );
    pmprop["fo:margin-top"] << "0cm";
    pmprop["fo:margin-bottom"] << "0cm";
    pmprop["fo:margin-left"] << "0cm";
    pmprop["fo:margin-right"] << "0cm";
    pmprop["fo:page-width"] << pw << "cm";
    pmprop["fo:page-height"] << ph << "cm";
    pmprop["style:print-orientation"]
        << (orientation == Landscape ? "landscape" : "portrait");

    Node& dp1 = autostyles.subnode("style:style");
    dp1["style:family"] << "drawing-page";
    dp1["style:name"] << "dp1";
    Node& dp1prop = dp1.subnode("style:properties");
    dp1prop["draw:background-size"] << "border";
    dp1prop["draw:fill"] << "none";

    Node& img = autostyles.subnode("style:style");
    img["style:family"] << "graphics";
    img["style:name"] << "gr_img";
    Node& imgprop = img.subnode("style:properties");
    imgprop["draw:stroke"] << "none";
    imgprop["draw:fill"] << "none";

    if( xfigobjects.size()>0 ) {
        // loop over all objects to find minmal/maximal depth
        vector<XfigObject*>::iterator i=xfigobjects.begin();
        depth_max = (*i)->depth;
        while( ++i != xfigobjects.end() )
            depth_max = max( depth_max, (*i)->depth );
    }

    ZipWriter sxd( sxdfile );

    // write mimetype
    sxd.GetStream("mimetype") << "application/vnd.sun.xml.draw" << flush;

    { // write manifest
        Node manifest("manifest:manifest");
        manifest["xmlns:manifest"] << "http://openoffice.org/2001/manifest";
        Node& zip = manifest.subnode("manifest:file-entry");
        zip["manifest:media-type"] << "application/vnd.sun.xml.draw";
        zip["manifest:full-path"] << "/";
        Node& cx = manifest.subnode("manifest:file-entry");
        cx["manifest:media-type"] << "text/xml";
        cx["manifest:full-path"] << "content.xml";
        Node& sx = manifest.subnode("manifest:file-entry");
        sx["manifest:media-type"] << "text/xml";
        sx["manifest:full-path"] << "styles.xml";
        Node& mx = manifest.subnode("manifest:file-entry");
        mx["manifest:media-type"] << "text/xml";
        mx["manifest:full-path"] << "meta.xml";
        sxd.GetStream("META-INF/manifest.xml" )
            << xmlheader << endl
            << "<!DOCTYPE manifest:manifest PUBLIC "
            "\"-//OpenOffice.org//DTD Manifest 1.0//EN\" \"Manifest.dtd\">"
            << endl
            << manifest;
    }

    { // write meta.xml
        time_t now = time(0);
        char creationtime[30];
        strftime( creationtime, sizeof(creationtime), "%Y-%m-%dT%H:%M:%S",
                  localtime(&now) );
        sxd.GetStream("meta.xml")
            << xmlheader << endl
            << "<!DOCTYPE office:document-meta PUBLIC"
            " \"-//OpenOffice.org//DTD OfficeDocument 1.0//EN\""
            " \"office.dtd\">" << endl
            << "<office:document-meta" << xmlnamespaces
            << " office:version=\"1.0\">" << endl
            << " <office:meta>" << endl
            << "  <meta:generator>fig2sxd</meta:generator>" << endl
            << "  <meta:creation-date>" << creationtime
            << "</meta:creation-date>" << endl
            << " </office:meta>" << endl
            << "</office:document-meta>" << endl;
    }

    { // write content.xml
        ostream& contentxml = sxd.GetStream( "content.xml" );
        contentxml << xmlheader << endl
                   << "<!DOCTYPE office:document-content PUBLIC"
            " \"-//OpenOffice.org//DTD OfficeDocument 1.0//EN\""
            " \"office.dtd\">" << endl
                   << "<office:document-content" << xmlnamespaces
                   << " office:version=\"1.0\" office:class=\"drawing\">"
                   << endl;

#ifdef SOME_STYLES_ONLY_IN_CONTENT
        // for narrow text, scaling must be defined here; other places
        // do not work as of OOo 2.4.0
        Node auto2("office:automatic-styles");
        Node& P1 = auto2.subnode("style:style");
        P1["style:name"] << "P1";
        P1["style:family"] << "paragraph";
        Node& P1prop = P1.subnode("style:properties");
        P1prop["style:text-scale"] << "90%";
        contentxml << auto2;
#endif

        contentxml << "<office:body>" << endl
                   << "<draw:page draw:name=\"page1\""
                   << " draw:master-page-name=\"Standard\""
                   << " draw:style-name=\"dp1\">" << endl;
        for( vector<XfigObject*>::iterator i=xfigobjects.begin();
             i != xfigobjects.end(); ++i )
            (*i)->write( contentxml );
        contentxml << "</draw:page>" << endl
                   << "</office:body>" << endl
                   << "</office:document-content>" << endl;
    }

    { // write styles.xml
        ostream& stylesxml = sxd.GetStream( "styles.xml" );
        stylesxml << xmlheader << endl
                  << "<!DOCTYPE office:document-styles PUBLIC"
            " \"-//OpenOffice.org//DTD OfficeDocument 1.0//EN\""
            " \"office.dtd\">" << endl
                  << "<office:document-styles" << xmlnamespaces
                  << " office:version=\"1.0\">" << endl
                  << styles
                  << autostyles
                  << "<office:master-styles>" << endl
                  << "  <draw:layer-set>" << endl
                  << "   <draw:layer draw:name=\"layout\"/>" << endl
                  << "   <draw:layer draw:name=\"background\"/>" << endl
                  << "   <draw:layer draw:name=\"backgroundobjects\"/>" << endl
                  << "   <draw:layer draw:name=\"controls\"/>" << endl
                  << "   <draw:layer draw:name=\"measurelines\"/>" << endl
                  << "  </draw:layer-set>" << endl
                  << " <style:master-page style:name=\"Standard\""
                  << "  style:page-master-name=\"PM1\""
                  << "  draw:style-name=\"dp1\"/>" << endl
                  << "</office:master-styles>" << endl
                  << "</office:document-styles>" << endl;
    }
}

static void read_file( istream& figfile )
{
    string linebuf;

    // check if it's a fig file; first line is a comment, but we
    // should not skip it
    getline( figfile, linebuf );
    if( linebuf.compare( 0, 8, "#FIG 3.2" ) != 0 ) {
        cerr << "linebuf =>" << linebuf << "<=" << endl;
        fail( "Not a figfile or unknown fig format (only 3.2 is known)." );
    }

    // read orientation
    skip_comment( figfile );
    getline( figfile, linebuf );
    if( linebuf == "Landscape" ) {
        orientation = Landscape;
    } else if( linebuf != "Portrait" ) {
        fail( "Bad orientation." );
    }

    // read justification
#ifdef UNUSED_VARIABLES
    enum { Center, FlushLeft } justification = Center;
#endif
    skip_comment( figfile );
    getline( figfile, linebuf );
    if( linebuf == "Flush left" ) {
#ifdef UNUSED_VARIABLES
        justification = FlushLeft;
#endif
    } else if( linebuf != "Center" ) {
        fail( "Bad justification." );
    }

    // read units
#ifdef UNUSED_VARIABLES
    enum { Metric, Inches } units = Metric;
#endif
    skip_comment( figfile );
    getline( figfile, linebuf );
    if( linebuf == "Inches" ) {
#ifdef UNUSED_VARIABLES
        units = Inches;
#endif
    } else if( linebuf != "Metric" ) {
        fail( "Bad units." );
    }

    // read papersize
    skip_comment( figfile );
    string papersize_name;
    figfile >> papersize_name;
    for( papersize=0; papersize<15; ++papersize ) {
        if( strcasecmp( papersize_name.c_str(), papersizes[papersize] ) == 0 )
            break;
    }
    if( papersize == 15 )
        fail( "Bad papersize." );

    // read magnification
    skip_comment( figfile );
    float magnification;
    figfile >> magnification;
    figfile.ignore( 1024, '\n' );

    // read multipageness
    skip_comment( figfile );
#ifdef UNUSED_VARIABLES
    enum { Single, Multiple } multipage = Single;
#endif
    getline( figfile, linebuf );
    if( linebuf == "Multiple" ) {
#ifdef UNUSED_VARIABLES
        multipage = Multiple;
#endif
    } else if( linebuf != "Single" ) {
        fail( "Bad multipageness." );
    }

    // read transparent color
    skip_comment( figfile );
    int transparentcolor = -5;
    figfile >> transparentcolor;
    if( transparentcolor < -3 || transparentcolor > 543 )
        fail( "Bad transparent color number." );

    // skip optional comment
    skip_comment( figfile );

    // read resolution and coordinate system id
    int coord_system;
    figfile >> resolution >> coord_system;
    if( resolution < 1 || coord_system != 2 )
        fail( "Bad resolution / coord_system." );

    while( 1 ) {
        // ignore comments; returns EOF if file ends
        skip_comment( figfile );
        if( figfile.eof() )
            break;

        int object_code;
        figfile >> object_code;
        switch( object_code ) {
        case 0:
            read_color( figfile );
            break;
        case 1: {
            Ellipse* e = new Ellipse();
            e->read( figfile );
            xfigobjects.push_back( e );
            break; }
        case 2: {
            Poly* p = new Poly();
            p->read( figfile );
            xfigobjects.push_back( p );
            break; }
        case 3: {
            Spline* s = new Spline();
            s->read( figfile );
            xfigobjects.push_back( s );
            break; }
        case 4: {
            Text* t = new Text();
            t->read( figfile );
            xfigobjects.push_back( t );
            break; }
        case 5: {
            Arc* a = new Arc();
            a->read( figfile );
            xfigobjects.push_back( a );
            break; }
        case 6: {
            OpenCompound* c = new OpenCompound();
            c->read( figfile );
            xfigobjects.push_back( c );
            break; }
        case -6: {
            CloseCompound* c = new CloseCompound();
            c->read( figfile );
            xfigobjects.push_back( c );
            break; }
        default: {
            ostringstream err;
            err << "Unknown object code: " << object_code;
            throw err.str(); }
        }
    }
}

static void usage( const char* cmdname, bool err=true )
{
    ostream& out = (err ? cerr : cout);
    out << cmdname << " [-w] [-l(ine)w(idth)1 l] [-stylebase s] figfile [sxdfile]" << endl << endl
        << "Using `-' as figfile makes the program read from stdin." << endl
        << endl
        << "Omitting sxdfile when figfile ends with .fig or .xfig, makes"<<endl
        << "the sxd file be named like figfile with .(x)fig replaced by .sxd."
        << endl << endl
        << "With -linewidth1 or -lw1 the with of lines with thickness 1 in xfig" << endl
        << "can be set (in cm), e.g. to 0 to have fine lines." << endl << endl
        << "With -w files with out-of-specification values will be accepted," << endl
        << "but a warning will be printed and the bad values sanitized." << endl << endl
        << "With -stylebase the \"Fig2Sxd\" prefix for styles can be replaced." << endl
        << "This can be useful if several converted figures are joined in" << endl
        << "the same document. The replacement prefix must consist of " << MIN_STYLEBASE << ".."
        << MAX_STYLEBASE << endl << "alphanumeric characters." << endl << endl
        << "This is fig2sxd version 0.23 (C) 2003-2022 Alexander Bürger."
        << endl;
}

static bool option( const char* option, const char* &value,
                    int &argc, char** argv )
{
    bool found = false;
    for( int a=1; a<argc; a++ ) {
        if( strcmp( option, argv[a] ) == 0 && a+1 < argc ) {
            value = argv[a+1];
            for( int aa=a; aa<argc-2; aa++ )
                argv[aa] = argv[aa+2];
            argc -= 2;
            a--;
            found = true;
        }
    }
    return found;
}

#if 1
static bool option0( const char* option, int &argc, char** argv )
{
    bool found = false;
    for( int a=1; a<argc; a++ ) {
        if( strcmp( option, argv[a] ) == 0 ) {
            for( int aa=a; aa<argc-1; aa++ )
                argv[aa] = argv[aa+1];
            argc -= 1;
            a--;
            found = true;
        }
    }
    return found;
}
#endif

extern bool out_of_range_error;

int try_catched_main( int argc, char* argv[] )
{
    setlocale( LC_NUMERIC, "C" );

    const char* tmp;
    if( option( "-linewidth1", tmp, argc, argv )
        | option( "-lw1", tmp, argc, argv ) )
    {
        float w = atof( tmp );
        if( w<0 )
            fail( "linewith must be >= 0" );
        LineFillStyle::linewith1 = w;
    }
    if( option( "-stylebase", tmp, argc, argv ) ) {
        const int length = strlen(tmp);
        bool okay = length>=MIN_STYLEBASE && length<=MAX_STYLEBASE;
        for(int i=0; okay && i<length; ++i) {
            if( !isalnum(tmp[i]) )
                okay = false;
        }
        if( okay ) {
            ostringstream lbase;
            lbase << tmp << "Line";
            LineFillStyle::base = lbase.str();
            ostringstream tbase;
            tbase << tmp << "Text";
            TextStyle::base = tbase.str();
        } else {
            cerr << "The style prefix must consist of " << MIN_STYLEBASE << ".."
                 << MAX_STYLEBASE <<" alphanumeric characters." << endl;
            exit( EXIT_FAILURE );
        }
    }
    if( option0( "-w", argc, argv ) ) {
        out_of_range_error = false;
        cerr << "Out of range values will be sanitized." << endl;
    }

    bool dotfig=false, dotxfig=false;
    string figfilename;
    unsigned int ffnl=0;
    if( argc>=2 ) {
        figfilename = argv[1];
        ffnl = figfilename.length();
    }
    if( argc==2 ) {
        dotfig  = ( ffnl>4 && figfilename.rfind( ".fig")+4==ffnl );
        dotxfig = ( ffnl>5 && figfilename.rfind(".xfig")+5==ffnl );
    }
    if( argc != 3 && !dotfig && !dotxfig ) {
        usage( argv[0], true );
        exit( EXIT_FAILURE );
    }

    initcolors();

    // read xfig file / stdin
    if( figfilename != "-" ) {
        ifstream infile( figfilename.c_str() );
        read_file( infile );
    } else {
        read_file( std::cin );
    }

    // write sxd file / stdout
    string sxd;
    if( dotfig )
        sxd = figfilename.substr(0,ffnl-4)+".sxd";
    else if( dotxfig )
        sxd = figfilename.substr(0,ffnl-5)+".sxd";
    else
        sxd = argv[2];
    if( sxd.empty() )
        throw string( "output filename is empty" );
    if( sxd != "-" ) {
        ofstream sxdfile( sxd.c_str(), ios::binary );
        write_file( sxdfile );
    } else {
        write_file( std::cout );
    }
    return EXIT_SUCCESS;
}

int main( int argc, char* argv[] )
{
    try {
        return try_catched_main( argc, argv );
    } catch( string msg ) {
        cerr << msg << endl << endl
             << "If you think this is message is caused by a problem in fig2sxd," << endl
             << "please contact the author(s) via http://fig2sxd.sourceforge.net/."  << endl
             << "Thank you." << endl << endl
             << "To get your file converted anyway, you could try the '-w' option." << endl;
        return EXIT_FAILURE;
    }
}
