/*
 * This is a Gtk2 replacement for the MP3FileManager program shipped
 * with the Sony NW-S23. It currently needs the following things
 * fixed (X marks the ones I've fixed):
 *
 * X dropping folders at specific positions doesn't work; they always
 *   get appended to the end of the folder list. You can move 'em
 *   around once they're on the device, though.
 * X changes are synced to the device far too often, and the syncing
 *   is a huge kludge that involves deleting the entire track/folder
 *   list, syncing that, then rebuilding the new list from
 *   scratch. Mostly this is because I need to add a few more hooks to
 *   mple.c
 * X doesn't delete mp*.dat files when tracks are deleted from the
 *   UI. The index entries *are* deleted, so the mpdat files won't
 *   play, though.
 * * Delete All button does nothing
 * * You can't select more than one thing at a time from the
 *   list. This is a serious pain if you're trying to make bulk
 *   changes, such as moving a half-dozen tracks. This can't be fixed
 *   without subclassing, see below.
 * * Delete All and Delete both have confirm dialogs in the Windows
 *   version.
 * * The file transfer stuff needs a little fixing - the main window
 *   should be disabled; this is done with - I think - an
 *   application-modal window on Windows. A better idea would be to
 *   thread the transfer process so you could keep adding files while
 *   the current transfer is in progress.
 * * You can't drop a deep directory structure on it and expect it to
 *   do something useful. Windows will collapse a Band/Album/Mp3.mp3
 *   filename into Folder "Band Album", Track Mp3.mp3
 * X Files should be listed by the mp3 filename, not the mp3 title.
 * * The windows version doesn't use a progress bar for
 *   transfers. While I like the progress bar, it does mean that the
 *   status bar is only half as wide as it should be.
 * * It would be nice if I could handle any URI that drag-and-drop
 *   throws me; at the very least, dragging a URL from firefox into
 *   the gadget would be cool.
 * * Context menu is broken
 * * Context menu should allow you to edit the metadata of a file
 * X The device path is hardcoded.
 * * Mount/Unmount device would be neat
 * * dropping is still not quite right
 *   - dropping a file at the end of the device
 *   - dropping a file onto a folder prepends instead of appends
 *
 * And internally;
 * * the code is full of memory leaks. I did a dry-run with valgrind,
 *   but there's so much noise from Gtk and X that I gave up.
 * * Globals. I hate globals. I hate hate hate hate them.
 * * Might be smart to isolate the unix-specific stuff if I'm going to
 *   try building this on Windows at some point. There are probably
 *   glib functions to help me.
 *
 * The code is, in general, pretty horrible, in part because I'm
 * trying to force the default GtkTree stuff to do things it's not
 * really meant to do. At some point I need to learn how to subclass
 * the GtkTreeStore so I can add on the bits I need, such as better
 * drag/drop, edit on button-click rather than button-press, etc.
 *
 * The other reason the code is horrible is that I've not done any
 * serious Gtk work before, and it's not exactly intuitive. I keep
 * finding useful glib functions after I've spent half an hour writing
 * equivalent code, too. Henry Spencer would be proud...
 */

/*
Sigh.

"GTK+ supports Drag-and-Drop in tree views with a high-level and a
 low-level API.

 The low-level API consists of the GTK+ DND API, augmented by some
 treeview utility functions: gtk_tree_view_set_drag_dest_row(),
 gtk_tree_view_get_drag_dest_row(),
 gtk_tree_view_get_dest_row_at_pos(),
 gtk_tree_view_create_row_drag_icon(), gtk_tree_set_row_drag_data()
 and gtk_tree_get_row_drag_data(). This API leaves a lot of
 flexibility, but nothing is done automatically, and implementing
 advanced features like hover-to-open-rows or autoscrolling on top of
 this API is a lot of work."

and the high-level API is equally a pain in the ass. Gah.
*/


#include <gtk/gtk.h>
#include <glade/glade.h>

#include <stdio.h>
#include <glib.h>
#include <string.h>
#include <sys/stat.h>
#include <ctype.h>
#include <errno.h>
#include <stdlib.h>
#include <libnw.h>

/* globals */
GladeXML *xml = NULL;
GtkTreeStore *tree = NULL;
GtkWidget *treeview = NULL;
nw_device_t *thisdev = NULL;

/* GRR.
   When the automatic drag/drop stuff on the treeview moves a row, it
   fires insert and then delete. Rather than causing excessive
   flapping on the device, I'll track the action with some globals and
   just do a rename at the appropriate time
*/
typedef enum {
  ModNONE,
  ModDELETE,
  ModRENAME,
} ModType;
ModType RENAMING = ModNONE;
GtkTreePath *RENAMEPATH = NULL;

/* This is used to make the drag/drop stuff a little clearer */
typedef enum {
  ObjFOLDER = 0,
  ObjFILE,
  ObjEXTERNAL,
  ObjDEV
} ObjType;

