import 'dart:io';
import 'dart:convert';
import 'package:nostr_console/event_ds.dart';
import 'package:nostr_console/tree_ds.dart';
import 'package:nostr_console/relays.dart';
import 'package:nostr_console/settings.dart';
import 'package:nostr_console/utils.dart';
import 'package:nostr_console/user.dart';
import 'package:bip340/bip340.dart';

Future<void> processAnyIncomingEvents(Store node, [bool printNotifications = true])  async {
  //print("In process incoming");
  reAdjustAlignment();

  const int waitMilliSeconds1 = 100, waitMilliSeconds2 = 200;

  Future<void> foo1() async {
    await Future.delayed(Duration(milliseconds: waitMilliSeconds1));
    return;
  }
  await foo1();

  // need a bit of wait to give other events to execute, so do a delay, which allows
  // relays to recieve and handle new events
  Future.delayed(const Duration(milliseconds: waitMilliSeconds2 ), ()  {
    
    Set<String> newEventIds = node.processIncomingEvent(getRecievedEvents());
    clearEvents();

    List<int> numPrinted1 = [0,0,0];
    if( printNotifications) {
      // print all the new trees, the ones that we want to print
      print("");
      numPrinted1 = node.printTreeNotifications(newEventIds);

      // need to clear because only top 20 events in each thread are printed or cleared with above
      int clearNotifications (Tree t) => t.treeSelector_clearNotifications();
      node.traverseStoreTrees(clearNotifications);

      // print direc room notifications if any, and print summary of all notifications printed
      directRoomNotifications(node, numPrinted1[0], numPrinted1[2]);
    }

  });

  
  Future<void> foo() async {
    await Future.delayed(Duration(milliseconds: waitMilliSeconds2));
    return;
  }
  await foo();
}

String mySign(String privateKey, String msg) {
  String randomSeed = getRandomPrivKey();
  randomSeed = randomSeed.substring(0, 32);
  return sign(privateKey, msg, randomSeed);
}

Future<void> mySleep(int duration) async {
  Future<void> foo() async {
    await Future.delayed(Duration(milliseconds: duration));
    return;
  }
  await foo();  
}

/* @function sendReplyPostLike Used to send Reply, Post and Like ( event 1 for reply and post, and event 7 for like/reaction)
 * If replyToId is blank, then it does not reference any e/p tags, and thus becomes a top post
 * otherwise e and p tags are found for the given event being replied to, if that event data is available
 */
Future<void> sendReplyPostLike(Store node, String replyToId, String replyKind, String content) async {
  content = addEscapeChars(content);
  String strTags = node.getTagStr(replyToId, exename, true, getTagsFromContent(content));
  if( replyToId.isNotEmpty && strTags == "") { // this returns empty only when the given replyto ID is non-empty, but its not found ( nor is it 64 bytes)
    print("${gWarningColor}The given target id was not found and/or is not a valid id. Not sending the event.$gColorEndMarker"); 
    return; 
  }

  int    createdAt = DateTime.now().millisecondsSinceEpoch ~/1000;
  String id = getShaId(userPublicKey, createdAt.toString(), replyKind, strTags, content);

  // generate POW if required
  String vanityTag = strTags;
  if (replyKind == "1" && gDifficulty > 0) {
    if( gDebug > 0) log.info("Starting pow");

    int numBytes = (gDifficulty % 4 == 0)? gDifficulty ~/ 4: gDifficulty ~/ 4 + 1;
    String zeroString = "";
    for( int i = 0; i < numBytes; i++) {
      zeroString += "0";
    }

    int numShaDone = 0;
    for( numShaDone = 0; numShaDone < 100000000; numShaDone++) {
      vanityTag = '$strTags,["nonce","$numShaDone","$gDifficulty"]';
      id = getShaId(userPublicKey, createdAt.toString(), replyKind, vanityTag, content);
      if( id.substring(0, numBytes) == zeroString) {
        break;
      }
    }

    await mySleep(500);

    if( gDebug > 0) log.info("Ending pow numShaDone = $numShaDone id = $id");
  }

  String sig = mySign(userPrivateKey, id);
  
  String toSendMessage = '["EVENT",{"id":"$id","pubkey":"$userPublicKey","created_at":$createdAt,"kind":$replyKind,"tags":[$vanityTag],"content":"$content","sig":"$sig"}]';
  //print("sending $toSendMessage");
  sendRequest( gListRelayUrls, toSendMessage);
  await mySleep(200);
}

// Sends a public channel message
Future<void> sendChannelMessage(Store node, Channel channel, String messageToSend, String replyKind) async {
  messageToSend = addEscapeChars(messageToSend);

  String strTags = node.getTagStrForChannel(channel, exename);
  int    createdAt = DateTime.now().millisecondsSinceEpoch ~/1000;
  
  String id = getShaId(userPublicKey, createdAt.toString(), replyKind, strTags, messageToSend);
  String sig = mySign(userPrivateKey, id);

  String toSendMessage = '["EVENT",{"id":"$id","pubkey":"$userPublicKey","created_at":$createdAt,"kind":$replyKind,"tags":[$strTags],"content":"$messageToSend","sig":"$sig"}]';

  //printInColor(toSendMessage, gCommentColor);
  sendRequest( gListRelayUrls, toSendMessage);

  Future<void> foo() async {
    await Future.delayed(Duration(milliseconds: 300));
    return;
  }
  await foo();
}

// Sends a public channel message
Future<void> sendChannelReply(Store node, Channel channel, String replyTo, String messageToSend, String replyKind) async {

  messageToSend = addEscapeChars(messageToSend);

  String strTags = node.getTagStrForChannelReply(channel, replyTo, exename);
  int    createdAt = DateTime.now().millisecondsSinceEpoch ~/1000;
  
  String id = getShaId(userPublicKey, createdAt.toString(), replyKind, strTags, messageToSend);
  String sig = mySign(userPrivateKey, id);

  String toSendMessage = '["EVENT",{"id":"$id","pubkey":"$userPublicKey","created_at":$createdAt,"kind":$replyKind,"tags":[$strTags],"content":"$messageToSend","sig":"$sig"}]';
  //printInColor(toSendMessage, gCommentColor);
  sendRequest( gListRelayUrls, toSendMessage);

  Future<void> foo() async {
    await Future.delayed(Duration(milliseconds: 300));
    return;
  }
  await foo();
}

// send DM
Future<void> sendDirectMessage(Store node, String otherPubkey, String messageToSend, {String replyKind = "4"}) async {
  //messageToSend = addEscapeChars(messageToSend); since this get encrypted , it does not need escaping
  String otherPubkey02 = "02$otherPubkey";
  String encryptedMessageToSend =        myEncrypt(userPrivateKey, otherPubkey02, messageToSend);

  //print("in sendDirectMessage: replyKind = $replyKind");

  //String replyKind = "4";
  String strTags = '["p","$otherPubkey"]';
  strTags += gWhetherToSendClientTag?',["client","nostr_console"]':'';
  int    createdAt = DateTime.now().millisecondsSinceEpoch ~/1000;

  String id = getShaId(userPublicKey, createdAt.toString(), replyKind, strTags, encryptedMessageToSend);
  String sig = mySign(userPrivateKey, id);
  String eventStrToSend = '["EVENT",{"id":"$id","pubkey":"$userPublicKey","created_at":$createdAt,"kind":$replyKind,"tags":[$strTags],"content":"$encryptedMessageToSend","sig":"$sig"}]';
  //print("calling send for str : $eventStrToSend");
  sendRequest( gListRelayUrls, eventStrToSend);

  Future<void> foo() async {
    await Future.delayed(Duration(milliseconds: 300));
    return;
  }
  await foo();
}

