/*
* 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>
*/

public class ExportOPML : Export {

  //-------------------------------------------------------------
  // Constructor
  public ExportOPML() {
    base( "opml", _( "OPML" ), { ".opml" }, true, true, false, true );
  }

  //-------------------------------------------------------------
  // 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" );
    Xml.Node* opml = new Xml.Node( null, "opml" );
    string    expand_state;
    Xml.Node* body = export_body( map, out expand_state );
    opml->new_prop( "version", "2.0" );
    opml->add_child( new Xml.Node.comment( _( "Generated by Minder" ) + " " + Minder.version ) );
    opml->add_child( export_head( Path.get_basename( fname ), expand_state ) );
    opml->add_child( body );
    doc->set_root_element( opml );
    if( send_to_clipboard() ) {
      var text = "";
      doc->dump_memory_format( out text );
      MinderClipboard.copy_text( text );
    } else {
      doc->save_format_file( fname, 1 );
    }
    delete doc;
    return( true );
  }

  //-------------------------------------------------------------
  // Generates the header for the document.
  private Xml.Node* export_head( string? title, string expand_state ) {
    Xml.Node* head = new Xml.Node( null, "head" );
    var now  = new DateTime.now_local();
    head->new_text_child( null, "title", (title ?? "Mind Map") );
    head->new_text_child( null, "dateCreated", now.to_string() );
    if( expand_state != "" ) {
      head->new_text_child( null, "expansionState", expand_state );
    }
    return( head );
  }

  //-------------------------------------------------------------
  // Generates the body for the document.
  private Xml.Node* export_body( MindMap map, out string expand_state ) {
    Xml.Node*  body    = new Xml.Node( null, "body" );
    Array<int> estate  = new Array<int>();
    int        node_id = 1;
    for( int i=0; i<map.get_nodes().length; i++ ) {
      export_node( map.get_nodes().index( i ), body, ref node_id, ref estate );
    }
    expand_state = "";
    for( int i=0; i<estate.length; i++ ) {
      if( i > 0 ) {
        expand_state += ",";
      }
      expand_state += estate.index( i ).to_string();
    }

    return( body );
  }

  //-------------------------------------------------------------
  // Generates the node for the document and adds it to the parent
  // XML node.
  private void export_node( Node node, Xml.Node* parent, ref int node_id, ref Array<int> expand_state ) {
    Xml.Node* outline = new Xml.Node( null, "outline" );
    outline->new_prop( "text", node.name.text.text );
    if( node.is_task() ) {
      bool checked = node.done_count > 0;
      outline->new_prop( "checked", checked.to_string() );
    }
    if( node.note != "" ) {
      outline->new_prop( "note", node.note );
    }
    if( (node.children().length > 1) && !node.folded ) {
      expand_state.append_val( node_id );
    }
    node_id++;
    for( int i=0; i<node.children().length; i++ ) {
      export_node( node.children().index( i ), outline, ref node_id, ref expand_state );
    }
    parent->add_child( outline );
  }

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

  //-------------------------------------------------------------
  // 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 OPML file
    var doc = Xml.Parser.read_file( fname, null, Xml.ParserOption.HUGE );
    if( doc == null ) {
      return( false );
    }

    // Load the contents of the file
    for( Xml.Node* it = doc->get_root_element()->children; it != null; it = it->next ) {
      if( it->type == Xml.ElementType.ELEMENT_NODE ) {
        Array<int>? expand_state = null;
        switch( it->name ) {
          case "head" :
            import_header( it, ref expand_state );
            break;
          case "body" :
            import_body( map, it, ref expand_state );
            break;
        }
      }
    }

    // Delete the OPML document
    delete doc;

    return( true );

  }

  //-------------------------------------------------------------
  // Parses the OPML head block for information that we will use.
  private void import_header( Xml.Node* n, ref Array<int>? expand_state ) {
    for( Xml.Node* it = n->children; it != null; it = it->next ) {
      if( it->type == Xml.ElementType.ELEMENT_NODE ) {
        switch( it->name ) {
          case "expansionState" :
            if( (it->children != null) && (it->children->type == Xml.ElementType.TEXT_NODE) ) {
              expand_state = new Array<int>();
              string[] values = n->children->get_content().split( "," );
              foreach (string val in values) {
                int intval = int.parse( val );
                expand_state.append_val( intval );
              }
            }
            break;
        }
      }
    }
  }

  private void import_body( MindMap map, Xml.Node* n, ref Array<int>? expand_state ) {

    int node_id = 1;

    // Clear the existing nodes
    map.get_nodes().remove_range( 0, map.get_nodes().length );

    // Load the contents of the file
    for( Xml.Node* it = n->children; it != null; it = it->next ) {
      if( it->type == Xml.ElementType.ELEMENT_NODE ) {
        if( it->name == "outline") {
          var root = new Node( map, map.layouts.get_default() );
          import_node( map, root, it, node_id, ref expand_state, map.get_theme() );
          if (map.get_nodes().length == 0) {
            root.posx = (map.canvas.get_allocated_width()  / 2) - 30;
            root.posy = (map.canvas.get_allocated_height() / 2) - 10;
          } else {
            map.get_nodes().index( map.get_nodes().length - 1 ).layout.position_root( map.get_nodes().index( map.get_nodes().length - 1 ), root );
          }
          map.get_nodes().append_val( root );
        }
      }
    }

  }

  private void import_node( MindMap map, Node node, Xml.Node* parent, int node_id, ref Array<int> expand_state, Theme theme ) {

    // Get the node name
    string? n = parent->get_prop( "text" );
    if( n != null ) {
      node.name.text.insert_text( 0, n );
    }

    // Get the task information
    string? t = parent->get_prop( "checked" );
    if( t != null ) {
      node.enable_task( true );
      if( bool.parse( t ) ) {
        node.set_task_done( true );
      }
    }

    // Get the note information
    string? o = parent->get_prop( "note" );
    if( o != null ) {
      node.note = o;
    }

    // Load the style
    node.style = map.global_style;

    // Figure out if this node is folded
    if( expand_state != null ) {
      node.folded = true;
      for( int i=0; i<expand_state.length; i++ ) {
        if( expand_state.index( i ) == node_id ) {
          node.folded = false;
          expand_state.remove_index( i );
          break;
        }
      }
    }

    node_id++;

    // Parse the child nodes
    for( Xml.Node* it2 = parent->children; it2 != null; it2 = it2->next ) {
      if( (it2->type == Xml.ElementType.ELEMENT_NODE) && (it2->name == "outline") ) {
        var child = new Node( map, node.layout );
        import_node( map, child, it2, node_id, ref expand_state, theme );
        child.attach( node, -1, theme );
      }
    }

    // Calculate the tree size
    node.tree_bbox = node.layout.bbox( node, -1, "import_opml" );
    node.tree_size = node.side.horizontal() ? node.tree_bbox.height : node.tree_bbox.width;

  }

}