/* and this clarifies the bits in the tree model */
typedef enum {
  ColNAME = 0,
  ColBLOB,
  ColTYPE,
  NUMCOLS,
} TreeCols;

/* this is used for the drop progress stuff */
typedef struct drop_ctx {
  guint count;
  guint max;
} drop_ctx;

/* 'df' */
gboolean capacity( guint *used, guint *total ) {
  *used = 0;
  *total = 0;

  return TRUE;
}

/* Track copy progress */
void progress( double percent, void *context ) {
  GtkWidget *sbar = glade_xml_get_widget( xml, "statusbar1" );
  guint ctxid;

  ctxid = gtk_statusbar_get_context_id(GTK_STATUSBAR( sbar ), "status" );

  gtk_statusbar_pop( GTK_STATUSBAR( sbar ), ctxid );

  if ( percent > 0 ) {
    gchar *msg = g_strdup_printf( "1/1 %.0f%%", percent * 100 );
    gtk_statusbar_push( GTK_STATUSBAR( sbar ), ctxid, msg );
    g_free( msg );
  } else {
    guint a, b;
    if ( capacity( &a, &b )) {
    }
  }

  gtk_main_iteration_do( FALSE );
}

/*  gtk_tree_model_foreach( GTK_TREE_MODEL( tree ), dumpmodel, NULL );*/
gboolean dumpmodel(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter,
                   gpointer userdata) {
  void *data;

  gtk_tree_model_get( model, iter, ColNAME, &data, -1 );
  if ( data != NULL) {
    /* cool, this works. data is whatever we slung in at gtk_tree_store_set */
    fprintf( stderr, "found data %s\n", (char *)data );
  }

  return FALSE; /* Return true to stop walking */
}

/*
 * Sync the model back to the device
 */
gboolean syncmodel_each( GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter,
                    gpointer userdata ) {
  void *data;
  void *type;

  gtk_tree_model_get( model, iter, ColBLOB, &data, ColTYPE, &type, -1 );
  if ( data ) {
    int retval;
    if ( (int *)type == ObjFOLDER ) {
      nw_folder_t *fptr = (nw_folder_t *)data;

      retval = nw_add_folder( thisdev, nw_get_folder_name( fptr ), 0 );

      if ( retval == 0 ) {
        /* uh-oh xxx */
      }
    } else {
      nw_track_t *tptr = (nw_track_t *)data;
      guint16 folder = g_list_length( nw_get_folderlist( thisdev ));

      retval = nw_add_to_folder( thisdev, folder, tptr,
                                 nw_get_tracknum( tptr ), 0 );

      if ( retval == 0 ) {
        /* uh-oh xxx */
      }
    }
  } else {
    /* probably a delete */
  }

  return FALSE;
}

void syncmodel( GtkTreeModel *model ) {
  GList *old;

  fprintf( stderr, "THIS CODE NEEDS REWORK\n" );

  old = nw_get_folderlist( thisdev );
  /* thisdev->folderlist = NULL; XXX
     previously, I just nuked the entire folderlist and rebuilt it
     from scratch. Aside from the fact that that's a bit wasteful, the
     new opaque nw_device_t doesn't allow direct access to the
     folderlist in this way.
  */
  nw_set_boolean( thisdev, "autosync", FALSE );

  gtk_tree_model_foreach( GTK_TREE_MODEL( tree ), syncmodel_each, NULL );

  g_list_free( old );

  nw_dev_sync( thisdev );
  nw_set_boolean( thisdev, "autosync", TRUE );
}

/*
 * uuuuuugly. edited passes the path to me as a bleedin' STRING.
 */
void edited( GtkCellRendererText *cellrenderertext, const gchar *cpath,
             const gchar *new_text ) {
  GtkTreePath *path = gtk_tree_path_new_from_string( cpath );
  GtkTreeIter iter;
  GValue gval = { 0, };

  gtk_tree_model_get_iter( GTK_TREE_MODEL( tree ), &iter, path );
  gtk_tree_store_set( tree, &iter, ColNAME, new_text, -1 );

  /* update the mple structure */
  if ( gtk_tree_path_get_depth( path ) == 1 ) {
    nw_folder_t *folder;

    gtk_tree_model_get_value( GTK_TREE_MODEL( tree ), &iter, ColBLOB, &gval );
    folder = g_value_get_pointer( &gval );

    if ( folder != NULL ) {
      dprintf( stderr, "renaming folder %s @ %s to %s\n", nw_get_folder_name( folder ),
               cpath, new_text );
      /* only update it if it actually needs updating! */
      if ( memcmp( nw_get_folder_name( folder ), new_text, strlen( new_text )) != 0 ) {
        nw_set_folder_name( folder, new_text );
        nw_dev_sync( thisdev );
      }
    } else {
      fprintf( stderr, "problem getting folder\n" );
    }
  } else if ( gtk_tree_path_get_depth( path ) == 2 ) {
    nw_track_t *track;

    gtk_tree_model_get_value( GTK_TREE_MODEL( tree ), &iter, ColBLOB, &gval );
    track = g_value_get_pointer( &gval );

    if ( track != NULL ) {
      if ( memcmp( nw_get_tag( track, NW_FILENAME ), new_text,
                   strlen( new_text )) != 0 ) {
        nw_set_tag( track, NW_FILENAME, new_text );
        nw_dev_sync( thisdev );
      }
    } else {
      fprintf( stderr, "problem getting track\n" );
    }
  }

  g_value_unset( &gval );
  gtk_tree_path_free( path );
}