// sends event e; used to send kind 3 event; can send other kinds too like channel create kind 40, or kind 0
// does not honor tags mentioned in the Event, excpet if its kind 3, when it uses contact list to create tags
Future<String> sendEvent(Store node, Event e, [int delayAfterSend = 500]) async {
  String strTags = "";
  int    createdAt = DateTime.now().millisecondsSinceEpoch ~/1000;
  String content = addEscapeChars( e.eventData.content);

  // read the contacts and make them part of the tags, and then the sha
  if( e.eventData.kind == 3) {
    strTags = ""; // only new contacts will be sent
    for(int i = 0; i < e.eventData.contactList.length; i++) {
      String relay = e.eventData.contactList[i].relay;
      if( relay == "") {
        relay = defaultServerUrl;
      }

      String comma = ",";
      if( i == e.eventData.contactList.length - 1) {
        comma = "";
      }
      String strContact = '["p","${e.eventData.contactList[i].contactPubkey}","$relay"]$comma';
      strTags += strContact;
    }
    
    // strTags += '["client","nostr_console"]';
  } else {
    strTags += '["client","nostr_console"]';
  }

  String id = getShaId(userPublicKey, createdAt.toString(), e.eventData.kind.toString(), strTags, content);
  String sig = mySign(userPrivateKey, id);

  String toSendMessage = '["EVENT",{"id":"$id","pubkey":"$userPublicKey","created_at":$createdAt,"kind":${e.eventData.kind.toString()},"tags":[$strTags],"content":"$content","sig":"$sig"}]';
  //print("in send event: calling sendrequest for string \n $toSendMessage");
  sendRequest(gListRelayUrls, toSendMessage);

  Future<void> foo() async {
    await Future.delayed(Duration(milliseconds: delayAfterSend));
    return;
  }
  await foo();
  return id;
}

Future<String> sendEventWithTags(Store node, Event e, String tags) async {
  String strTags = tags;
  int    createdAt = DateTime.now().millisecondsSinceEpoch ~/1000;
  String content = addEscapeChars( e.eventData.content);

  String id = getShaId(userPublicKey, createdAt.toString(), e.eventData.kind.toString(), strTags, content);
  String sig = mySign(userPrivateKey, id);

  String toSendMessage = '["EVENT",{"id":"$id","pubkey":"$userPublicKey","created_at":$createdAt,"kind":${e.eventData.kind.toString()},"tags":[$strTags],"content":"$content","sig":"$sig"}]';
  //print("in send event: calling sendrequest for string \n $toSendMessage");
  sendRequest(gListRelayUrls, toSendMessage);

  Future<void> foo() async {
    await Future.delayed(Duration(milliseconds: 500));
    return;
  }
  await foo();
  return id;
}

bool sendDeleteEvent(Store node, String eventIdToDelete) {
  if( node.allChildEventsMap.containsKey(eventIdToDelete)) {
    Tree? tree = node.allChildEventsMap[eventIdToDelete];
    if( tree != null) {
      if( tree.event.eventData.id == eventIdToDelete && tree.event.eventData.pubkey == userPublicKey) {
        // to delte this event
        String replyKind = "5"; // delete event
        String content = "";
        String strTags = '["e","$eventIdToDelete"]';
        strTags += gWhetherToSendClientTag?',["client","nostr_console"]':'';

        int    createdAt = DateTime.now().millisecondsSinceEpoch ~/1000;
        String id = getShaId(userPublicKey, createdAt.toString(), replyKind, strTags, content);

        String sig = mySign(userPrivateKey, id);
        String toSendMessage = '["EVENT",{"id":"$id","pubkey":"$userPublicKey","created_at":$createdAt,"kind":$replyKind,"tags":[$strTags],"content":"$content","sig":"$sig"}]';
        sendRequest( gListRelayUrls, toSendMessage);
        print("sent event delete request with id = $id");
      } else {
        print("${gWarningColor}The given id was not found and/or is not a valid id, or is not your event. Not deleted.$gColorEndMarker"); 
      }
    } else {
      print("Event not found. Kindly ensure you have entered a valid event id.");
    }
  }

  return false;
}

void reAdjustAlignment() {
    // align the text again in case the window size has been changed
    if( gAlignment == "center") {
      try {
        var terminalColumns = gDefaultTextWidth;

        if( stdout.hasTerminal ) {
          terminalColumns = stdout.terminalColumns;
        }

        if(  gTextWidth > terminalColumns) {
          gTextWidth = terminalColumns - 5;
        }          
        gNumLeftMarginSpaces = (terminalColumns - gTextWidth )~/2;
      } on StdoutException catch (e) {
        print("Terminal information not available");
        if( gDebug>0)  print(e.message);
        gNumLeftMarginSpaces = 0;
      }
    }
    Store.reCalculateMarkerStr();
}

void printProfile(Store node, String profilePubkey) {
  bool onlyUserPostAndLike (Tree t) => t.treeSelectorUserPostAndLike({profilePubkey});
  node.printStoreTrees(0, DateTime.now().subtract(Duration(hours:gHoursDefaultPrint)), onlyUserPostAndLike);

  // if contact list was found, get user's feed, and keep the contact list for later use 
  String authorName = getAuthorName(profilePubkey);
  String pronoun = "";
  if( profilePubkey == userPublicKey) {
    printUnderlined("\nYour profile - $authorName:");
    pronoun = "You";
  } else {
    printUnderlined("\nProfile for $authorName");
    pronoun = "They";
  }

  String about = gKindONames[profilePubkey]?.about??"";
  String picture = gKindONames[profilePubkey]?.picture??"";
  String lud06 = gKindONames[profilePubkey]?.lud06??"";
  String lud16 = gKindONames[profilePubkey]?.lud16??"";
  String displayName= gKindONames[profilePubkey]?.display_name??"";
  String website = gKindONames[profilePubkey]?.website??"";
  int    dateLastUpdated    = gKindONames[profilePubkey]?.createdAt??0;
  bool   verified = gKindONames[profilePubkey]?.nip05Verified??false;
  String nip05Id  = gKindONames[profilePubkey]?.nip05Id??"";

  // print QR code
  print("The QR code for public key:\n\n");
  try {
    print(getPubkeyAsQrString(profilePubkey));
  } catch(e) {
    print("Could not generate qr code.  \n");
  }

  // print LNRUL lud06 if it exists
  if( lud06.length > gMinLud06AddressLength) {
    try {
      String lud06LNString = "lightning:$lud06";

      List<int>? typesAndModule = getTypeAndModule(lud06LNString);
      if( typesAndModule != null) {
        print("Printing lud06 LNURL as QR:\n\n");
        print(getPubkeyAsQrString(lud06LNString, typesAndModule[0], typesAndModule[1]));
      }
    } catch(e) {
      print("Could not generate qr code for the lnurl given.  \n");
    }
  }

  // print LNRUL lud16 if it exists
  if( lud16.length > gMinLud16AddressLength) {
    try {
      String lud16LNString = lud16;
      List<int>? typesAndModule = getTypeAndModule(lud16LNString);
      if( typesAndModule != null) {
        print("Printing lud16 address as QR:\n\n");
        print(getPubkeyAsQrString(lud16LNString, typesAndModule[0], typesAndModule[1]));
      }
    } catch(e) {
      print("Could not generate qr code for the given address.\n");
    }
  }

  print("\nName        : $authorName ( $profilePubkey ).");
  print("About       : $about");
  print("Picture     : $picture");
  print("display_name: $displayName");
  print("Website     : $website");
  print("Lud06       : $lud06");
  print("Lud16       : $lud16");
  print("Nip 05      : ${verified?"yes. $nip05Id":"no"}");
  print("\nLast Updated: ${getPrintableDate(dateLastUpdated)}\n");

  // get the latest kind 3 event for the user, which lists his 'follows' list
  Event? profileContactEvent = getContactEvent(profilePubkey);
  if (profileContactEvent != null ) {
    
    if( profilePubkey != userPublicKey) {
      if( profileContactEvent.eventData.contactList.any((x) => (x.contactPubkey == userPublicKey))) {
          print("* They follow you");
      } else {
          print("* They don't follow you");
      }
    }

    // print mutual follows
    node.printMutualFollows(profileContactEvent, authorName);
    print("");
    
    // print follow list
    stdout.write("$pronoun follow ${profileContactEvent.eventData.contactList.length} accounts:  ");
    profileContactEvent.eventData.contactList.sort();
    for (var x in profileContactEvent.eventData.contactList) {
      stdout.write("${getAuthorName(x.contactPubkey)}, ");
    }
    print("\n");
  }

  // print followers
  List<String> followers = node.getFollowers(profilePubkey);
  stdout.write("$pronoun have ${followers.length} followers:  ");
  followers.sort((a, b) => getAuthorName(a).compareTo(getAuthorName(b)));
  for (var x in followers) {
    stdout.write("${getAuthorName(x)}, ");
  }
  print("");              
  print("");
}

