/// Please use spaces instead of tabs

// See the documentation: "HACKING" and "ABSTRACT"

#include "config.h"             // include autoconf settings

#include <ctype.h>
#include <stdio.h>
#include <errno.h>
extern int errno;               // Just in case

#include <string>
#include <set>
#include <map>
#include <vector>
#include <cassert>
using std::string;
using std::set;
using std::map;
using std::vector;
using std::make_pair;

#include "c-client-header.h"

#include "configuration.h"     // configuration parsing and setup
#include "options.h"           // options and default settings
#include "commandline.h"       // commandline parsing
#include "types.h"             // MailboxMap, Passwd
#include "store.h"             // Store
#include "channel.h"           // Channel
#include "mail_handling.h"     // functions implementing various
                               // synchronization steps and helper functions

//------------------------------- Defines  -------------------------------

#define CREATE   1
#define NOCREATE 0

//------------------------ Global Variables ------------------------------

// current operation mode
enum operation_mode_t operation_mode = mode_unknown;
// options and default settings 
options_t options;

// won't link correctly if this is static - why?
Store*       match_pattern_store;

//////////////////////////////////////////////////////////////////////////
// The password for the current context
// Required, because we don't know inside the c-client callback functions
// which context (store1, store2, channel) we are in
Passwd * current_context_passwd = NULL;
//////////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////////
//
int main(int argc, char** argv)
//
//////////////////////////////////////////////////////////////////////////
{
  Channel channel;
  Store& store_a = channel.store_a;
  Store& store_b = channel.store_b;
  MsgIdsPerMailbox lasttime, thistime;
  MailboxMap deleted_mailboxes;   // present last time, but not this time
  MailboxMap empty_mailboxes;
  int success;
  bool& debug = options.debug;

#include "linkage.c"

  //
  // Parse arguments, read config file, choose operation mode
  // --------------------------------------------------------
  {
    string config_file;
    vector<string> channels_and_stores;
    // bad command line parameters
    if (! read_commandline_options( argc, argv, options,
                                   channels_and_stores, config_file) )
      exit(1);         
    operation_mode = setup_channel_stores_and_mode( config_file,
                                                    channels_and_stores,
                                                    channel);
    if ( operation_mode == mode_unknown )
      exit(1);
  }

  store_a.boxes.clear();
  store_b.boxes.clear();

  // initialize c-client environment (~/.imparc etc.)
  env_init( getenv("USER"), getenv("HOME"));

  // open a read only the connection to the first store
  if ( store_a.isremote ) {
    if (! store_a.store_open( OP_HALFOPEN | OP_READONLY) )
      return 1;
  }
  else
  {
    store_a.stream = NULL;
  }

  // in case we want to sync - open a read only the connection
  // to the second store
  if (operation_mode == mode_sync && store_b.isremote)
  {
    if (! store_b.store_open( OP_HALFOPEN | OP_READONLY) )
      return 1;
  }
  else
  {
    store_b.stream = NULL;
  }

  
  // Get list of all mailboxes from first store
  //
  if (debug) printf( " Items in store \"%s\":\n", store_a.name.c_str() );
  if (! store_a.acquire_mail_list() && options.log_warn) {
    printf( " Store pattern doesn't match any selectable mailbox\n");
  }
  if (store_a.delim == '!') {
    store_a.get_delim();
  }
  if (store_a.delim == '!') {  // this should not happen
    assert(0);
  }
  else if (debug) {
    // store_b.delim can be '' for INBOXes
    if ( ! store_a.delim )
      printf(" No delimiter found for store \"%s\"\n", store_a.name.c_str());
    else
      printf( " Delimiter for store \"%s\" is '%c'\n",
              store_a.name.c_str(), store_a.delim );
  }


  // Display which drivers we're using for accessing the first store
  if (debug) {
    store_a.display_driver();
  }

  ///////////////////////////// mode_list //////////////////////////////

  // Display listing of the first mail store in case we're in list mode
  if ( operation_mode == mode_list ) {
    if ( options.show_from | options.show_message_id ) {
      for ( MailboxMap::iterator curr_mbox = store_a.boxes.begin() ; 
            curr_mbox != store_a.boxes.end() ;
            curr_mbox++ )
      {
        printf("\nMailbox: %s\n", curr_mbox->first.c_str());
        if( curr_mbox->second.no_select )
          printf("  not selectable\n");
        else {
          // TODO: shouldn't this be OP_READONLY
          store_a.stream = store_a.mailbox_open( curr_mbox->first, 0);
          if (! store_a.stream) break;
          if (! store_a.list_contents() )
            exit(1);
        }
      }
    }
    else {
      print_list_with_delimiter(store_a.boxes, stdout, "\n");
    } 
   exit(0);
  }

  //////////////////////////////////////////////////////////////////////
  //////////// from this point on we are only dealing with /////////////
  ////////////////// mode_diff or mode_sync ////////////////////////////
  //////////////////////////////////////////////////////////////////////

  ///////////////////////////// mode_sync //////////////////////////////

  // Get list of all mailboxes and delimiter from second store
  //
  if ( operation_mode == mode_sync ) {

    store_b.boxes.clear();

    if (debug) printf( " Items in store \"%s\":\n", store_b.name.c_str() );
    // Get a list of mailboxes from the second store
    if (! store_b.acquire_mail_list() && options.log_warn )
    {
      printf( " Store pattern doesn't match any selectable mailbox\n");
    }
    // Display which drivers we're using for accessing the second store
    if (debug) store_b.display_driver();

    // Making sure we get ahold of the mailbox-hierarchy delimiter
    if (store_b.delim == '!')
      store_b.get_delim();
    if (store_a.delim == '!') {  // this should not happen
      assert(0);
    }
    else if (debug) {
      if (! store_b.delim )
        printf(" No delimiter found for store \"%s\"\n", store_b.name.c_str());
      else
        printf( " Delimiter for store \"%s\" is '%c'\n",
                store_b.name.c_str(), store_b.delim);
    }
  }


  //////////////////////// mode_diff or mode_sync //////////////////////

  // Display all the mailboxes we've found
  if (debug)
  {
    printf(" All seen mailboxes: \n");
    printf("  in first store: \n");
    print_list_with_delimiter( store_a.boxes, stdout, " " );
    printf("  in second store: \n");
    print_list_with_delimiter( store_b.boxes, stdout, " " );
    printf("\n");
  }

  // Read in what mailboxes and messages we've seen the last time
  // we've synchronized
  if (! channel.read_lasttime_seen( lasttime, deleted_mailboxes) )
    exit(1);    // failed to read in msinfo or similar


  // Iterate over all mailboxes and sync or diff each
  //
  // our comparison operator for our stores compares lenghts
  // that means that we're traversing the store from longest to
  // shortest mailbox name - this makes sure that we'll first see
  // and create mailboxes with longer "path"names that means 
  // submailboxes first
  success = 1; // TODO: this is bogus isn't it?
  for ( MailboxMap::iterator curr_mbox = store_a.boxes.begin(); 
        curr_mbox != store_b.boxes.end();
        curr_mbox++ )
  {
    if ( curr_mbox == store_a.boxes.end()) { // if we're done with store_a
      curr_mbox = store_b.boxes.begin();     // continue with store_b
      if ( curr_mbox == store_b.boxes.end()) break;
    }

    // skip if the current mailbox has allready been synched
    if ( curr_mbox->second.done)
      continue;
    
    // if mailbox doesn't exist in either one of the stores -> create it
    if ( store_a.boxes.find( curr_mbox->first ) == store_a.boxes.end() )
      if ( ! store_a.mailbox_create( curr_mbox->first ) )
        continue;
    if ( store_b.boxes.find( curr_mbox->first ) == store_b.boxes.end() )
      if ( ! store_b.mailbox_create( curr_mbox->first ) )
        continue;

    // when traversing store_a's boxes we don't need to worry about
    // whether it has been synched yet or not.  It isn't unless we're
    // in store_b that it matters whether the current mailbox has been
    // traversed in store_a allready
    store_b.boxes.find(curr_mbox->first)->second.done = true;

    // skip unselectable (== can't contain mails) boxes
    if ( store_a.boxes.find( curr_mbox->first )->second.no_select ) {
      if ( debug )
        printf( "%s is not selectable: skipping\n", curr_mbox->first.c_str() );
      continue;
    }
    if ( store_b.boxes.find( curr_mbox->first )->second.no_select ) {
      if ( debug )
        printf( "%s is not selectable: skipping\n", curr_mbox->first.c_str() );
      continue;
    }

    if (options.show_from)
      printf("\n *** %s ***\n", curr_mbox->first.c_str());

    MsgIdSet msgids_lasttime( lasttime[curr_mbox->first] ), msgids_union, msgids_now;
    MsgIdPositions msgidpos_a, msgidpos_b;

    if (options.show_summary) {
      printf("%s: ",curr_mbox->first.c_str());
      fflush(stdout);
    }
    else {
      printf("\n");
    }

    // fetch_message_ids(): map message-ids to message numbers
    //                      and optionally remember duplicates. 
    //
    // Attention: from here on we're operating on streams to single
    //            _mailboxes_! That means that from here on
    //            streamx_stream is connected to _one_ specific
    //            mailbox.
   
    // Messges that should be removed in store_a respectively in store_b
    MsgIdSet remove_a, remove_b;

    // open and fetch message ID's from the mailbox in the first store
    store_a.stream = store_a.mailbox_open( curr_mbox->first, OP_READONLY );
    if (! store_a.stream)
    {
      store_a.print_error( "opening and writing", curr_mbox->first);
      continue;
    }
    if (! store_a.fetch_message_ids( msgidpos_a , remove_a) )
    {
      store_a.print_error( "fetching of mail ids", curr_mbox->first);
      continue;
    }

    // if we're in sync mode open and fetch message IDs from the
    // mailbox in the second store
    if( operation_mode == mode_sync ) {
      store_b.stream = store_b.mailbox_open( curr_mbox->first, OP_READONLY);
      if (! store_b.stream) {
        store_b.print_error( "fetching of mail ids", curr_mbox->first);
        continue;
      }
      if (! store_b.fetch_message_ids( msgidpos_b, remove_b )) {
        store_b.print_error( "fetching of mail ids", curr_mbox->first);
        continue;
      }
    } else if( operation_mode == mode_diff ) {
      for( MsgIdSet::iterator i=msgids_lasttime.begin();
           i!=msgids_lasttime.end();
           i++ )
      {
           msgidpos_b[*i] = 0;
      }
    }

    // Create the set of all seen message IDs in a mailbox:
    // + message IDs seen the last time
    // + message IDs seen in the mailbox from store_a
    // + message IDs seen in the mailbox from store_b
    // 
    // msgids_union = union(msgids_lasttime, msgids_a, msgids_b)
    msgids_union = msgids_lasttime;
    for( MsgIdPositions::iterator i = msgidpos_a.begin();
         i != msgidpos_a.end() ;
         i++ )
    {
      msgids_union.insert(i->first);
    }
    for( MsgIdPositions::iterator i = msgidpos_b.begin();
         i != msgidpos_b.end();
         i++)
    {
      msgids_union.insert(i->first);
    }

    // Messages that should be copied from store_a to store_b,
    // from store_b to store_a
    MsgIdSet copy_a_b, copy_b_a;

    // Iterate over all messages that were seen in a mailbox last time,
    // in store_a and in store_b
    for ( MsgIdSet::iterator i=msgids_union.begin();
          i!=msgids_union.end();
          i++ )
    {
      // determine first what to do with a message
      bool in_a = msgidpos_a.count(*i);
      bool in_b = msgidpos_b.count(*i);
      bool in_l = msgids_lasttime.count(*i);

      int a_b_l = (  (in_a ? 0x100 : 0) 
                   + (in_b ? 0x010 : 0)
                   + (in_l ? 0x001 : 0) );

      switch (a_b_l) {

      case 0x100:  // New message on a
        copy_a_b.insert(*i);
        msgids_now.insert(*i);
        break;

      case 0x010:  // New message on b
        copy_b_a.insert(*i);
        msgids_now.insert(*i);
        break;

      case 0x111:  // Kept message
      case 0x110:  // New message, present in a and b, no copying
                   // necessary
        msgids_now.insert(*i);
        break;

      case 0x101:  // Deleted on b
        remove_a.insert(*i);
        break;

      case 0x011:  // Deleted on a
        remove_b.insert(*i);
        break;

      case 0x001:  // Deleted on both
        break;

      case 0x000:  // Shouldn't happen
      default:
        assert(0);
        break;
      }


    }

    unsigned long now_n = msgids_now.size();

    switch (operation_mode) {
    
     /////////////////////////// mode_sync ///////////////////////////
    
     case mode_sync:
      {
        bool success;
        unsigned long removed_a = 0, removed_b = 0, copied_a_b = 0,
                      copied_b_a = 0;

        //////////////////// copying messages ///////////////////////
        
        if (debug)
          printf( " Copying messages from store \"%s\" to store \"%s\"\n",
                  store_a.name.c_str(), store_b.name.c_str() );

        if (! channel.open_for_copying( curr_mbox->first, a_to_b) )
          exit(1);
        for ( MsgIdSet::iterator i =copy_a_b.begin(); i !=copy_a_b.end(); i++) {
          success = channel.copy_message( msgidpos_a[*i], *i,
                                          curr_mbox->first, a_to_b );
          if (success) copied_a_b++;
          else         msgids_now.erase(*i);
          // if we've failed to copy the message over we'll pretend that we
          // haven't seen it at all. That way mailsync will have to rediscover
          // and resync the same message again next time
        }

        if (debug)
          printf( " Copying messages from store \"%s\" to store \"%s\"\n",
                  store_b.name.c_str(), store_a.name.c_str() );

        if (! channel.open_for_copying( curr_mbox->first, b_to_a) )
          exit(1);
        for ( MsgIdSet::iterator i=copy_b_a.begin(); i !=copy_b_a.end(); i++) {
          success = channel.copy_message( msgidpos_b[*i], *i,
                                          curr_mbox->first, b_to_a );
          if (success) copied_b_a++;
          else         msgids_now.erase(*i);
        }
        
        printf("\n");
        if (copied_a_b) printf( "%lu copied %s->%s.\n", copied_a_b,
                                store_a.name.c_str(), store_b.name.c_str() );
        if (copied_b_a) printf( "%lu copied %s->%s.\n", copied_b_a,
                                store_b.name.c_str(), store_a.name.c_str() );
        if (removed_a)  printf( "%lu deleted on %s.\n",
                                removed_a, store_a.name.c_str() );
        if (removed_b)  printf( "%lu deleted on %s.\n",
                                removed_b, store_b.name.c_str() );
        if (options.show_summary) {
          printf( "%lu remain%s.\n", now_n, now_n != 1 ? "" : "s");
          fflush(stdout);
        } else {
          printf( "%lu messages remain in %s\n",
                  now_n, curr_mbox->first.c_str() );
        }

        //////////////////// removing messages ///////////////////////

        if ( options.delete_messages && (! options.simulate) ) {
        
          if (debug) printf( " Removing messages from store \"%s\"\n",
                             store_a.name.c_str() );

          // TODO: check first if there are any messages to be removed before
          //       opening
          store_a.stream = store_a.mailbox_open( curr_mbox->first, 0 );
          if (! store_a.stream)
          {
            store_a.print_error( "opening for removal ", curr_mbox->first);
          }
          else
            for( MsgIdSet::iterator i =remove_a.begin(); i !=remove_a.end(); i++) {
              success = store_a.flag_message_for_removal( msgidpos_a[*i], *i, "< ");
              if (success) removed_a++;
            }
      
          if (debug) printf( " Removing messages from store \"%s\"\n",
                             store_b.name.c_str() );

          // TODO: check first if there are any messages to be removed before
          //       opening
          store_b.stream = store_b.mailbox_open( curr_mbox->first, 0 );
          if (! store_b.stream)
          {
            store_a.print_error( "opening for removal ", curr_mbox->first);
          }
          else
            for( MsgIdSet::iterator i =remove_b.begin(); i !=remove_b.end(); i++) {
              success = store_b.flag_message_for_removal( msgidpos_b[*i], *i, "> ");
              if (success) removed_b++;
            }

          //////////////////////// expunging emails /////////////////////////
          // this *needs* to be done *after* coying as the *last* step
          // otherwise the order of the mails will get messed up since
          // some random messages inbewteen have been deleted in the mean
          // time and the message numbers we know don't correspond to
          // messages in the mailbox/store any more
        
          if (debug) printf( " Expunging messages\n" );

          int n_expunged_a = store_a.mailbox_expunge( curr_mbox->first );
          int n_expunged_b = store_b.mailbox_expunge( curr_mbox->first );
          if (n_expunged_a) printf( "Expunged %d mail%s in store %s\n"
                                  , n_expunged_a
                                  , n_expunged_a == 1 ? "" : "s"
                                  , store_a.name.c_str() );
          if (n_expunged_b) printf( "Expunged %d mail%s in store %s\n"
                                  , n_expunged_b
                                  , n_expunged_b == 1 ? "" : "s"
                                  , store_b.name.c_str() );
        }

        //////////////////////// deleting empty mailboxes /////////////////////////
        
        if (options.delete_empty_mailboxes) {
          if (now_n == 0) {
            // add empty mailbox to empty_mailboxes
            empty_mailboxes[ curr_mbox->first ];
            deleted_mailboxes[ curr_mbox->first ];
          }
        }
      } // end case mode_sync
      break;

     /////////////////////////// mode_diff ///////////////////////////
    
     case mode_diff:
      {
        if ( copy_a_b.size() )
          printf( "%d new, ", copy_a_b.size() );
        if (remove_b.size())
          printf( "%d deleted, ", remove_b.size() );
        printf( "%d currently at store %s.\n",
                msgids_now.size(), store_b.name.c_str());
      }
      break;

     default:
      break;
    }

    thistime[curr_mbox->first] = msgids_now;

// TODO: why do we want to close the boxes?
// instead of expunging emails we could also use mail_open(OP_EXPUNGE) instead...
//    // close local boxes
//    if (!store_a.isremote)
//      store_a.stream = mail_close(store_a.stream);
//    if (store_b.stream && !store_b.isremote)
//      store_b.stream = mail_close(store_b.stream);

  } // end loop over all mailboxes

  if (store_a.isremote) store_a.stream = mail_close(store_a.stream);
  if (store_b.isremote) store_b.stream = mail_close(store_b.stream);

  // TODO: which success are we talking about? Above there are two instances
  //       of "success" declared which mask each other out...
  if (!success)
    return 1;

  if ( options.delete_empty_mailboxes && operation_mode==mode_sync )
  {
    string fullboxname;

    if (store_a.isremote) {
      store_a.stream = NIL;
      store_a.store_open( OP_HALFOPEN );
    } else {
      store_a.stream = NULL;
    }
    if (store_b.isremote) {
      store_b.stream = NIL;
      store_b.store_open( OP_HALFOPEN );
    } else {
      store_b.stream = NULL;
    }
    for ( MailboxMap::iterator mailbox = empty_mailboxes.begin() ; 
          mailbox != empty_mailboxes.end() ;
          mailbox++ )
    {
      fullboxname = store_a.full_mailbox_name( mailbox->first);
      printf("%s: deleting\n", mailbox->first.c_str());
      printf("  %s", fullboxname.c_str());
      fflush(stdout);
      current_context_passwd = &(store_a.passwd);
      if (mail_delete(store_a.stream, nccs(fullboxname)))
        printf("\n");
      else
        printf(" failed\n");
      fullboxname = store_b.full_mailbox_name( mailbox->first);
      printf("  %s", fullboxname.c_str());
      fflush(stdout);
      current_context_passwd = &(store_b.passwd);
      if (mail_delete(store_b.stream, nccs(fullboxname))) 
        printf("\n");
      else
        printf(" failed\n");
    }
  }

  if (operation_mode==mode_sync)
    if (!options.simulate)
      channel.write_thistime_seen( deleted_mailboxes, thistime);

  return 0;
}