/*
 * model-frobbing signals
 */
void row_changed( GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter ) {
  /* nothing to do! */
}

void row_inserted( GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter ) {

  if ( RENAMING == ModRENAME ) {
    gchar *pathstring = gtk_tree_path_to_string( path );
    /* OUCH */
    RENAMEPATH = gtk_tree_path_new_from_string( pathstring );
    dprintf( stderr, "renaming %s\n", pathstring );
    g_free( pathstring );
  } else {
    /* regular insert; nothing to do */
  }
}

/*
 * This gets called AFTER the row has been deleted, so you can't pull
 * the correct values out of the TreeModel...
 */
void row_deleted( GtkTreeModel *model, GtkTreePath *path ) {
  ObjType what;
  GList *fnode, *tnode = NULL;
  nw_folder_t *fptr;
  nw_track_t *tptr = NULL;
  gint *src_idx, *dst_idx = NULL;
  gchar *src = NULL, *dest = NULL;

  /* fetch the src & destination index */
  src_idx = gtk_tree_path_get_indices( path );
  if ( RENAMING == ModRENAME ) {
    dst_idx = gtk_tree_path_get_indices( RENAMEPATH );

    /* if the drop point is before the current point, the src_idx will
       be out of whack */
    if ( gtk_tree_path_get_depth( path ) == 2 ) {
      if ( dst_idx[1] < src_idx[1] ) {
        src_idx[1]--;
      }
    }
    if ( dst_idx[0] < src_idx[0] ) {
      src_idx[0]--;
    }
  }

  /* figure out what we're dropping/renaming */
  if ( gtk_tree_path_get_depth( path ) == 1 ) {
    what = ObjFOLDER;
  } else if ( gtk_tree_path_get_depth( path ) == 2 ) {
    what = ObjFILE;
  } else {
    fprintf( stderr, "WTF?\n" );
    goto out;
  }

  /* now locate the thing to be deleted */
  fnode = g_list_nth( nw_get_folderlist( thisdev ), src_idx[0] );
  if ( fnode == NULL ) {
    fprintf( stderr, "can't find folder %d (%d folders on device)\n",
             src_idx[0], g_list_length( nw_get_folderlist( thisdev )));
    goto out;
  } else {
    fptr = fnode->data;
    dest = nw_get_folder_name( fptr );
  }

  if ( what == ObjFILE ) {
    tnode = g_list_nth( nw_get_folder_tracklist( fptr ), src_idx[1] );
    if ( tnode == NULL ) {
      fprintf( stderr, "can't find track %d (%d tracks in folder)\n",
               src_idx[1], g_list_length( nw_get_folder_tracklist( fptr )));
      goto out;
    } else {
      tptr = tnode->data;
      dest = g_strdup_printf( "%s/%s (%d)", dest,
                              nw_get_tag( tptr, NW_FILENAME ),
                              src_idx[1] );
    }
  }

  if ( RENAMING == ModDELETE ) {
    if ( what == ObjFILE ) {
      if ( nw_del_track( thisdev, nw_get_tracknum( tptr )) &&
           nw_dev_sync( thisdev )) {
      } else {
        perror( "delete track" );
      }
    } else {
      if ( nw_del_folder( thisdev, src_idx[0] + 1 ) &&
           nw_dev_sync( thisdev )) {
      } else {
        perror( "delete folder" );
      }
    }
  } else if ( RENAMING == ModRENAME ) {
    ObjType swhat;
    GList *sfnode;
    nw_folder_t *sfptr;

    if ( gtk_tree_path_get_depth( RENAMEPATH ) == 1 ) {
      swhat = ObjFOLDER;
    } else if ( gtk_tree_path_get_depth( RENAMEPATH ) == 2 ) {
      swhat = ObjFILE;
    } else {
      fprintf( stderr, "source wtf?\n" );
    }

    sfnode = g_list_nth( nw_get_folderlist( thisdev ), dst_idx[0] );
    if ( sfnode == NULL ) {
      if ( what == ObjFILE ) {
        fprintf( stderr, "can't find folder %d (%d folders on device)\n",
                 dst_idx[0], g_list_length( nw_get_folderlist( thisdev ) ));
      } else {
        src = g_strdup( "end of device" );
        swhat = ObjFOLDER;
      }
    } else {
      sfptr = sfnode->data;
      src = nw_get_folder_name( sfptr );
    }

    if ( what == ObjFILE ) {

      fprintf( stderr, "%s -> %s, slot %d\n", dest, src,
               dst_idx[1] + 1 );

      if ( nw_mv_track( thisdev, nw_get_tracknum( tptr ), dst_idx[0] + 1,
                          dst_idx[1] + 1 ) &&
           nw_dev_sync( thisdev )) {

        goto out;
      } else {
        perror( "mple_mv_track" );
      }
    } else {
      if ( nw_mv_folder( thisdev, src_idx[0] + 1, dst_idx[0] + 1 ) &&
           nw_dev_sync( thisdev )) {
        goto out;
      } else {
        perror( "mple_mv_folder" );
      }
    }
  }

 out:
  /* done with this */
  RENAMING= ModNONE;
  if ( RENAMEPATH != NULL ) {
    gtk_tree_path_free( RENAMEPATH );
    RENAMEPATH = NULL;
  }
}