void printVerifiedAccounts(Store node) {

  List<dynamic> listVerified = []; // num follows, pubkey, name, nip05id

  printUnderlined("NIP 05 Verified Users");
  print("")  ;
  print("Username                    Num Followers       pubkey                                                             Nip Id\n");

  gKindONames.forEach((key, value) {
    String pubkey = key;
    if( value.nip05Verified) {
      List<String> followers = node.getFollowers(pubkey);
      listVerified.add([followers.length, pubkey, getAuthorName(pubkey), value.nip05Id]);
    }
  });

  listVerified.sort((a, b) => a[0] > b[0]? -1: (a[0] == b[0]? 0: 1));
  for(var verifiedEntry in listVerified) {
    print("${verifiedEntry[2].padRight(30)}  ${verifiedEntry[0].toString().padRight(4)}            ${verifiedEntry[1]}   ${verifiedEntry[3]}");
  }
  print("\nHow to use: To get best results, print the main feed a couple of times right after starting; and then check NIP verified list. This gives application time to do the verification from user's given servers.\n\n");
}

void printMenu(List<String> menuOptions) {

  int longestMenuOption = 0;
  for(int i = 0; i < menuOptions.length;i++) {
    if( longestMenuOption < menuOptions[i].length) {
      longestMenuOption = menuOptions[i].length;
    }
  }

  var terminalColumns = gDefaultTextWidth;

  if( stdout.hasTerminal ) {
    terminalColumns = stdout.terminalColumns;
  }

  if( longestMenuOption + 5> gMenuWidth ) {
    gMenuWidth = longestMenuOption + 8;
  }

  if( terminalColumns~/gMenuWidth > 4) {
    terminalColumns = gMenuWidth * 4;
  }

  int rowLen = 0;
  for(int i = 0; i < menuOptions.length;i++) {
    String str = "${i+1}. ${menuOptions[i]}";
    str = str.padRight(gMenuWidth);
    stdout.write(str);
    rowLen += gMenuWidth;

    if( rowLen + gMenuWidth> terminalColumns ) {
      stdout.write("\n" );
      rowLen = 0;
    }
  }
  stdout.write("\n" );
}

int showMenu(List<String> menuOptions, String menuName, [String menuInfo = ""]) {

  if(menuInfo.isNotEmpty) {
    print("\n$menuInfo\n");
  }

  while(true) {
    printInColor("                                     $menuName", yellowColor);
    print("\n");

    printMenu(menuOptions);
    String promptWithName = userPrivateKey.length == 64? 
                              "Signed in as $gCommentColor${getAuthorName(userPublicKey)}$gColorEndMarker": 
                              "${gWarningColor}You are not signed in so can't send any messages$gColorEndMarker";

    stdout.write("$promptWithName. ");
    stdout.write("Type option number: ");
    String? userOptionInput = stdin.readLineSync();
    String userOption = userOptionInput??"";

    userOption = userOption.trim();
    if( userOption == 'x') {
      userOption = menuOptions.length.toString();
    }
    if( int.tryParse(userOption) != null) {
      try{
        int? valueOption = int.tryParse(userOption);
        if( valueOption != null) {
          if( valueOption >= 1 && valueOption <= menuOptions.length) {
            reAdjustAlignment(); // in case user has changed alignment
            print('You picked: $valueOption');
            // reset this
            gInvalidInputCount = 0;
            return valueOption;
          }
        }
      } on FormatException catch (e) {
        print(e.message);
      } on Exception catch (e) {
        print(e);
      }    
    }
    printWarning("\nInvalid option. Kindly try again. The valid options are from 1 to ${menuOptions.length}");
    gInvalidInputCount++;

    if( gInvalidInputCount > gMaxInValidInputAccepted) {
      printWarning("The program has received an invalid input more than $gMaxInValidInputAccepted. There seems to be some problem etc, so exiting");
      exit(0);
      //programExit();
    }
  }
}

bool confirmFirstContact() {
  String s = getStringFromUser(
    """\nIt appears your contact list is empty. 
If you are a new user, you should proceed, but if you already 
had added people in past, then that contact list would be overwritten. 
         
Do you want to proceed. Press y/Y or n/N: """, "n");

  if( s == 'y' || s == 'Y') {
    return true;
  }

  return false;
}

void printPubkeys(Set<String> pubkey) {
  print("${myPadRight("pubkey",64)}  ${myPadRight("name", 20)}    ${myPadRight("about", 40)}   ${myPadRight("Nip05", 30)}");
  for (var x in pubkey) {
    print("$x  ${myPadRight(getAuthorName(x),  20)}    ${myPadRight(gKindONames[x]?.about??"", 40)}   ${myPadRight(gKindONames[x]?.nip05Id??"No", 30)}");
  }
  print("");
}

void printPubkeyResult(Set<String> pubkey) {

  if( pubkey.isEmpty) {
    print("There is no pubkey for that given name.");
    return;
  } else {
    if( pubkey.length == 1) {
      print("There is 1 public key for the given name, which is: ");
    } else {
      print("There are ${pubkey.length} public keys for the given name, which are: ");
    }
  }
  printPubkeys(pubkey);
}

