/*
* Copyright (c) 2018-2025 (https://github.com/phase1geo/Minder)
*
* 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., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA
*
* Authored by: Trevor Williams <phase1geo@gmail.com>
*/

using GLib;
using Gdk;
using Gee;

public class ExportFreemind : Export {

  //-------------------------------------------------------------
  // Constructor
  public ExportFreemind() {
    base( "freemind", _( "Freemind" ), { ".mm" }, true, true, false, false );
  }

  //-------------------------------------------------------------
  // Exports the given drawing area to the file of the given name
  public override bool export( string fname, MindMap map ) {
    Xml.Doc*  doc  = new Xml.Doc( "1.0" );
    doc->set_root_element( export_map( map ) );
    doc->save_format_file( fname, 1 );
    delete doc;
    return( false );
  }

  //-------------------------------------------------------------
  // Generates the header for the document
  private Xml.Node* export_map( MindMap map ) {
    Xml.Node* xmap = new Xml.Node( null, "map" );
    xmap->new_prop( "version", "1.0.1" );
    xmap->add_child( new Xml.Node.comment( _( "Generated by Minder" ) + " " + Minder.version ) );
    var nodes = map.get_nodes();
    for( int i=0; i<nodes.length; i++ ) {
      xmap->add_child( export_node( nodes.index( i ), map ) );
    }
    return( xmap );
  }

  //-------------------------------------------------------------
  // Exports the given node information
  private Xml.Node* export_node( Node node, MindMap map ) {

    Xml.Node* n = new Xml.Node( null, "node" );

    n->new_prop( "ID", "id_" + node.id().to_string() );
    n->new_prop( "TEXT", node.name.text.text );
    if( node.linked_node != null ) {
      n->new_prop( "LINK", "#id_" + node.linked_node.node_id.to_string() );
    }
    n->new_prop( "FOLDED", node.folded.to_string() );
    if( node.link_color != null ) {
      n->new_prop( "COLOR", Utils.color_from_rgba( node.link_color ) );
    }
    n->new_prop( "POSITION", ((node.side == NodeSide.LEFT) ? "left" : "right") );
    if( node.is_in_sequence() ) {
      n->new_prop( "NUMBERED", "true" );
    }

    if( node.group ) {
      n->add_child( export_cloud( node, map ) );
    }

    n->add_child( export_edge( node, map ) );
    n->add_child( export_font( node, map ) );

    if( node.note != "" ) {
      n->add_child( export_note( node ) );
    }

    // Add arrowlinks
    int         index = 0;
    Connection? conn  = null;
    while( (conn = map.connections.get_attached_connection( node, index++ )) != null ) {
      if( conn.from_node == node ) {
        n->add_child( export_arrowlink( conn, map ) );
      }
    }

    // Add nodes
    for( int i=0; i<node.children().length; i++ ) {
      n->add_child( export_node( node.children().index( i ), map ) );
    }

    return( n );

  }

  //-------------------------------------------------------------
  // Exports the cloud information
  private Xml.Node* export_cloud( Node node, MindMap map ) {
    Xml.Node* n = new Xml.Node( null, "cloud" );
    return( n );
  }

  //-------------------------------------------------------------
  // Exports the given node link as an edge
  private Xml.Node* export_edge( Node node, MindMap map ) {
    Xml.Node* n = new Xml.Node( null, "edge" );
    n->new_prop( "STYLE", (node.style.link_type.name() == "curved") ? "bezier" : "linear" );
    if( node.link_color != null ) {
      n->new_prop( "COLOR", Utils.color_from_rgba( node.link_color ) );
    }
    n->new_prop( "WIDTH", node.style.link_width.to_string() );
    return( n );
  }

  //-------------------------------------------------------------
  // Exports the given node font
  private Xml.Node* export_font( Node node, MindMap map ) {
    Xml.Node* n = new Xml.Node( null, "font" );
    n->new_prop( "NAME",   node.style.node_font.get_family() );
    n->new_prop( "SIZE",   (node.style.node_font.get_size() / Pango.SCALE).to_string() );
    n->new_prop( "BOLD",   ((node.name.text.text.substring( 0, 3 ) == "<b>") || (node.name.text.text.substring( 0, 6 ) == "<i><b>")).to_string() );
    n->new_prop( "ITALIC", ((node.name.text.text.substring( 0, 3 ) == "<i>") || (node.name.text.text.substring( 0, 6 ) == "<b><i>")).to_string() );
    return( n );
  }