/*
 * populate the tree on startup
 */
const GtkTargetEntry row_targets[] = {
  { "GTK_TREE_MODEL_ROW", GTK_TARGET_SAME_WIDGET, 0 },
  { "text/uri-list", 0, 0 }, /* nautilus */
  /* mozilla drop types:
     text/x-moz-url
     _NETSCAPE_URL
     text/x-moz-url-data
     text/x-moz-url-desc
     text/_moz_htmlcontext
     text/_moz_htmlinfo
     text/html
     text/unicode
     text/plain
  */
};

gboolean mp3fm_startup( GtkWidget *widget, GdkEvent *event, gpointer user_data ) {
  GList *node;
  GtkTreeIter iter, titer;
  GtkCellRenderer *renderer;
  GtkTreeViewColumn *column;
  GList *players;

  players = nw_get_devs( NULL );
  if ( g_list_length( players ) == 0 ) {
    fprintf( stderr, "No players detected, bailing\n" );
    return FALSE;
    exit( 1 );
  } else if ( g_list_length( players ) > 1 ) {
    fprintf( stderr, "Multiple players detected, using first one\n" );
  }

  thisdev = g_list_nth_data( players, 0 );
  if ( thisdev == NULL ) {
    perror( "dev init" );
    exit( 1 );
  }
  nw_set_progress_func( thisdev, progress );
  nw_set_boolean( thisdev, "autosync", FALSE ); /* Woohoo... */

  /* clear the progress bar */
  progress( 0, NULL );

  /* now load up the tree view with the data */
  treeview = glade_xml_get_widget( xml, "treeview" );

  /* tree consists of a string to display, a link to the folder/track
     pointer structure for the displayed string, and a flag indicating
     if it's a track or not */
  tree = gtk_tree_store_new( NUMCOLS, G_TYPE_STRING, G_TYPE_POINTER,
                             G_TYPE_INT );

  if ( nw_parse_directory( thisdev )) {
    node = nw_get_folderlist( thisdev );

    while( node ) {
      GList *tnode =NULL;
      nw_track_t *tptr = NULL;
      gchar *label = NULL;
      nw_folder_t *fptr = node->data;

      label = nw_get_folder_name( fptr );
      if ( strlen( label ) == 0 ) {
        g_free( label );
        label = g_strdup( "(No Name)" );
      }

      gtk_tree_store_append( tree, &iter, 0 );
      gtk_tree_store_set( tree, &iter,
                          ColNAME, label,
                          ColBLOB, fptr,
                          ColTYPE, ObjFOLDER,
                          -1 );
      g_free( label );

      tnode = nw_get_folder_tracklist( fptr );
      while( tnode ) {
        tptr = tnode->data;
        label = nw_get_tag( tptr, NW_FILENAME );

        gtk_tree_store_append( tree, &titer, &iter );
        gtk_tree_store_set( tree, &titer,
                            ColNAME, label,
                            ColBLOB, tptr,
                            ColTYPE, ObjFILE,
                            -1 );
        g_free( label );

        tnode = g_list_next( tnode );
      }

      node = g_list_next( node );
    }
  }

  g_signal_connect( G_OBJECT( tree ), "row_changed", G_CALLBACK( row_changed ),
                    NULL );

  g_signal_connect( G_OBJECT( tree ), "row_inserted",
                    G_CALLBACK( row_inserted ), NULL );

  g_signal_connect( G_OBJECT( tree ), "row_deleted",
                    G_CALLBACK( row_deleted ), NULL );

  gtk_tree_view_set_model( GTK_TREE_VIEW(treeview), GTK_TREE_MODEL(tree));

  renderer = gtk_cell_renderer_text_new ();
  g_object_set( G_OBJECT( renderer ), "editable", TRUE, NULL );
  g_signal_connect( G_OBJECT( renderer ), "edited",
                    G_CALLBACK( edited ), NULL );

  column = gtk_tree_view_column_new_with_attributes ( "TITLE",
                                                      renderer,
                                                      "text", 0,
                                                      NULL);

  gtk_tree_view_append_column( GTK_TREE_VIEW (treeview), column );

  /* further startup frobbery - this allows me to override what the
     treeview thinks it can accept */
  gtk_tree_view_enable_model_drag_dest( GTK_TREE_VIEW( treeview ), row_targets,
                                        G_N_ELEMENTS( row_targets ),
                                        GDK_ACTION_MOVE );

  return TRUE;
}