Future<void> otherOptionsMenuUi(Store node) async {
  bool continueOtherMenu = true;
  while(continueOtherMenu) {

    await processAnyIncomingEvents(node); // this takes 300 ms

    int option = showMenu([ 
                            'Search by client name',         // 1
                            'Edit your profile',             // 2
                            'Delete event',                  // 3
                            'Re-Broadcast contact list+',    // 4
                            'Application stats',             // 5
                            'Help and About',               // 6
                            'E(x)it to main menu'],          // 7

                          "Other Options Menu");                     // menu name
    switch(option) {
      case 1:
        stdout.write("Enter nostr client name whose events you want to see: ");
        String? $tempWords = stdin.readLineSync();
        String clientName = $tempWords??"";
        if( clientName != "") {
          bool fromClient (Tree t) => t.treeSelectorClientName(clientName);
          node.printStoreTrees(0, DateTime.now().subtract(Duration(hours:gHoursDefaultPrint)), fromClient); // search for last gHoursDefaultPrint hours only
        }
        break;

      case 2: //edit your profile
        if( userPublicKey == "" || userPrivateKey == "") {
          printWarning("No private key provided so you can't edit your profile.");
          break;
        }

        print("Your current name: ${getAuthorName(userPublicKey)}");
        print("Your 'about me': ${gKindONames[userPublicKey]?.about}");
        print("Your current profile picture: ${gKindONames[userPublicKey]?.picture}");
        print("Your current display name: ${gKindONames[userPublicKey]?.display_name}");
        print("Your current website: ${gKindONames[userPublicKey]?.website}");
        print("Your current NIP 05 id: ${gKindONames[userPublicKey]?.nip05Id}");
        print("Your current lud06: ${gKindONames[userPublicKey]?.lud06}");
        print("Your current lud16: ${gKindONames[userPublicKey]?.lud16}");


        // TODO use robohash in future: https://robohash.org/npub19yzp0sntplrcl6v85kxqahtkqzyh03s9g0w6suljfzmqm0uf5ywqwpjkda

        print("\n\nEnter new data. Leave blank to use the old value. Some clients use name, others use display name; you can enter same value for both:\n");
        String userName =     getStringFromUser("Enter your new name                  : ", getAuthorName(userPublicKey));
        String userAbout =    getStringFromUser("Enter new 'about me' for yourself    : ", gKindONames[userPublicKey]?.about??"");
        String userPic =      getStringFromUser("Enter url to your new display picture: ", gKindONames[userPublicKey]?.picture??"https://placekitten.com/200/200");
        String displayName = getStringFromUser("Enter your new display name          : ", gKindONames[userPublicKey]?.display_name??"");
        String website =      getStringFromUser("Enter your new website               : ", gKindONames[userPublicKey]?.website??"");
        String nip05id =   getStringFromUser("Enter your nip 05 id. Leave blank if unknown/none: ", gKindONames[userPublicKey]?.nip05Id??"");
        String lud06 =     getStringFromUser("Enter your lud06 or lnurl. Leave blank if unknown/none: ", gKindONames[userPublicKey]?.lud06??"");
        String lud16 =     getStringFromUser("Enter your lud16 address. Leave blank if unknown/none: ", gKindONames[userPublicKey]?.lud16??"");
        
        String strLud06 =  lud06.isNotEmpty? '"lud06":"$lud06",': ''; 
        String strLud16 =  lud16.isNotEmpty? '"lud16":"$lud16",': ''; 
        String strDispName =  displayName.isNotEmpty? '"display_name":"$displayName",': ''; 
        String strWebsite =  website.isNotEmpty? '"website":"$website",': ''; 

        String content = "{\"name\": \"$userName\", \"about\": \"$userAbout\", \"picture\": \"$userPic\"${ nip05id.isNotEmpty ? ", $strDispName $strWebsite $strLud06 $strLud16 \"nip05\": \"$nip05id\"":""}}";
        int    createdAt = DateTime.now().millisecondsSinceEpoch ~/1000;

        EventData eventData = EventData('id', userPublicKey, createdAt, 0, content, [], [], [], [], {}, );
        Event userKind0Event = Event("EVENT", "id", eventData, [], "");
        String userKind0EventId = await sendEvent(node, userKind0Event); // takes 400 ms
        printInColor("Updated your profile.\n", gCommentColor);
        await processAnyIncomingEvents(node, false); // get latest event, this takes 300 ms
        break;

      case 3:
        if( userPublicKey == "" || userPrivateKey == "") {
          printWarning("No private key provided so you can't delete any event.");
          break;
        }

        stdout.write("Enter event id to delete: ");
        String? $tempEventId = stdin.readLineSync();
        String userInputId = $tempEventId??"";
        Set<String> eventIdToDelete = node.getEventEidFromPrefix(userInputId);

        if( eventIdToDelete.length == 1) {
          String toDeleteId = eventIdToDelete.first;
          print("Going to send a delete event for the following event with id $toDeleteId");
          sendDeleteEvent(node, eventIdToDelete.first);
          await processAnyIncomingEvents(node, false); // get latest event, this takes 300 ms
        } else {
          if( eventIdToDelete.isEmpty) {
            printWarning("Could not find the given event id. Kindly try again, by entering a 64 byte long hex event id, or by entering a unique prefix for the given event id.");
          } else {
            printWarning("Invalid Event Id(s). Kindly enter a more unique id.");
          }
        }
        break;

      case 4:
        print("TODO");
        break;
        printSet(gListRelayUrls, "Going to broadcast your contact list ( kind 3) and About me( kind 0) to all relays. The relays are: ", ",");
        stdout.write("Hold on, sending events to relays ...");
        
        int count  = 0;
        Set<int> kindBroadcast = {};
        node.allChildEventsMap.forEach((id, tree) {
          if(  tree.event.eventData.pubkey == userPublicKey && [0,3].contains(tree.event.eventData.kind)) {
            sendEvent(node, tree.event, 100);
            count++;
          }
        });
        stdout.write("..done\n");
        print("\nFinished re-broadcasting $count events to all the servers.");

        break;

      case 5: // application info
        print("\n\n");
        printUnderlined("Application stats");
        //print("\n");
        relays.printInfo();
        print("\n");
        printUnderlined("Event and User Info");
        //print("Total number of kind-1 posts:  ${node.count()}");

        print("\nEvent distribution by event kind:\n");
          node.printEventInfo();
        print("\nTotal number of all events:    ${node.allChildEventsMap.length}");

        print("\nTotal events translated for $gNumTranslateDays days: $numEventsTranslated");

        print("Total number of user profiles: ${gKindONames.length}\n");
        printUnderlined("Logged in user Info");
        if( userPrivateKey.length == 64) {
          print("You are signed in, and your public key is:       $userPublicKey");
        } else {
          print("You are not signed in, and are using public key: $userPublicKey");
        }
        print("Your name as seen in metadata event is:          ${getAuthorName(userPublicKey)}\n");

        printVerifiedAccounts(node);
        break;

      case 6:
        print(helpAndAbout);
        break;

      case 7:
        continueOtherMenu = false;
        break;

      default:
        break;
    }
  }
  return;
}

// sends event creating a new public channel
Future<void> createPublicChannel(Store node) async {
  String channelName = getStringFromUser("Enter channel name: ");
  String channelAbout = getStringFromUser("Enter description for channel: ");
  String channelPic = getStringFromUser("Enter display picture if any: ", "https://placekitten.com/200/200");
  String content = "{\"name\": \"$channelName\", \"about\": \"$channelAbout\", \"picture\": \"$channelPic\"}";
  int    createdAt = DateTime.now().millisecondsSinceEpoch ~/1000;

  EventData eventData = EventData('id', userPublicKey, createdAt, 40, content, [], [], [], [], {}, );
  Event channelCreateEvent = Event("EVENT", "id", eventData, [], "");
  String newChannelId = await sendEvent(node, channelCreateEvent); // takes 400 ms
  print("Created new channel with id: $newChannelId");
  await processAnyIncomingEvents(node, false); // get latest event, this takes 300 ms
}

Future<void> channelMenuUI(Store node) async {
  bool continueChatMenu = true;
  
  bool justShowedChannels = false;
  while(continueChatMenu) {

    await processAnyIncomingEvents(node); // this takes 300 ms

    if( !justShowedChannels) {
      printInColor("                                     Public Channels ", gCommentColor);
      node.printChannelsOverview(node.channels, gNumRoomsShownByDefault, selectorShowAllRooms, node.allChildEventsMap, null);
      justShowedChannels = true;
    }

    String menuInfo = """Public channel howto: To enter a channel, enter first few letters of its name or the channel identifier. 
                      When inside a channel, the first column is the id of the given post. It can be used when you want to reply to a specific post.
                      To reply to a specific post, type '/reply <first few letters of id of post to reply to> <your message>. 
                      The most latest updated channels are shown at bottom.
                      When in a channel, press 'x' to exit. """;
    int option = showMenu([ 'Enter a channel',           // 1
                            'Show all public channels',  // 2
                            'Show all tag channels',     // 3
                            'Create a public channel',   // 4
                            'E(x)it to main menu'],      // 5
                          "Public Channels Menu", // name of menu
                          menuInfo);
    switch(option) {
      case 1:

        justShowedChannels = false;
        bool showChannelOption = true;
        stdout.write("\nType channel id or name, or their 1st few letters; or type 'x' to go to menu: ");
        String? $tempUserInput = stdin.readLineSync();
        String channelId = $tempUserInput??"";

        if( channelId == "x") {
          showChannelOption = false; 
          clearScreen();
        }
        int pageNum = 1;
        bool firstIteration = true;
        while(showChannelOption) {
          reAdjustAlignment();
          if( firstIteration) {
            clearScreen();
            firstIteration = false;
          }

          String fullChannelId = node.showChannel(node.channels, channelId, null, null, null, pageNum); // direct channel does not need this, only encrypted channels needs them
          if( fullChannelId == "") {
            //print("Could not find the given channel.");
            showChannelOption = false;
            break;
          }

          stdout.write("\nType message; or type 'x' to exit, or press <enter> to refresh: ");
          $tempUserInput = stdin.readLineSync(encoding: utf8);
          String messageToSend = $tempUserInput??"";
          print("got word: $messageToSend");

          if( messageToSend != "") {
            if( messageToSend == 'x') {
              showChannelOption = false;
            } else {
              int retval = 0;
              if( (retval = messageToSend.isChannelPageNumber(gMaxChannelPagesDisplayed)) != 0 ) {
                print('is channel page number: $retval');
                pageNum = retval;
              } else {

                // in case the program was invoked with --pubkey, then user can't send messages
                if( userPrivateKey == "") {
                    printWarning("Since no user private key has been supplied, posts/messages can't be sent. Invoke with --prikey \n");
                } else {
                  if( messageToSend.length >= 7 && messageToSend.substring(0, 7).compareTo("/reply ") == 0) {
                    List<String> tokens = messageToSend.split(' ');
                    if( tokens.length >= 3) {
                      String replyTo = tokens[1];
                      Channel? channel = node.getChannelFromId(node.channels, fullChannelId);
                      String actualMessage = messageToSend.substring(7);

                      if( messageToSend.indexOf(tokens[1]) + tokens[1].length < messageToSend.length) {
                        actualMessage = messageToSend.substring( messageToSend.indexOf(tokens[1]) + tokens[1].length + 1);
                      }

                      if( channel != null) {
                        await sendChannelReply(node, channel, replyTo, actualMessage, getPostKindFrom( channel.roomType));
                        pageNum = 1; // reset it 
                      }
                    }

                  } else {
                    // send message to the given room
                    Channel? channel = node.getChannelFromId(node.channels, fullChannelId);
                    if( channel != null) {
                      await sendChannelMessage(node, channel,  messageToSend, getPostKindFrom(channel.roomType));
                      pageNum = 1; // reset it 
                    }
                  }
                }
              }
            }
          } else {
            print("Refreshing...");
          }
          clearScreen();
          await processAnyIncomingEvents(node, false);
        } // end while showChannelOption
        break;

      case 2:
        clearScreen();
        printInColor("                                 All Public Channels ", gCommentColor);
        node.printChannelsOverview(node.channels, node.channels.length, selectorShowOnlyPublicChannel, node.allChildEventsMap, null);
        justShowedChannels = true;
        break;

      case 3:
        clearScreen();
        printInColor("                                 All Tag Channels", gCommentColor);
        node.printChannelsOverview(node.channels, node.channels.length, selectorShowOnlyTagChannel, node.allChildEventsMap, null);
        justShowedChannels = true;
        break;

      case 4:
        clearScreen();
        if( userPrivateKey == "") {
            printWarning("Since no user private key has been supplied, you cannot create channels or send any event. Invoke with --prikey \n");
            justShowedChannels = false;
            break;
        }
        print("Creating new channel. Kindly enter info about channel: \n");
        await createPublicChannel(node);
        clearScreen();
        justShowedChannels = false;
        // TODO put user in the newly created channel
        break;

      case 5:
        continueChatMenu = false;
        break;

      default:
        break;
    }
  }
  return;
}