  //-------------------------------------------------------------
  // Exports the node's note as Freemind XML data.
  private Xml.Node* export_note( Node node ) {

    Xml.Node* rc   = new Xml.Node( null, "richcontent" );
    Xml.Node* html = new Xml.Node( null, "html" );
    Xml.Node* head = new Xml.Node( null, "head" );

    var note_html = Utils.markdown_to_html( node.note, "body" );
    var note_doc  = Xml.Parser.parse_memory( note_html, note_html.length );
    var body      = note_doc->get_root_element()->copy( 1 );

    html->add_child( head );
    html->add_child( body );

    rc->new_prop( "TYPE", "NOTE" );
    rc->add_child( html );

    delete note_doc;

    return( rc );

  }

  //-------------------------------------------------------------
  // Exports the given connection as an arrowlink
  private Xml.Node* export_arrowlink( Connection conn, MindMap map ) {
    Xml.Node* n = new Xml.Node( null, "arrowlink" );
    if( conn.color != null ) {
      n->new_prop( "COLOR", Utils.color_from_rgba( conn.color ) );
    } else {
      n->new_prop( "COLOR", Utils.color_from_rgba( map.get_theme().get_color( "connection_background" ) ) );
    }
    n->new_prop( "DESTINATION", "id_" + conn.to_node.id().to_string() );
    n->new_prop( "STARTARROW",  ((conn.style.connection_arrow == "none") || (conn.style.connection_arrow == "fromto")) ? "None" : "Default" );
    n->new_prop( "ENDARROW",    ((conn.style.connection_arrow == "none") || (conn.style.connection_arrow == "tofrom")) ? "None" : "Default" );
    return( n );
  }

  //-------------------------------------------------------------
  // Reads the contents of an OPML file and creates a new document
  // based on the stored information.
  public override bool import( string fname, MindMap map ) {

    // Read in the contents of the Freemind file
    var doc = Xml.Parser.read_file( fname, null, (Xml.ParserOption.HUGE | Xml.ParserOption.RECOVER) );
    if( doc == null ) {
      return( false );
    }

    // Load the contents of the file
    import_map( map, doc->get_root_element() );

    // Update the drawing area
    map.queue_draw();

    // Delete the OPML document
    delete doc;

    return( true );

  }

  //-------------------------------------------------------------
  // Parses the OPML head block for information that we will use
  private void import_map( MindMap map, Xml.Node* n ) {

    var color_map = new HashMap<string,RGBA?>();
    var id_map    = new HashMap<string,int>();
    var link_ids  = new Array<NodeLinkInfo?>();
    var to_nodes  = new Array<string>();

    // Not sure what to do with the version information
    string? v = n->get_prop( "version" );
    if( v != null ) {
      // Not sure what to do with this value
    }

    for( Xml.Node* it = n->children; it != null; it = it->next ) {
      if( it->type == Xml.ElementType.ELEMENT_NODE ) {
        if( it->name == "node" ) {
          var root = import_node( it, map, null, color_map, id_map, link_ids, to_nodes );
          map.model.position_root_node( root );
          map.get_nodes().append_val( root );
        }
      }
    }

    // Connect linked nodes
    for( int i=0; i<link_ids.length; i++ ) {
      link_ids.index( i ).node.linked_node = new NodeLink.for_local( id_map.get( link_ids.index( i ).id_str ) );
    }

    // Finish up the connections
    for( int i=0; i<to_nodes.length; i++ ) {
      if( id_map.has_key( to_nodes.index( i ) ) ) {
        var to_node = map.model.get_node( map.get_nodes(), id_map.get( to_nodes.index( i ) ) );
        if( to_node != null ) {
          map.connections.complete_connection( i, to_node );
        }
      }
    }

  }