/*
 * clean up
 */
void shutdown( GtkButton *widget, gpointer user_data ) {
  if ( thisdev != NULL ) {
    nw_dev_free( thisdev );
  }
  gtk_exit( 0 );
}

/*
 * noop for the moment
 */
void on_mount_device1_activate( GtkMenuItem *widget, gpointer user_data ) {
  GtkWidget *contextmenu = glade_xml_get_widget( xml, "contextmenu" );
  gtk_widget_hide( contextmenu );

  /* system( "/bin/mount /media/NW-S23" ); */
}

/*
 * pop up the context menu
 */
gboolean button_press( GtkWidget *widget, GdkEventButton *event, gpointer user_data ) {
  if ( event->button == 3 ) {
    /* doesn't appear to be working, for some reason */
    GtkWidget *contextmenu = glade_xml_get_widget( xml, "contextmenu" );

    fprintf( stderr, "showing context menu %p\n", contextmenu );
    gtk_widget_show_all( contextmenu );

    return TRUE;
  } else {
  }
  return FALSE; /* propagate */
}

/*
 * Handle delete button
 */
void on_delButton_clicked( GtkButton *widget, gpointer user_data ) {
  GtkTreeSelection *selected;
  GtkTreeModel *model = GTK_TREE_MODEL( tree );
  GList *paths;
  GtkTreeIter iter;
  void *data;

  /* get the currently selected tree item */
  selected = gtk_tree_view_get_selection( GTK_TREE_VIEW( treeview ));

  if ( selected == NULL ) {
    return;
  }

  paths = gtk_tree_selection_get_selected_rows( selected, &model );

  RENAMING = ModDELETE;

  while( paths ) {
    GtkTreePath *path = paths->data;

    gtk_tree_model_get_iter( model, &iter, path );
    gtk_tree_model_get( model, &iter, ColNAME, &data, -1 );
    gtk_tree_store_remove( tree, &iter );

    paths = g_list_remove( paths, path );
    gtk_tree_path_free( path );
  }

  return;
}

void on_delAllButton_clicked( GtkButton *widget, gpointer user_data ) {
  GtkWidget *confirm;
  GtkWidget *message;

  confirm = glade_xml_get_widget( xml, "delete_ok_dialogue" );
  message = glade_xml_get_widget( xml, "delete_ok_label" );

  gtk_label_set_text( GTK_LABEL( message ), "Delete all files?" );

  gtk_widget_show( confirm );
}

/*
 * attempt to verify that a legal drop is being
 * performed. Specifically:
 * - don't allow a folder to be dropped inside a folder
 * - don't allow a file to be dropped on another file
 * - don't allow a file to be dropped outside a folder
 * Since I can't tell much about an externally dropped file at this
 * point in the process, that case is less rigourously checked
 */