// sends event creating a new public channel
Future<void> createEncryptedChannel(Store node) async {
  String channelName = getStringFromUser("Enter encrypted channel name: ");
  String channelAbout = getStringFromUser("Enter description for the new encrypted channel: ");
  String channelPic = getStringFromUser("Enter display picture if any: ", "https://placekitten.com/200/200");
  String content = "{\"name\": \"$channelName\", \"about\": \"$channelAbout\", \"picture\": \"$channelPic\"}";
  int    createdAt = DateTime.now().millisecondsSinceEpoch ~/1000;

  List<String> participants = [userPublicKey];
  String pTags = '';
  for( int i = 0; i < participants.length; i++) {
    if( i > 0) {
      pTags += ",";
    }
    pTags += '["p","${participants[i]}"]';
  }

  EventData eventData = EventData('id', userPublicKey, createdAt, 140, content, [], [], [], [], {}, );
  Event encryptedChannelCreateEvent = Event("EVENT", "id", eventData, [], "");
  String newEncryptedChannelId = await sendEventWithTags(node, encryptedChannelCreateEvent, pTags); // takes 400 ms
  clearScreen();
  print("Created new encrypted channel with id: $newEncryptedChannelId\n");

  String newPriKey = getRandomPrivKey();
  
  // Created and going to use new random privake key
  String channelPriKey = newPriKey, channelPubKey = myGetPublicKey(newPriKey);

  // now send password as direct message to yourself and to all the people you tagged
  String messageToSend = "App Encrypted Channels: inviting you to encrypted channel $newEncryptedChannelId encrypted using private public keys $channelPriKey $channelPubKey";
  for( int i = 0; i < participants.length; i++) {
    // send message to all ( including self which is in that list)
    await sendDirectMessage(node, participants[i], messageToSend, replyKind: gSecretMessageKind.toString());
  }

  await processAnyIncomingEvents(node, false); // get latest event, this takes 300 ms
}

// sends event creating a new public channel
Future<void> updateEncryptedChannel(Store node, String channelId, 
                                    String channelName, String channelAbout, String channelPic, String content, String tags, 
                                    Set<String> participants, Set<String> newParticipants) async {

  List<String> keys = getEncryptedChannelKeys(node.encryptedGroupInviteIds, node.allChildEventsMap, channelId);
  if( keys.length == 2) {
    String channelPriKey = keys[0], channelPubKey = keys[1];

    // now send password as direct message to yourself and to all the people you tagged
    String messageToSend = "App Encrypted Channels: inviting you to encrypted channel $channelId encrypted using private public keys $channelPriKey $channelPubKey";
    
    // send message to all new participants
    newParticipants.forEach((participant) async {
      await sendDirectMessage(node, participant, messageToSend, replyKind: gSecretMessageKind.toString());
    });

    int    createdAt = DateTime.now().millisecondsSinceEpoch ~/1000;
    EventData eventData = EventData('id', userPublicKey, createdAt, 141, content, [], [], [], [], {}, );
    Event channelUpdateEvent = Event("EVENT", "id", eventData, [], "");
    await sendEventWithTags(node, channelUpdateEvent, tags); // takes 400 ms

    await processAnyIncomingEvents(node, false); // get latest event, this takes 300 ms
  } else {
    printWarning("Could not find shared-secret keys for the channel. Could not update.");
  }
}

String encryptChannelMessage(Store node, String channelId, String messageToSend) {
  String encryptedMessage = '';

  List<String> keys = getEncryptedChannelKeys(node.encryptedGroupInviteIds, node.allChildEventsMap, channelId);
  if( keys.length != 2) {
    printWarning("Could not get channel secret for channel id: $channelId");
    return '';
  }

  String priKey = keys[0], pubKey = keys[1];
  encryptedMessage = myEncrypt(priKey, "02$pubKey", messageToSend);

  return encryptedMessage;
}

Future<void> addUsersToEncryptedChannel(Store node, String fullChannelId, Set<String> newPubKeys) async {
    // first check user is creator of channel
    Event? channelEvent = node.allChildEventsMap[fullChannelId]?.event;
    if( channelEvent != null) {
      if( channelEvent.eventData.pubkey == userPublicKey) {

        Channel? channel = node.getChannelFromId(node.encryptedChannels, fullChannelId);
        if( channel != null ) {

          Set<String> newParticipants = {};
          Set<String> participants = channel.participants;
          int numOldUsers = participants.length;

          // now send invite 
          List<String> toAdd = [];

          for(var newPubkey in newPubKeys) {
            if( newPubkey.length != 64) {

              printWarning("Invalid pubkey. The given pubkey should be 64 byte long. offending pubkey: $newPubkey");
              continue;
            }
            toAdd.add(newPubkey);
            newParticipants.add(newPubkey);
            participants.add(newPubkey);
          }
          
          String channelName = node.getChannelNameFromId(node.encryptedChannels, fullChannelId);
          String channelAbout = "";
          String channelPic = "https://placekitten.com/200/200";
          String content = channelEvent.eventData.content;

          String tags = '["e","$fullChannelId"]';
          for (var participant in participants) { 
            tags += ',["p","$participant"]';
          }

          int numNewUsers = participants.length;

          if( numNewUsers > numOldUsers) {
            print("\nSending kind 141 invites to new participants: $newParticipants");
            await updateEncryptedChannel(node, fullChannelId, channelName, channelAbout, channelPic, content, tags, participants, newParticipants);
          } else {
            printWarning("\nNote: No new users added. Kindly check whether the given user(s) aren't already members of the group, that pubkeys are valid etc");
          }
        }
      } else {
        printWarning("Not being the creator of this channel, you cannot add members to it.");
      }
    }
}