  //-------------------------------------------------------------
  // Parses the given Freemind node
  public Node import_node( Xml.Node* n, MindMap map, Node? parent, HashMap<string,RGBA?> color_map, HashMap<string,int> id_map, Array<NodeLinkInfo?> link_ids, Array<string> to_nodes ) {

    var node = new Node( map, map.layouts.get_default() );

    // Make sure the style has a default value
    node.style = StyleInspector.styles.get_style_for_level( ((parent == null) ? 0 : 1), null );

    string? i = n->get_prop( "ID" );
    if( i != null ) {
      id_map.set( i, node.id() );
    }

    string? t = n->get_prop( "TEXT" );
    if( t != null ) {
      node.name.text.insert_text( 0, t );
    }

    string? f = n->get_prop( "FOLDED" );
    if( f != null ) {
      node.folded = bool.parse( f );
    }

    string? l = n->get_prop( "LINK" );
    if( (l != null) && (l.substring( 0, 1 ) == "#") ) {
      link_ids.append_val( NodeLinkInfo( l.substring( 1 ), node ) );
    }

    string? c = n->get_prop( "COLOR" );
    if( c != null ) {
      if( color_map.has_key( c ) ) {
        node.link_color = color_map.get( c );
      } else {
        node.link_color = map.get_theme().next_color();
        color_map.set( c, node.link_color );
      }
    }

    string? p = n->get_prop( "POSITION" );
    if( p != null ) {
      node.side = (p == "left") ? NodeSide.LEFT : NodeSide.RIGHT;
    }

    var num = n->get_prop( "NUMBERED" );
    if( (num != null) && (parent != null) ) {
      parent.sequence = bool.parse( num );
    }

    // Parse the child nodes
    for( Xml.Node* it = n->children; it != null; it = it->next ) {
      if( it->type == Xml.ElementType.ELEMENT_NODE ) {
        switch( it->name ) {
          case "node"        :  import_node( it, map, node, color_map, id_map, link_ids, to_nodes );  break;
          case "edge"        :  import_edge( it, node );  break;
          case "font"        :  import_font( it, node );  break;
          case "icon"        :  break;  // Not implemented
          case "cloud"       :  import_cloud( it, node );  break;
          case "arrowlink"   :  import_arrowlink( it, map, node, to_nodes );  break;
          case "richcontent" :  import_richcontent( it, map, node );  break;
        }
      }
    }

    // Attach the new node to its parent
    if( parent != null ) {
      node.attach( parent, -1, map.get_theme() );
    }

    return( node );

  }

  private void import_edge( Xml.Node* n, Node node ) {

    string? s = n->get_prop( "STYLE" );
    if( s != null ) {
      switch( s ) {
        case "bezier" :  node.style.link_type = new LinkTypeCurved();    break;
        case "linear" :  node.style.link_type = new LinkTypeStraight();  break;
      }
    }

    string? c = n->get_prop( "COLOR" );
    if( c != null ) {
      // Not implemented - link color and node color must be the same
    }

    string? w = n->get_prop( "WIDTH" );
    if( w != null ) {
      node.style.link_width = int.parse( w );
    }

  }

  private void import_font( Xml.Node* n, Node node ) {

    string? f = n->get_prop( "NAME" );
    if( f != null ) {
      node.style.node_font.set_family( f );
    }

    string? s = n->get_prop( "SIZE" );
    if( s != null ) {
      node.style.node_font.set_size( int.parse( s ) * Pango.SCALE );
    }

    string? b = n->get_prop( "BOLD" );
    if( b != null ) {
      if( bool.parse( b ) ) {
        node.name.text.insert_text( 0, "<b>" + node.name.text.text + "</b>" );
      }
    }

    string? i = n->get_prop( "ITALIC" );
    if( i != null ) {
      if( bool.parse( i ) ) {
        node.name.text.insert_text( 0, "<i>" + node.name.text.text + "</i>" );
      }
    }

  }

  private void import_cloud( Xml.Node* n, Node node ) {

    node.group = true;

  }

  private void import_arrowlink( Xml.Node* n, MindMap map, Node from_node, Array<string> to_nodes ) {

    var conn        = new Connection( map, from_node );
    var start_arrow = "None";
    var end_arrow   = "None";

    string? c = n->get_prop( "COLOR" );
    if( c != null ) {
      // Not implemented
    }

    string? d = n->get_prop( "DESTINATION" );
    if( d != null ) {
      to_nodes.append_val( d );
    }

    string? sa = n->get_prop( "STARTARROW" );
    if( sa != null ) {
      start_arrow = sa;
    }

    string? ea = n->get_prop( "ENDARROW" );
    if( ea != null ) {
      end_arrow = ea;
    }

    // Stylize the arrow
    switch( start_arrow + end_arrow ) {
      case "NoneNone"       :  conn.style.connection_arrow = "none";    break;
      case "NoneDefault"    :  conn.style.connection_arrow = "fromto";  break;
      case "DefaultNone"    :  conn.style.connection_arrow = "tofrom";  break;
      case "DefaultDefault" :  conn.style.connection_arrow = "both";    break;
    }

    // Add the connection to the connections list
    map.connections.add_connection( conn );

  }

  //-------------------------------------------------------------
  // Parses richcontent for a note.  If found parses the note.
  private void import_richcontent( Xml.Node* n, MindMap map, Node node ) {

    string? t = n->get_prop( "TYPE" );
    if( (t != null) && (t == "NOTE") ) {
      for( Xml.Node* it = n->children; it != null; it = it->next ) {
        if( (it->type == Xml.ElementType.ELEMENT_NODE) && (it->name.down() == "html") ) {
          HtmlToMarkdown.reset();
          node.note = HtmlToMarkdown.parse_xml( it ).strip();
        }
      }
    }

  }

}