gboolean drag_drop_ok(GtkWidget           *widget,
                      GdkDragContext      *context,
                      gint                x,
                      gint                y,
                      guint               time) {
  GtkTreePath *path;
  GList *paths;
  GtkTreeModel *model = GTK_TREE_MODEL( tree );
  GtkTreeSelection *selected;
  ObjType source, dest;
  GValue gval = { 0, };
  GtkTreeViewDropPosition pos;
  gboolean destexists;

  if ( gtk_drag_get_source_widget( context ) != NULL ) {
    selected = gtk_tree_view_get_selection( GTK_TREE_VIEW( treeview ));
    if ( selected != NULL ) {
      GtkTreePath *path;
      GtkTreeIter iter;

      if (( paths = gtk_tree_selection_get_selected_rows( selected, &model ))
          != NULL ) {
        /* generic solution, we'd have to cater for multiple
           selections. the treeview is single-select only... */
        path = paths->data;
        gtk_tree_model_get_iter( model, &iter, path );
        gtk_tree_model_get_value( model, &iter, ColTYPE, &gval );
        source = g_value_get_int( &gval );
        g_value_unset( &gval );
        gtk_tree_path_free( path );
      } else {
        /* nothing selected... */
        return TRUE;
      }
    } else {
      return TRUE;
    }
  } else {
    /* drop from external source e.g. nautilus
     * can't tell what we'll do with it until it's actually dropped, though.
     */
    GList *node = context->targets;

    source = ObjDEV;

    while( node ) {
      char *possible_type;
      int i;

      possible_type = gdk_atom_name ((GdkAtom) node->data);

      /* do this in such a way that I don't have to redo it if I
         expand the row_targets list */
      for ( i = 0; i < sizeof( row_targets )/sizeof( GtkTargetEntry ); i++ ) {
        if ( strcmp( possible_type, row_targets[i].target ) == 0 ) {
          source = ObjEXTERNAL;
        }
#ifdef DEBUG
        else {
          fprintf( stderr, "can't use type [%s]\n", possible_type );
        }
#endif
      }

      g_free( possible_type );
      if ( source != ObjDEV ) {
        break;
      }
      node = g_list_next( node );
    }

    if ( source == ObjDEV ) {
#ifdef DEBUG
      fprintf( stderr, "bailing out, no useful types found\n" );
#endif
      return TRUE;
    }
  }

  /* identify what we're dropping on */
  destexists = gtk_tree_view_get_dest_row_at_pos( GTK_TREE_VIEW( widget ), x, y, &path, &pos );

  if ( path != NULL ) {
    GtkTreeIter iter;
    GValue val = { 0, };

    gtk_tree_model_get_iter( model, &iter, path );
    gtk_tree_model_get_value( model, &iter, ColTYPE, &val );
    dest = g_value_get_int( &val );
    g_value_unset( &val );

#ifdef DEBUG
    {
      gchar *p;
      gtk_tree_model_get_value( model, &iter, ColNAME, &val );
      p = gtk_tree_path_to_string( path );
      fprintf( stderr, "drop path: %s (%s, %s)\n", p,
               g_value_get_string( &val ),
               dest == ObjFILE ? "file" : "folder" );
      g_value_unset( &val );
      g_free( p );
    }
#endif
  } else {
    dest = ObjDEV; /* dropped outside existing stuff */
  }

  if ( source == ObjFOLDER ) {
    if ( destexists && ( pos == GTK_TREE_VIEW_DROP_INTO_OR_BEFORE ||
                         pos == GTK_TREE_VIEW_DROP_INTO_OR_AFTER )) {
#ifdef DEBUG
      fprintf( stderr, "can't drop folder into existing folder/file\n" );
#endif
      return TRUE;
    }
  } else { /* source is a file or an external */
    switch( dest ) {
    default:
    case ObjDEV:
      if ( source == ObjEXTERNAL ) {
        return FALSE;
      }
#ifdef DEBUG
      fprintf( stderr, "Can't drop file on device\n" );
#endif
      return TRUE;
      break;

    case ObjFILE:
      if ( destexists && ( pos != GTK_TREE_VIEW_DROP_BEFORE &&
                           pos != GTK_TREE_VIEW_DROP_AFTER )) {
#ifdef DEBUG
        /* ideally we'd treat this as a drop before, assuming there's
           a way for me to pass that back to the caller */
        fprintf( stderr, "can't drop anything on existing file (%d)\n",
                 pos );
#endif
        return TRUE;
      }
      break;

    case ObjFOLDER:
      if ( source == ObjEXTERNAL ) {
        return FALSE;
      }
      if ( !destexists ) {
#ifdef DEBUG
        fprintf( stderr, "can't drop file on non-existent folder\n" );
#endif
        return TRUE;
      }
      if ( pos != GTK_TREE_VIEW_DROP_INTO_OR_BEFORE &&
           pos != GTK_TREE_VIEW_DROP_INTO_OR_AFTER ) {
#ifdef DEBUG
        fprintf( stderr, "can't drop file outside folder\n" );
#endif
        return TRUE;
      }
      break;
    }
  }

  return FALSE;
}

gboolean drag_drop( GtkWidget           *widget,
                    GdkDragContext      *context,
                    gint                x,
                    gint                y,
                    guint               time,
                    gpointer            user_data ) {
  if ( drag_drop_ok( widget, context, x, y, time ) == TRUE ) {
    return TRUE; /* not accepting this, thanks */
  }

  return FALSE;
}

/*
 * find all files in a tree. Dumb, susceptible to loops, etc.
 * I'm planning on adding a hook function which will only collect
 * compatible files (MP3, 44k1 sampling rate, 8-320kbit) and give up
 * when it's reached the capacity of the device. For now, just be
 * careful, eh?
 */
GList *findfiles( const gchar *start, GList *files ) {
  GDir *dir;
  GError *error;

  if ( g_file_test( start, G_FILE_TEST_IS_DIR )) {
    const gchar *readname;

    dir = g_dir_open( start, 0, &error );
    if ( dir == NULL ) {
      return files;
    }
    while(( readname = g_dir_read_name( dir )) != NULL ) {
      gchar *newname;

      /* these appear to be helpfully removed for me, but just in
         case... */
      if (( strcmp( readname, "." ) == 0 ) ||
          ( strcmp( readname, ".." ) == 0 )) {
        continue;
      }

      /* this leaks memory pretty badly, but apparently by design. */
      newname = g_build_path( "/", start, readname, NULL );

      /* testing here saves some recursion */
      if ( g_file_test( newname, G_FILE_TEST_IS_DIR )) {
        files = findfiles( newname, files );
        g_free( newname );
      } else {
        files = g_list_append( files, newname ); /* gets freed at
                                                    cleanup */
      }
    }
    g_dir_close( dir );
  } else {
    files = g_list_append( files, g_strdup( start ));
  }

  return files;
}