Future<void> sendInvitesForEncryptedChannel(Store node, String channelId, Set<String> newPubKeys) async {
    // first check user is creator of channel
    Event? channelEvent = node.allChildEventsMap[channelId]?.event;
    
    if( channelEvent != null) {
      if( channelEvent.eventData.pubkey == userPublicKey) {
    
        Channel? channel = node.getChannelFromId(node.encryptedChannels, channelId);
        if( channel != null ) {
          List<String> keys = getEncryptedChannelKeys(node.encryptedGroupInviteIds, node.allChildEventsMap, channelId);

          String channelPriKey = keys[0], channelPubKey = keys[1];

          // now send password as direct message to yourself and to all the people you tagged
          String messageToSend = "App Encrypted Channels: inviting you to encrypted channel $channelId encrypted using private public keys $channelPriKey $channelPubKey";
          
          // send message to all new participants
          newPubKeys.forEach((participant) async {
            await sendDirectMessage(node, participant, messageToSend, replyKind: gSecretMessageKind.toString());
          });
        }
      }
    }
}

Future<void> encryptedChannelMenuUI(Store node) async {
 
  bool continueChatMenu = true;
  
  bool justShowedChannels = false;
  while(continueChatMenu) {
    await processAnyIncomingEvents(node, false); // this takes 300 ms

    if( !justShowedChannels) {
      printInColor("                                  Encrypted Channels ", gCommentColor);
      node.printChannelsOverview(node.encryptedChannels, gNumRoomsShownByDefault, selectorShowAllRooms, node.allChildEventsMap, node.encryptedGroupInviteIds);
      justShowedChannels = true;
    }

    String menuInfo = """Encrypted Channel howto: Enter a channel by typing the first few unique letters of its pubkey or full name.
                         Once in a room/channel: add new participants by typing '/add <their 64 byte hex public key>' and pressing enter,
                         To reply to a message, type '/reply <first few letters of id of post to reply to> <your message>,
                         The channels updated latest are shown at bottom.
                         Type '/help' to see more info. When in a channel, press 'x' and then enter to exit. """;

    int option = showMenu([ 'Enter an encrypted channel',         // 1
                            'Show all encrypted channels',        // 2
                            'Create encrypted channel',           // 3
                            'E(x)it to main menu'],               // 4
                          "Encrypted Channels Menu",  // name of menu
                          menuInfo); 
    switch(option) {
      case 1:

        justShowedChannels = false;
        bool showChannelOption = true;
        stdout.write("\nType channel id or name, or their 1st few letters; or type 'x' to go to menu: ");
        String? $tempUserInput = stdin.readLineSync();
        String channelId = $tempUserInput??"";

        if( channelId == "x") {
          showChannelOption = false; 
        }
        bool firstIteration = true;
        int pageNum = 1;
        while(showChannelOption) {
          reAdjustAlignment();

          if( firstIteration) {
            clearScreen();
            firstIteration = false;
          }

          String fullChannelId = node.showChannel(node.encryptedChannels, channelId, node.allChildEventsMap, node.encryptedGroupInviteIds, node.encryptedChannels, pageNum);
          if( fullChannelId == "") {
            //print("Could not find the given channel.");
            showChannelOption = false;
            break;
          }

          stdout.write("\nType message; or type 'x' to exit, or press <enter> to refresh: ");
          $tempUserInput = stdin.readLineSync(encoding: utf8);
          String messageToSend = $tempUserInput??"";

          if( messageToSend != "") {
            if( messageToSend == 'x') {
              showChannelOption = false;
            } else {
              int retval = 0;
              if( (retval = messageToSend.isChannelPageNumber(gMaxChannelPagesDisplayed) ) != 0) {
                pageNum = retval;
              } else {

                // in case the program was invoked with --pubkey, then user can't send messages
                if( userPrivateKey == "") {
                    printWarning("Since no user private key has been supplied, posts/messages can't be sent. Invoke with --prikey");
                } else {
                  if( messageToSend.startsWith('/add ')) {
                    Set<String> newPubKeys = messageToSend.split(' ').sublist(1).toSet();
                    await addUsersToEncryptedChannel(node, fullChannelId, newPubKeys);
                    continue;
                  }

                  Channel? channel = node.getChannelFromId(node.encryptedChannels, fullChannelId);
                  if( channel == null) {
                    break;
                  }

                  switch( messageToSend.trim()) {
                  case '/reinvite all': 
                    clearScreen();
                    Set<String> participantPubkeys = channel.participants;
                    print("Sending the shared secret again to: $participantPubkeys");
                    await sendInvitesForEncryptedChannel(node, fullChannelId, participantPubkeys);
                    continue;  // get to next while loop to avoid clearscreen

                  case '/members':
                    clearScreen();
                    print("\nMembers names and pubkeys:\n");
                    printPubkeys(channel.participants );
                    print("");
                    continue;  // get to next while loop to avoid clearscreen
                    
                  case  '/help':
                    clearScreen();
                    print("Help commands available:");
                    print("""\n                                /members                     - print names/pubkeys of all members
                                /add <pubkey1> <pubkey2> ... - Space-separated pubkeys are taken as new user pubkeys, and they're added to group (admin only).
                                /reinvite all                - send secret password to all again (admin only)
                    """);
                    continue;  // get to next while loop to avoid clearscreen
                    
                  default:
                    // send message to the given room
                    if( messageToSend.length >= 7 && messageToSend.substring(0, 7).compareTo("/reply ") == 0) {
                      List<String> tokens = messageToSend.split(' ');
                      if( tokens.length >= 3) {
                        String replyTo = tokens[1];
                        String actualMessage = messageToSend.substring(7);

                        if( messageToSend.indexOf(tokens[1]) + tokens[1].length < messageToSend.length) {
                          actualMessage = messageToSend.substring( messageToSend.indexOf(tokens[1]) + tokens[1].length + 1);
                        }

                        String encryptedMessageToSend = encryptChannelMessage(node, fullChannelId, actualMessage);
                        if( encryptedMessageToSend != "") {
                          await sendChannelReply(node, channel, replyTo, encryptedMessageToSend, "142");
                          pageNum = 1; // reset it 
                        } else {
                          printWarning("\nCould not get send reply message because could not encrypt message.");
                        } 
                      }
                    } 
                    else {
                      String encryptedMessageToSend = encryptChannelMessage(node, fullChannelId, messageToSend);
                      if( encryptedMessageToSend != "") {
                        await sendChannelMessage(node, channel, encryptedMessageToSend, "142");
                        pageNum = 1; // reset it 
                      } else {
                        printWarning("\nCould not get send message because could not encrypt message.");
                      }
                    }
                  }// inner switch
                }
              }
            }
          } else {
            print("Refreshing...");
          }
          clearScreen();
          await processAnyIncomingEvents(node, false);
        } // end while showChennelOption ( showing each page)
        break;

      case 2:
        clearScreen();
        printInColor("                              All Encrypted Channels ", gCommentColor);
        node.printChannelsOverview(node.encryptedChannels, node.encryptedChannels.length, selectorShowAllRooms, node.allChildEventsMap, node.encryptedGroupInviteIds);
        justShowedChannels = true;
        break;

      case 3:
        clearScreen();
        if( userPrivateKey == "") {
            printWarning("Since no user private key has been supplied, you cannot create channels or send any event. Invoke with --prikey \n");
            justShowedChannels = false;
            break;
        }

        print("Creating new encrypted channel. Kindly enter info about channel: \n");
        await createEncryptedChannel(node);
        justShowedChannels = false;
        break;

      case 4:
        continueChatMenu = false;
        break;

      default:
        break;
    }
  }
  return;
}