/*
 * This is called when a drop is made.
 *
 * At present it's a mess. Realistically, it should first determine
 * candidate files, then it should do the copy.
 */
void drag_data_received(GtkWidget          *widget,
                        GdkDragContext     *context,
                        gint                x,
                        gint                y,
                        GtkSelectionData   *selection_data,
                        guint               info,
                        guint               time_,
                        gpointer            user_data) {
  GtkTreePath *path;
  GtkTreeModel *model = GTK_TREE_MODEL( tree );
  GtkTreeIter iter, *folder_iter = NULL, track_iter, parent;
  ObjType source, dest = ObjFILE; /* ehhh. */
  gboolean destexists;
  GtkTreeViewDropPosition pos;
  GValue gval = { 0, };
  gchar *srcuri, *destfolder;
  guint16 folder, idx, newtrack;
  nw_folder_t *fptr;
  gchar **files;
  static gchar *newfolder = "New Album";
  /* integrating new findfiles() function... */
  GList *filelist = NULL;
  GList *node = NULL;

  if ( selection_data == NULL ) {
    return;
  }

  /* don't interfere with the internal stuff */
  if ( gtk_drag_get_source_widget( context ) != NULL ) {
    RENAMING = ModRENAME;
    return;
  }

  srcuri = g_strndup( (const gchar *)selection_data->data,
                      selection_data->length );
  files = g_strsplit( srcuri, "\r\n", 0 );

  /* It is possible that we're dealing with multiple dropped items, so
     deal with them all */
  for ( idx = 0; files[idx] != NULL; idx++ ) {
    gchar *thisfile, *thisdir;

    /* find out what the source is */
    if ( g_str_has_prefix( files[idx], "file://" )) {
      struct stat statbuf;
      gchar *hostname;
      GError *e;

      hostname = NULL;
      e = NULL;

      thisfile = g_filename_from_uri( files[idx], &hostname, &e );

      /* discard trailing CRLF */
      if ( g_str_has_suffix( thisfile, "\r\n" )) {
        thisfile[strlen( thisfile ) - 2 ] = '\0';
      }

      if ( stat( thisfile, &statbuf ) != 0 ) {
        fprintf( stderr, "error statting %s: %s\n", thisfile, strerror( errno ));
        goto loop_end;
      }

      if ( S_ISDIR( statbuf.st_mode )) {
        source = ObjFOLDER;
        destfolder = (gchar *)g_basename( thisfile );
        thisdir = g_strdup( thisfile );
      } else {
        source = ObjFILE;
        destfolder = NULL;
      }
    } else {
      /* we don't handle other URIs just yet! */
      goto loop_end;
    }

    /* identify the target of the drop */
    destexists = gtk_tree_view_get_dest_row_at_pos( GTK_TREE_VIEW( widget ),
                                                    x, y, &path, &pos );

    if ( path != NULL ) {
      gtk_tree_model_get_iter( model, &iter, path );
      gtk_tree_model_get_value( model, &iter, ColTYPE, &gval );
      dest = g_value_get_int( &gval );
      g_value_unset( &gval );
      if ( destfolder == NULL ) {
        if ( dest == ObjFOLDER ) {
          gtk_tree_model_get_value( model, &iter, ColNAME, &gval );
          destfolder = (gchar *)g_value_get_string( &gval );
          folder_iter = &iter;
        } else {
          gtk_tree_model_iter_parent( model, &parent, &iter );
          gtk_tree_model_get_value( model, &parent, ColNAME, &gval );
          destfolder = (gchar *)g_value_get_string( &gval );
          folder_iter = &parent;
        }
      }
    } else {
      if ( source == ObjFILE ) {
        dest = ObjDEV; /* dropped outside existing stuff */
        destfolder = newfolder;
      }
    }

    /* determine if we allow the drop to happen */
    if ( source == ObjFOLDER ) {
      if ( destexists && ( pos == GTK_TREE_VIEW_DROP_INTO_OR_BEFORE ||
                           pos == GTK_TREE_VIEW_DROP_INTO_OR_AFTER )) {
        goto loop_end;
      }
    } else { /* source is a file or an external */
      switch( dest ) {
      case ObjDEV:
        dest = ObjFOLDER;
        break;

      case ObjFILE:
        /* files only get dropped before and after - no into */
        if ( pos != GTK_TREE_VIEW_DROP_BEFORE &&
             pos != GTK_TREE_VIEW_DROP_AFTER ) {
          goto loop_end;
        }
        break;

      default:
      case ObjFOLDER:
        if ( pos != GTK_TREE_VIEW_DROP_INTO_OR_BEFORE &&
             pos != GTK_TREE_VIEW_DROP_INTO_OR_AFTER ) {
          goto loop_end;
        }
        break;
      }
    }

    /* look up the destination folder */
    folder = nw_get_folder( thisdev, destfolder, 0 );

    if ( !folder ) {
      folder = nw_add_folder( thisdev, destfolder, 0 );
      if ( !folder ) {
        goto loop_end;
      }

      /* added new folder */
      fptr = g_list_nth_data( nw_get_folderlist( thisdev ), folder - 1 );
      gtk_tree_store_append( tree, &parent, 0 );
      gtk_tree_store_set( tree, &parent,
                          ColNAME, g_strdup( destfolder ),
                          ColBLOB, fptr,
                          ColTYPE, ObjFOLDER,
                          -1 );
      folder_iter = &parent;
    }

    if ( source == ObjFOLDER ) {
      filelist = findfiles( thisfile, filelist );
      node = filelist;
    }

    while( 1 ) {
      guint16 ind = 0;

      if ( source == ObjFOLDER ) {
        thisfile = node->data;
        if (( node = g_list_next( node )) == NULL ) {
          break;
        }
      }

      /* where in the device are we putting the file? */
      if ( source == ObjFILE ) {
        gchar *p;

        if ( path != NULL ) {
          p = gtk_tree_path_to_string( path );

          /* path == 1 -> dropped on a folder
             == 2 -> dropped on a file position, which we care about */
          if ( gtk_tree_path_get_depth( path ) == 2 ) {
            gint *indices = gtk_tree_path_get_indices( path );
            ind = indices[gtk_tree_path_get_depth( path ) - 1] + 1;
          }
          g_free( p );
        }
      }

      newtrack = nw_add_track( thisdev, thisfile, NULL, folder, ind );
      if ( nw_dev_sync( thisdev ) == 0 ) {
        perror( "sync" );
      }

      if ( newtrack ) {
        nw_track_t *tptr;
        gchar *label;

        /* for some reason this version of fptr doesn't get updated when
           the above call modifies the folder's tracklist. Not sure
           why that is, but this refreshes it correctly. */
        fptr = g_list_nth_data( nw_get_folderlist( thisdev ), folder - 1 );
        tptr = g_list_nth_data( nw_get_folder_tracklist( fptr ), newtrack - 1 );
        label = nw_get_tag( tptr, NW_FILENAME );

        /* make sure the drop position is correct */
        if ( source == ObjFOLDER && pos == GTK_TREE_VIEW_DROP_BEFORE ) {
          ind = ind - 1;
        }

        gtk_tree_store_insert( tree, &track_iter, folder_iter, ind );
        gtk_tree_store_set( tree, &track_iter, ColNAME, label, ColBLOB, tptr,
                            ColTYPE, ObjFILE, -1 );

      } else {
        fprintf( stderr, "failed to add track %s: %s\n",
                 thisfile, strerror( errno ));
      }

      /* only adding a single file? time to get out! */
      if ( source == ObjFILE ) {
        break;
      }
    }

  loop_end:
    if ( G_IS_VALUE( &gval )) {
      g_value_unset( &gval );
    }

    /* clean up the filelist if we used it */
    if ( filelist != NULL ) {
      node = filelist;
      while( node ) {
        g_free( node->data );
        node = g_list_next( node );
      }
      g_list_free( filelist );
      filelist = NULL;
    }
  }
  g_strfreev( files );
  g_free( srcuri );

  /* clear the progress bar */
  progress( 0, NULL );
}