Future<void> PrivateMenuUI(Store node) async {
  bool justShowedChannels = false;
  bool continueChatMenu = true;
  while(continueChatMenu) {
    await processAnyIncomingEvents(node, true); // this takes 300 ms

    if( !justShowedChannels) {
      printInColor("                                Direct Messages", gCommentColor);
      node.printDirectRoomsOverview(showAllRooms, gNumRoomsShownByDefault, node.allChildEventsMap);
      justShowedChannels = true;
    }

    String menuInfo = """Direct Message howto: To send a Direct Message to someone for the first time, enter their 64 byte hex pubkey into menu option #1.
                      To enter or continue a conversation seen in overview, enter the first few letters of the other person's name or of their pubkey.
                      Latest conversations are shown at bottom.""";
    int option = showMenu([ 
                            'Reply or Send a direct message',
                            'Show all direct rooms',
                            'E(x)it to main menu'],          // 3
                          "Direct Message Menu", // name of menu
                          menuInfo); 
    switch(option) {

      case 1:
        // in case the program was invoked with --pubkey, then user can't send messages
        if( userPrivateKey == "") {
            print("Since no private key has been supplied, messages and replies can't be sent. Invoke with --prikey \n");
            justShowedChannels = false;
            clearScreen();
            break;
        }

        bool showChannelOption = true;
        stdout.write("\nType user public key, or their name, or their 1st few letters; or type 'x' to cancel: ");
        String? $tempUserInput = stdin.readLineSync();
        String directRoomId = $tempUserInput??"";

        if( directRoomId == "x") {
          showChannelOption = false; 
        }
        int pageNum = 1;
        while(showChannelOption) {

          String fullChannelId = node.showDirectRoom(directRoomId, pageNum);
          if( fullChannelId == "") {
            printWarning("Could not find the given direct room.");
            showChannelOption = false;
            break;
          }

          stdout.write("\nType message; or type 'x' to exit, or press <enter> to refresh: ");
          $tempUserInput = stdin.readLineSync(encoding: utf8);
          String messageToSend = $tempUserInput??"";

          if( messageToSend != "") {
            if( messageToSend == 'x') {
              showChannelOption = false;
            } else {
              int retval = 0;
              if( (retval = messageToSend.isChannelPageNumber(gMaxChannelPagesDisplayed)) != 0 ) {
                pageNum = retval;
              } else {
                  // in case the program was invoked with --pubkey, then user can't send messages
                  if( userPrivateKey == "") {
                    printWarning("Since no user private key has been supplied, posts/messages can't be sent. Invoke with --prikey \n");
                  }
                  // send message to the given room
                  await sendDirectMessage(node, fullChannelId, messageToSend);
                  await processAnyIncomingEvents(node, false); // get latest message
                  
                  pageNum = 1; // reset it 
              }
            }
          } else {
            print("Refreshing...");
          }
          clearScreen();
          
        }
        justShowedChannels = false;
        break;

      case 2:
        clearScreen();
        printInColor("                                Direct Messages", gCommentColor);
        node.printDirectRoomsOverview(showAllRooms, node.directRooms.length, node.allChildEventsMap);
        justShowedChannels = true;
        break;

      case 3:
        continueChatMenu = false;
        break;

      default:
        break;
    }
  }
  return;
}

Future<void> socialMenuUi(Store node) async {
   
    clearScreen();

    await processAnyIncomingEvents(node); // this takes 300 ms

    bool socialMenuContinue = true;
    bool firstTime = true;
    while(socialMenuContinue) {

      if( !firstTime) {
        await processAnyIncomingEvents(node); // this takes 300 ms
        firstTime = false;
      }

      // the main menu
      int option = showMenu([
                             'Your Feed',         // 1
                             'Post/Reply/Like',   // 2
                             'Replies/Likes to you',// 3
                             'Your Posts',        // 4 
                             'Your Replies/Likes',//5
                             'Follows\' Posts/Replies/Likes',   // 6
                             'Search word(s) or event id',    // 7
                             'Follow new contact',            // 8
                             'Show user profile',             // 9
                             'Change # of hours printed', // 10
                             'E(x)it to main menu'], // 11
                             "Social Network Menu");
      
      switch(option) {
        case 1:
          bool selectorTrees_followActionsNoNotifications (Tree t) => t.treeSelectorUserPostAndLike(getFollows( userPublicKey).union(gDefaultFollows).union({userPublicKey}), enableNotifications: false);
          node.printStoreTrees(0, DateTime.now().subtract(Duration(hours:gHoursDefaultPrint)), selectorTrees_followActionsNoNotifications);
          await processAnyIncomingEvents(node, true);
          break;

        case 2:
          // in case the program was invoked with --pubkey, then user can't send messages
          if( userPrivateKey == "") {
              printWarning("Since no user private key has been supplied, posts/messages can't be sent. Invoke with --prikey \n");
              break;
          }
          stdout.write("Type comment to post/reply (type '+' to send a like): ");
          String? $contentVar = stdin.readLineSync();
          String content = $contentVar??"";
          if( content == "") {
            clearScreen();  
            break;
          }

          stdout.write("\nType id of event to reply to (leave blank to make a new post; type x to cancel): ");
          String? $replyToVar = stdin.readLineSync();
          String replyToId = $replyToVar??"";
          print("got id");
          if( replyToId == "x") {
            print("Cancelling post/reply.");
            break;
          }
          String replyKind = "1";
          if( content == "+") {
            print("Sending a like to given post.");
            replyKind = "7";
          } else if( content == "!") {
            print("Hiding the given post.");
            replyKind = "7";
          }

          await sendReplyPostLike(node, replyToId, replyKind, content);
          clearScreen();

          await processAnyIncomingEvents(node, true);
          break;

        case 3:
          clearScreen();
          bool selectorTrees_userNotifications (Tree t) => t.treeSelectorotificationsFor({userPublicKey});
          int notificationHours = gHoursDefaultPrint>24? gHoursDefaultPrint: 24; // minimum 24
          List<int> numPrinted = node.printStoreTrees(0, DateTime.now().subtract(Duration(hours:notificationHours)), selectorTrees_userNotifications);
          if( numPrinted[2] > 0) {
            print("Showed ${numPrinted[2]} replies/likes that were made to your posts.\n");
          } else {
            print("No replies or likes.");
          }

          await processAnyIncomingEvents(node, true);
          break;
        case 4:
          clearScreen();
          List<int> numPrinted = node.printStoreTrees(0, DateTime.now().subtract(Duration(hours:gHoursDefaultPrint)), selectorTrees_selfPosts);
          if( numPrinted[0] > 0) {
            print("Showed ${numPrinted[0]} posts made by you in last $gHoursDefaultPrint hours.\n");
          } else {
            print("No posts made by you in last $gHoursDefaultPrint hours.");
          }

          await processAnyIncomingEvents(node, true);
          break;
        case 5:
          clearScreen();
          bool selectorTrees_userActions (Tree t) => t.treeSelectorUserPostAndLike({userPublicKey});
          List<int> numPrinted = node.printStoreTrees(0, DateTime.now().subtract(Duration(hours:gHoursDefaultPrint)), selectorTrees_userActions);
          if( numPrinted[0] > 0) {
            print("Showed ${numPrinted[0]} thread where you replied or liked in in last $gHoursDefaultPrint hours.\n");
          } else {
            print("No replies/likes made by you in last $gHoursDefaultPrint hours.");
          }
          await processAnyIncomingEvents(node, true);
          break;

        case 6:
          clearScreen();
          bool selectorTrees_followActionsWithNotifications (Tree t) => t.treeSelectorUserPostAndLike(getFollows( userPublicKey), enableNotifications: true);
          List<int> numPrinted = node.printStoreTrees(0, DateTime.now().subtract(Duration(hours:gHoursDefaultPrint)), selectorTrees_followActionsWithNotifications);
          if( numPrinted[0] > 0) {
            print("Showed ${numPrinted[0]} threads where your follows participated.\n");
          } else {
            print("No threads to show where your follows participated in last $gHoursDefaultPrint hours.");
          }
          await processAnyIncomingEvents(node, true);
          break;

        case 7: // search word or event id
          clearScreen();
          stdout.write("Enter word(s) to search: ");
          String? $tempWords = stdin.readLineSync();
          String words = $tempWords??"";
          if( words != "") {
            bool onlyWords (Tree t) => t.treeSelectorHasWords(words.toLowerCase());
            List<int> numPrinted = node.printStoreTrees(0, DateTime.now().subtract(Duration(hours:gHoursDefaultPrint)), onlyWords, gMaxInteger); // search for last default hours only
            if( numPrinted[0] == 0) {
              print("\nNot found in the last $gHoursDefaultPrint hours. Try increasing the number of days printed, from social network options to search further back into history.\n");
            }
          } else {
            printWarning("Blank word entered. Try again.");
          }

          break;

  /*
        case 700: // display contact list
          String authorName = getAuthorName(userPublicKey);
          List<Contact>? contactList = gKindONames[userPublicKey]?.latestContactEvent?.eventData.contactList;
          if( contactList != null) {
            print("\nHere is the contact list for user $userPublicKey ($authorName), which has ${contactList.length} profiles in it:\n");
            contactList.forEach((Contact contact) => stdout.write("${getAuthorName(contact.id)}, "));
            print("");
          }
          break;
  */
        case 8: // follow new contact
          // in case the program was invoked with --pubkey, then user can't send messages
          if( userPrivateKey == "") {
              printWarning("Since no user private key has been supplied, posts/messages can't be sent. Invoke with --prikey");
              break;
          }

          clearScreen();
          stdout.write("Enter username or first few letters of user's public key( or full public key): ");
          String? $tempUserName = stdin.readLineSync();
          String userName = $tempUserName??"";
          if( userName != "") {
            Set<String> pubkey = getPublicKeyFromName(userName); 
            
            printPubkeyResult(pubkey);
            
            if( pubkey.length > 1) {
              if( pubkey.length > 1) {
                printWarning("Got multiple users with the same name. Try again, and type a more unique name or id-prefix");
              }
            } else {
              if (pubkey.isEmpty && userName.length != 64) {
                  printWarning("Could not find the user with that id or username. You can try again by providing the full 64 byte long hex public key.");
              } 
              else {
                if( pubkey.isEmpty) {
                  printWarning("Could not find the user with that id or username in internal store/list. However, since the given id is 64 bytes long, taking that as hex public key and adding them as contact.");
                  pubkey.add(userName);
                }

                String pk = pubkey.first;

                // get this users latest contact list event ( kind 3 event)
                Event? contactEvent = getContactEvent(userPublicKey);
                
                if( contactEvent != null) {
                  Event newContactEvent = contactEvent;

                  bool alreadyContact = false;
                  for(int i = 0; i < newContactEvent.eventData.contactList.length; i++) {
                    if( newContactEvent.eventData.contactList[i].contactPubkey == pubkey.first) {
                      alreadyContact = true;
                      break;
                    }
                  }
                  if( !alreadyContact) {
                    print('Sending new contact event');
                    Contact newContact = Contact(pk, defaultServerUrl);
                    newContactEvent.eventData.contactList.add(newContact);
                    getUserEvents(gListRelayUrls, pk, gLimitPerSubscription, getSecondsDaysAgo(gLimitFollowPosts));
                    sendEvent(node, newContactEvent);
                  } else {
                    print("The contact already exists in the contact list. Republishing the old contact list.");
                    getUserEvents(gListRelayUrls, pk, gLimitPerSubscription, getSecondsDaysAgo(gLimitFollowPosts));
                    sendEvent(node, contactEvent);
                  }
                } else {
                    // TODO fix the send event functions by streamlining them

                    if(confirmFirstContact()) {
                      print('Sending first contact event');
                      String newId = "", newPubkey = userPublicKey,  newContent = "";
                      int newKind = 3;
                      List<List<String>> newEtags = [];
                      List<String> newPtags = [pk];
                      List<List<String>> newTags = [[]];
                      Set<String> newNewLikes = {};
                      int newCreatedAt = DateTime.now().millisecondsSinceEpoch ~/ 1000; 
                      List<Contact> newContactList = [ Contact(pk, defaultServerUrl) ];

                      EventData newEventData = EventData(newId, newPubkey, newCreatedAt, newKind, newContent, newEtags, newPtags, newContactList, newTags, newNewLikes,);
                      Event newEvent = Event( "EVENT", newId, newEventData,  [], "");
                      getUserEvents(gListRelayUrls, pk, gLimitPerSubscription, getSecondsDaysAgo(gLimitFollowPosts));
                      sendEvent(node, newEvent);
                    }
                }
              }
            }
          }
          await processAnyIncomingEvents(node, true);
          break;


        case 9:
          clearScreen();
          stdout.write("Printing profile of a user; type username or first few letters of user's public key( or full public key): ");
          String? $tempUserName = stdin.readLineSync();
          String userName = $tempUserName??"";
          stdout.write( "user name: $userName");
          if( userName != "") {
            Set<String> pubkey = getPublicKeyFromName(userName); 

            printPubkeyResult(pubkey);

            if( pubkey.length > 1) {
              if( pubkey.length > 1) {
                printWarning("Got multiple users with the same name. Try again, and/or type a more unique name or their full public keys.");
              }
            } else {
              if (pubkey.isEmpty ) {
                printWarning("Could not find the user with that id or username.");
              } 
              else {
                printProfile(node, pubkey.first);
              }
            }
          }
          break;

        case 10: // change number of days printed
          clearScreen();
          stdout.write("Enter number of hours for which you want to see latest posts: ");
          String? $tempHoursDefaultPrint = stdin.readLineSync();
          String strHoursDefaultPrint  = $tempHoursDefaultPrint??"";

          try {
            gHoursDefaultPrint =  int.parse(strHoursDefaultPrint);
            print("Changed number of hours printed to $gHoursDefaultPrint");
          } on FormatException catch (e) {
            printWarning("Invalid input. Kindly try again."); 
            if( gDebug > 0) print(" ${e.message}"); 
            continue;
          } on Exception catch (e) {
            printWarning("Invalid input. Kindly try again."); 
            if( gDebug > 0) print(" $e"); 
            continue;
          }    
          break;

        case 11:
          default:
            socialMenuContinue = false;
        } // end menu switch
    } // end while
} // end socialMenuUi()

void directRoomNotifications(Store node, [int x = 0, int y = 0]) {
  //print("In showAllNotifications. x = $x y = $y");

  List<int> numPrinted = [x, 0, y];


  // print direct messages and count the number printed
  bool showNotifications (ScrollableMessages room) => room.selectorNotifications();
  int numDirectRoomsPrinted = node.printDirectRoomsOverview( showNotifications, 100, node.allChildEventsMap);
  
  if( numDirectRoomsPrinted > 0) {
    print("\n");
  }

  int totalNotifications = numPrinted[2] + numDirectRoomsPrinted;
  if( totalNotifications > 0) {
    print("Showed $totalNotifications notifications.\n");
  }

  //print("printed $totalNotifications notifications")  ;
}

Future<void> mainMenuUi(Store node) async {
   
    var n;

    /* TODO
    ProcessSignal.sigint.watch().listen((signal) {
      print(" caught ${n} of 3");
      clearScreen();
      print("\nExiting. Writing file $gEventsFilename. ");
      if( gEventsFilename != "") {
        node.writeEventsToFile(gEventsFilename);
      }
      exit(0);
    }); */


    clearScreen();

    //Show only notifications
    await processAnyIncomingEvents(node); // this takes 300 ms

    bool mainMenuContinue = true;
    bool firstTime = true;
    while(mainMenuContinue) {

      if( !firstTime) {
        await processAnyIncomingEvents(node); // this takes 300 ms
      }
      firstTime = false;

      // the main menu
      int option = showMenu(['Global Feed',      // 1 
                             'Social Network',   // 2
                             'Public Channels',  // 3
                             'Encrypted Channels',// 4
                             'Private Messages', // 5
                             'Other Options',    // 6
                             'E(x)it Application'],            // 7
                             "Main Menu");
      
      switch(option) {
        case 1:
          node.printStoreTrees(0, DateTime.now().subtract(Duration(hours:gHoursDefaultPrint)), selectorTrees_all);
          break;

        case 2:
          clearScreen();
          await socialMenuUi(node);
          clearScreen();
          break;

        case 3:
          clearScreen();
          await channelMenuUI(node);
          clearScreen();
          break;

        case 4:
          clearScreen();
          await encryptedChannelMenuUI(node);
          clearScreen();
          break;

        case 5:
          clearScreen();
          await PrivateMenuUI(node);
          clearScreen();
          break;

        case 6:
          clearScreen();
          await otherOptionsMenuUi(node);
          clearScreen();
          break;

        case 7:
        default:
          mainMenuContinue = false;
          String authorName = getAuthorName(userPublicKey);
          clearScreen();
          print("\nFinished Nostr session for user: $authorName ($userPublicKey)");
          if( gEventsFilename != "") {
            await node.writeEventsToFile(gEventsFilename);
          }
          exit(0);
      } // end menu switch
    } // end while
} // end mainMenuUi()


Future<void> programExit([String message= ""]) async {
    if( gEventsFilename != "") {
      await gStore?.writeEventsToFile(gEventsFilename);
    }
  print("In programexit");
    exit(0);
}