/* return a cue as to whether the current drag/drop is possible */
/* as soon as this returns true, the entire drag breaks. GRR. */
gboolean drag_motion( GtkWidget *widget,
                      GdkDragContext *context,
                      gint x, gint y, guint time, gpointer user_data ) {
  if ( drag_drop_ok( widget, context, x, y, time ) == TRUE ) {
    /* not possible */
    context->action = 0;
  } else {
    /* possible */
    if ( gtk_drag_get_source_widget( context ) != NULL ) {
      context->action = GDK_ACTION_MOVE;
    } else {
      context->action = GDK_ACTION_COPY;
    }
  }

  return FALSE;
}


/*
 * double-click on a row to open/close it. This is actually superceded
 * by the fact that doubleclicking on a row will /edit/ it...
 */
gboolean on_treeview_row_activated( GtkWidget *widget, GtkTreePath *path,
                                    GtkTreeViewColumn *column ) {

  if (!gtk_tree_view_row_expanded( GTK_TREE_VIEW(widget), path )) {
    gtk_tree_view_expand_row( GTK_TREE_VIEW(widget), path, FALSE );
  } else {
    gtk_tree_view_collapse_row( GTK_TREE_VIEW(widget), path );
  }

  return TRUE;
}

/*
 * the fun starts here
 */
int main( int argc, char *argv[] ) {
  gtk_init( &argc, &argv );

  /* load the interface */
  xml = glade_xml_new( "mp3filemanager.glade", NULL, NULL );

  /* connect the signals in the interface */
  glade_xml_signal_autoconnect( xml );

  /* start the event loop */
  gtk_main();

  return 0;
}

