import 'dart:io';
import 'dart:convert';
import 'package:nostr_console/event_ds.dart';
import 'package:nostr_console/settings.dart';

typedef fTreeSelector = bool Function(Tree a);

bool selectAll(Tree t) {
  return true;
}

class ScrollableMessages {
  String       topHeader;
  List<String> messageIds;

  ScrollableMessages(this.topHeader, this.messageIds);

  void printOnePage(Map<String, Tree> tempChildEventsMap, [int page = 1])  {
    if( page < 1) {
      if( gDebug > 0) log.info("In ScrollableMessages::printOnepage  got page = $page");
      page = 1;
    }

    printCenteredHeadline(topHeader);

    int i = 0, startFrom = 0, endAt = messageIds.length;
    int numPages = 1;

    if( messageIds.length > gNumChannelMessagesToShow ) {
      endAt = messageIds.length - (page - 1) * gNumChannelMessagesToShow;
      if( endAt < gNumChannelMessagesToShow) endAt = gNumChannelMessagesToShow;
      startFrom = endAt - gNumChannelMessagesToShow;
      numPages = (messageIds.length ~/ gNumChannelMessagesToShow) + 1;
      if( page > numPages) {
        page = numPages;
      }
    }
    if( gDebug > 0) print("StartFrom $startFrom  endAt $endAt  numPages $numPages room.messageIds.length = ${messageIds.length}");
    for( i = startFrom; i < endAt; i++) {
      String eId = messageIds[i];
      Event? e = tempChildEventsMap[eId]?.event;
      if( e!= null) {
        print(e.eventData.getStrForChannel(0));
        //print("");
      }
    }

    if( messageIds.length > gNumChannelMessagesToShow) {
      print("\n");
      printDepth(0);
      stdout.write("${gNotificationColor}Displayed page number ${page} (out of total $numPages pages, where 1st is the latest 'page').\n");
      printDepth(0);
      stdout.write("To see older pages, enter numbers from 1-${numPages}.${gColorEndMarker}\n\n");
    }
  }
}

class ChatRoom extends ScrollableMessages {
  String       chatRoomId; // id of the kind 40 start event
  String       internalChatRoomName; 
  String       about;
  String       picture;

  ChatRoom(this.chatRoomId, this.internalChatRoomName, this.about, this.picture, List<String> messageIds) : 
            super (  internalChatRoomName.isEmpty? chatRoomId: internalChatRoomName + "( " + chatRoomId + " )" , 
                     messageIds);

  String get chatRoomName {
    return internalChatRoomName;
  }

  void set chatRoomName(String newName){
    internalChatRoomName = newName;
    super.topHeader = newName + " (${chatRoomId.substring(0,6)})";
  }
 }

class DirectMessageRoom extends ScrollableMessages{
  String       otherPubkey; // id of user this DM is happening

  DirectMessageRoom(this.otherPubkey, List<String> messageIds):
            super ( "${getAuthorName(otherPubkey)} ($otherPubkey)", messageIds) {
              //print ("Created direct room with otherPubkey = $otherPubkey");
            }
 }

class Tree {
  Event          event;                   // is dummy for very top level tree. Holds an event otherwise.
  List<Tree>     children;            // only has kind 1 events
  Store?         store;

  Tree(this.event, this.children,this.store );
  factory Tree.withoutStore(Event e, List<Tree> c) {
    return Tree(e, c, null);
  }

  void setStore(Store s) {
    store = s;
  }

  /***********************************************************************************************************************************/
  /* The main print tree function. Calls the reeSelector() for every node and prints it( and its children), only if it returns true. 
   */
  int printTree(int depth, DateTime newerThan) {
    int numPrinted = 0;

    //if( event.eventData.pubkey != gDummyAccountPubkey) { // don't print dummy events
      event.printEvent(depth);
      numPrinted++;
    //}

    bool leftShifted = false;
    for( int i = 0; i < children.length; i++) {

      stdout.write("\n");  
      printDepth(depth+1);
      stdout.write("|\n");

      // if the thread becomes too 'deep' then reset its depth, so that its 
      // children will not be displayed too much on the right, but are shifted
      // left by about <leftShiftThreadsBy> places
      if( depth > maxDepthAllowed) {
        depth = maxDepthAllowed - leftShiftThreadsBy;
        printDepth(depth+1);
        stdout.write("<${getNumDashes((leftShiftThreadsBy + 1) * gSpacesPerDepth - 1)}+\n");        
        leftShifted = true;
      }

      numPrinted += children[i].printTree(depth+1, newerThan);
    }

    if( leftShifted) {
      stdout.write("\n");
      printDepth(depth+1);
      print(">");
    }

    return numPrinted;
  }

  // returns the time of the most recent comment
  int getMostRecentTime(int mostRecentTime) {
    if( children.isEmpty)   {
      return event.eventData.createdAt;
    }
    if( event.eventData.createdAt > mostRecentTime) {
      mostRecentTime = event.eventData.createdAt;
    }

    int mostRecentIndex = -1;
    for( int i = 0; i < children.length; i++) {
      int mostRecentChild = children[i].getMostRecentTime(mostRecentTime);
      if( mostRecentTime <= mostRecentChild) {
        mostRecentTime = mostRecentChild;
        mostRecentIndex = i;
      }
    }
    if( mostRecentIndex == -1) { 
      Tree? top = store?.getTopTree(this);
      // typically this should not happen. child nodes/events can't be older than parents 
      return (top?.event.eventData.createdAt)??mostRecentTime;
    } else {
      return mostRecentTime;
    }
  }

  // returns true if the treee or its children has a reply or like for the user with public key pk; and notification flags are set for such events
  bool hasRepliesAndLikes(String pk) {
    //print("----- pk = $pk");
    bool hasReaction = false;
    bool childMatches = false;

    if( event.eventData.pubkey == pk &&  gReactions.containsKey(event.eventData.id)) {
      List<List<String>>? reactions = gReactions[event.eventData.id];
      if( reactions  != null) {
        if( reactions.length > 0) {
          //print("has reactions");
          reactions.forEach((reaction) {  
            // dont add notificatoin for self reaction
            Event? reactorEvent = store?.allChildEventsMap[reaction[0]]?.event;
            if( reactorEvent != null) {
              if( reactorEvent.eventData.pubkey != pk){ // ignore self likes 
                event.eventData.newLikes.add(reaction[0]);
                hasReaction = true;
              }
            }
          });
        }
      }
    }

    if( event.eventData.pubkey == pk && children.length > 0) {
      for( int i = 0; i < children.length; i++ ) {
        children.forEach((child) {  
          // if child is someone else then set notifications and flag, means there are replies to this event 
          childMatches = child.event.eventData.isNotification =  ((child.event.eventData.pubkey != pk)? true: false) ; 
        }); 
      }
    }

    for( int i = 0; i < children.length; i++ ) {
      if( children[i].hasRepliesAndLikes(pk)) {
        childMatches = true;
      }
    }

    if( hasReaction || childMatches) {
      //print("returning true");
      return true;
    }
    return false;
  } 


  // returns true if the treee or its children has a post or like by user; and notification flags are set for such events
  bool hasUserPostAndLike(String pubkey) {
    bool hasReacted = false;

    if( gReactions.containsKey(event.eventData.id))  {
      List<List<String>>? reactions = gReactions[event.eventData.id];
      if( reactions  != null) {
        for( int i = 0; i < reactions.length; i++) {
          if( reactions[i][0] == pubkey) {
            event.eventData.newLikes.add(pubkey);
            hasReacted = true;
            break;
          }
        }
      }
    }

    bool childMatches = false;
    for( int i = 0; i < children.length; i++ ) {
      if( children[i].hasUserPostAndLike(pubkey)) {
        childMatches = true;
      }
    }
    if( event.eventData.pubkey == pubkey) {
      event.eventData.isNotification = true;
      return true;
    }
    if( hasReacted || childMatches) {
      return true;
    }
    return false;
  } 

  // returns true if the given words exists in it or its children
  bool hasWords(String word) {
    if( event.eventData.content.length > 2000) { // ignore if content is too large, takes lot of time
      return false;
    }

    bool childMatches = false;
    for( int i = 0; i < children.length; i++ ) {
      // ignore too large comments
      if( children[i].event.eventData.content.length > 2000) {
        continue;
      }

      if( children[i].hasWords(word)) {
        childMatches = true;
      }
    }

    if( event.eventData.content.toLowerCase().contains(word) || event.eventData.id == word ) {
      event.eventData.isNotification = true;
      return true;
    }
    if( childMatches) {
      return true;
    }
    return false;
  } 

  // returns true if the event or any of its children were made from the given client, and they are marked for notification
  bool fromClientSelector(String clientName) {
    //if(gDebug > 0) print("In tree selector hasWords: this id = ${e.eventData.id} word = $word");

    bool byClient = false;
    List<List<String>> tags = event.eventData.tags;
    for( int i = 0; i < tags.length; i++) {
      if( tags[i].length < 2) {
        continue;
      }
      if( tags[i][0] == "client" && tags[i][1].contains(clientName)) {
        event.eventData.isNotification = true;
        byClient = true;
        break;
      }
    }

    bool childMatch = false;
    for( int i = 0; i < children.length; i++ ) {
      if( children[i].fromClientSelector(clientName)) {
        childMatch = true;
      }
    }
    if( byClient || childMatch) {
      //print("SOME matched $clientName ");
      return true;
    }
    //print("none matched $clientName ");

    return false;
  } 

  // counts all valid events in the tree: ignores the dummy nodes that are added for events which aren't yet known
  int count() {
    int totalCount = 0;

    if( event.eventData.pubkey != gDummyAccountPubkey) { // don't count dummy events
        totalCount = 1;
    }

    for(int i = 0; i < children.length; i++) {
      totalCount += children[i].count(); // then add all the children
    }

    return totalCount;
  }


} // end Tree

/***********************************************************************************************************************************/
/*  
 * The actual tree holds only kind 1 events, or only posts
 * This Store class holds events too in its map, and in its chatRooms structure
 */
class Store {
  List<Tree>        topPosts;            // only has kind 1 events

  Map<String, Tree>  allChildEventsMap;   // has events of kind typesInEventMap
  List<String>       eventsWithoutParent;
  Map<String, ChatRoom> chatRooms = {};
  Map<String, DirectMessageRoom> directRooms = {};

  Store(this.topPosts, this.allChildEventsMap, this.eventsWithoutParent, this.chatRooms, this.directRooms) {
    allChildEventsMap.forEach((eventId, tree) {
      if( tree.store == null) {
        tree.setStore(this);
      }
    });
  }

  static const Set<int>   typesInEventMap = {0, 1, 3, 4, 5, 7, 40, 42}; // 0 meta, 1 post, 3 follows list, 7 reactions

  static void handleChannelEvents( Map<String, ChatRoom> rooms, Map<String, Tree> tempChildEventsMap, Event ce) {
      String eId = ce.eventData.id;
      int    eKind = ce.eventData.kind;

      switch(eKind) {
      case 42:
      {
        if( gCheckEventId == ce.eventData.id)          print("In handleChannelEvents: processing $gCheckEventId ");
        String chatRoomId = ce.eventData.getChatRoomId();
        if( chatRoomId != "") { // sometimes people may forget to give e tags or give wrong tags like #e
          if( rooms.containsKey(chatRoomId)) {
            if( gDebug > 0) print("chat room already exists = $chatRoomId adding event to it" );
    
            if( gCheckEventId == ce.eventData.id) print("Adding new message $eId to a chat room $chatRoomId. ");
   
            addMessageToChannel(chatRoomId, eId, tempChildEventsMap, rooms);
    
          } else {
            if( gCheckEventId == ce.eventData.id) print("Adding new message $eId to NEW chat room $chatRoomId. ");
            rooms[chatRoomId] = ChatRoom(chatRoomId, "", "", "", []);
            addMessageToChannel(chatRoomId, eId, tempChildEventsMap, rooms);
          }
        }
      }
      break;
      case 40:
       {
        //numKind40Events++;
        String chatRoomId = eId;
        try {
          dynamic json = jsonDecode(ce.eventData.content);
          if( rooms.containsKey(chatRoomId)) {
            if( rooms[chatRoomId]?.chatRoomName == "") {
              //if( gDebug > 0) print('Added room name = ${json['name']} for $chatRoomId' );
              rooms[chatRoomId]?.chatRoomName = json['name'];
            }
          } else {
            String roomName = "", roomAbout = "";
            if(  json.containsKey('name') ) {
              roomName = json['name'];
            }
            
            if( json.containsKey('about')) {
              roomAbout = json['about'];
            }
            List<String> emptyMessageList = [];
            ChatRoom room = ChatRoom(chatRoomId, roomName, roomAbout, "", emptyMessageList);
            rooms[chatRoomId] = room;
            //if( gDebug > 0) print("Added new chat room $chatRoomId with name ${json['name']} .");
          }
        } on Exception catch(e) {
          if( gDebug > 0) print("In From Event. Event type 40. Json Decode error for event id ${ce.eventData.id}. error = $e");
        }
      }
        break;
      default:
        break;  
      } // end switch
  }

  static void handleDirectMessages( Map<String, DirectMessageRoom> directRooms, Map<String, Tree> tempChildEventsMap, Event ce) {
      String eId = ce.eventData.id;
      int    eKind = ce.eventData.kind;

      if( !isValidDirectMessage(ce.eventData)) {
        return;
      }

      switch(eKind) {
      case 4:
      {
        String directRoomId = getDirectRoomId(ce.eventData);
        if( directRoomId != "") {
          if( directRooms.containsKey(directRoomId)) {
            if( gDebug > 0) print("Adding new message ${ce.eventData.id} to a direct room $directRoomId sender pubkey = ${ce.eventData.pubkey}. ");
            addMessageToDirectRoom(directRoomId, eId, tempChildEventsMap, directRooms);
          } else {
            List<String> temp = [];
            temp.add(eId);
            DirectMessageRoom newDirectRoom= DirectMessageRoom(directRoomId,  temp);
            directRooms[directRoomId] = newDirectRoom;
            if( gDebug > 0) print("Adding new message ${ce.eventData.id} to NEW direct room $directRoomId.  sender pubkey = ${ce.eventData.pubkey}.");
          }
        } else {
          if( gDebug > 0) print("Could not get chat room id for event ${ce.eventData.id}  sender pubkey = ${ce.eventData.pubkey}.");
        }
      }
      break;
      default:
        break;  
      } // end switch
  }
 
  /***********************************************************************************************************************************/
  // @method create top level Tree from events. 
  // first create a map. then process each element in the map by adding it to its parent ( if its a child tree)
  factory Store.fromEvents(Set<Event> events) {
    if( events.isEmpty) {
      return Store( [], {}, [], {}, {});
    }

    // create a map tempChildEventsMap from list of events, key is eventId and value is event itself
    Map<String, Tree> tempChildEventsMap = {};
    events.forEach((event) { 
      // only add in map those kinds that are supported or supposed to be added ( 0 1 3 7 40)
      if( typesInEventMap.contains(event.eventData.kind)) {
        tempChildEventsMap[event.eventData.id] = Tree.withoutStore( event, []); 
      }
    });

    processDeleteEvents(tempChildEventsMap); // handle returned values perhaps later

    processReactions(events, tempChildEventsMap);


    // once tempChildEventsMap has been created, create connections between them so we get a tree structure from all these events.
    List<Tree>  topLevelTrees = [];// this will become the children of the main top node. These are events without parents, which are printed at top.
    List<String> tempWithoutParent = [];
    Map<String, ChatRoom> rooms = {};
    Map<String, DirectMessageRoom> tempDirectRooms= {};

    int numEventsNotPosts = 0; // just for debugging info
    int numKind40Events   = 0;
    int numKind42Events   = 0;
    if( gDebug > 0) print("In Tree from Events: after adding all required events of type ${typesInEventMap} to tempChildEventsMap map, its size = ${tempChildEventsMap.length} ");

    tempChildEventsMap.forEach((newEventId, tree) {
      int eKind = tree.event.eventData.kind;
      if( eKind == 42 || eKind == 40) {
        handleChannelEvents(rooms, tempChildEventsMap, tree.event);
      }

      if( eKind == 4) {
        handleDirectMessages(tempDirectRooms, tempChildEventsMap, tree.event);
      }

      // only posts, of kind 1, are added to the main tree structure
      if( eKind != 1) {
        numEventsNotPosts++;
        return;
      }

      if(tree.event.eventData.eTags.isNotEmpty ) {
        // is not a parent, find its parent and then add this element to that parent Tree
        String parentId = tree.event.eventData.getParent();
        if( tree.event.eventData.id == gCheckEventId) {
          if(gDebug >= 0) print("In Tree FromEvents: got id: $gCheckEventId");
        }

        if(tempChildEventsMap.containsKey( parentId)) {
          if( tempChildEventsMap[parentId]?.event.eventData.kind != 1) { // since parent can only be a kind 1 event
            if( gDebug > 1) log.info("In Tree.fromEvents: Not adding: got a kind 1 event whose parent is not a type 1 post: $newEventId . parent kind: ${tempChildEventsMap[parentId]?.event.eventData.kind}");
            return;
          }
          tempChildEventsMap[parentId]?.children.add(tree); 
        } else {
           // in case where the parent of the new event is not in the pool of all events, 
           // then we create a dummy event and put it at top ( or make this a top event?) TODO handle so that this can be replied to, and is fetched
           Event dummy = Event("","",  EventData(parentId,gDummyAccountPubkey, tree.event.eventData.createdAt, 1, "Unknown parent event", [], [], [], [[]], {}), [""], "[json]");

           Tree dummyTopNode = Tree.withoutStore(dummy, []);
           dummyTopNode.children.add(tree);
           tempWithoutParent.add(tree.event.eventData.id); 
          
           // add the dummy evnets to top level trees, so that their real children get printed too with them
           // so no post is missed by reader
           topLevelTrees.add(dummyTopNode);
        }
      }
    }); // going over tempChildEventsMap and adding children to their parent's .children list

    // add parent trees as top level child trees of this tree
    for( var tree in tempChildEventsMap.values) {
        if( tree.event.eventData.kind == 1 &&  tree.event.eventData.eTags.isEmpty) {  // only posts which are parents
            topLevelTrees.add(tree);
        }
    }

    if(gDebug != 0) print("In Tree FromEvents: number of events in map which are not kind 1 = ${numEventsNotPosts}");
    if(gDebug != 0) print("In Tree FromEvents: number of events in map of kind 40 = ${numKind40Events}");
    if(gDebug != 0) print("In Tree FromEvents: number of events in map of kind 42 = ${numKind42Events}");
    if(gDebug != 0) print("In Tree FromEvents: number of events without parent in fromEvents = ${tempWithoutParent.length}");

    // create a dummy top level tree and then create the main Tree object
    return Store( topLevelTrees, tempChildEventsMap, tempWithoutParent, rooms, tempDirectRooms);
  } // end fromEvents()

   /***********************************************************************************************************************************/
   /* @processIncomingEvent inserts the relevant events into the tree and otherwise processes likes, delete events etc.
    *                        returns the id of the relevant ones actually inserted so that they can be printed as notifications. 
    */
  Set<String> processIncomingEvent(Set<Event> newEventsToProcess) {
    if( gDebug > 0) log.info("In insertEvetnts: allChildEventsMap size = ${allChildEventsMap.length}, called for ${newEventsToProcess.length} NEW events");

    Set<String> newEventIdsSet = {};

    // add the event to the main event store thats allChildEventsMap
    newEventsToProcess.forEach((newEvent) { 
      
      if( allChildEventsMap.containsKey(newEvent.eventData.id)) {// don't process if the event is already present in the map
        return;
      }

      // handle reaction events and return if we could not find the reacted to. Continue otherwise to add this to notification set newEventIdsSet
      if( newEvent.eventData.kind == 7) {
        //print("going to call processRreactin");
        if( processReaction(newEvent, allChildEventsMap) == "") {
          if(gDebug > 0) print("In insertEvents: For new reaction ${newEvent.eventData.id} could not find reactedTo or reaction was already present by this reactor");
          return;
        }
      }

      // handle delete events. return if its not handled for some reason ( like deleted event not found)
      if( newEvent.eventData.kind == 5) {
        processDeleteEvent(allChildEventsMap, newEvent);
        if(gDebug > 0) print("In insertEvents: For new deleteion event ${newEvent.eventData.id} could not process it.");
        return;
      }

      if( newEvent.eventData.kind == 4) {
        if( !isValidDirectMessage(newEvent.eventData)) { // direct message not relevant to user are ignored; also otherwise validates the message that it has one p tag
          return;
        }
      }

      if( newEvent.eventData.kind == 0) {
        processKind0Event(newEvent);
      }

      // only kind 0, 1, 3, 4, 5( delete), 7, 40, 42 events are added to map, return otherwise
      if( !typesInEventMap.contains(newEvent.eventData.kind) ) {
        return;
      }

      // expand mentions ( and translate if flag is set) and then add event to main event map
      newEvent.eventData.translateAndExpandMentions(); // this also handles dm decryption for kind 4 messages, for kind 1 will do translation/expansion; 

      // add them to the main store of the Tree object
      allChildEventsMap[newEvent.eventData.id] = Tree(newEvent, [], this);

      // add to new-notification list only if this is a recent event ( because relays may send old events, and we dont want to highlight stale messages)
      if( newEvent.eventData.createdAt > getSecondsDaysAgo(gDontHighlightEventsOlderThan)) {
        newEventIdsSet.add(newEvent.eventData.id);
      }
    });
    
    // now go over the newly inserted event, and add its to the tree for kind 1 events, add 42 events to channels. rest ( such as kind 0, kind 3, kind 7) are ignored.
    newEventIdsSet.forEach((newId) {
      Tree? newTree = allChildEventsMap[newId];
      if( newTree != null) {  // this should return true because we just inserted this event in the allEvents in block above

        switch(newTree.event.eventData.kind) {
          case 1:
            // only kind 1 events are added to the overall tree structure
            if( newTree.event.eventData.eTags.isEmpty) {
                // if its a new parent event, then add it to the main top parents ( this.children)
                topPosts.add(newTree);
            } else {
                // if it has a parent , then add the newTree as the parent's child
                String parentId = newTree.event.eventData.getParent();
                if( allChildEventsMap.containsKey(parentId)) {
                  allChildEventsMap[parentId]?.children.add(newTree);
                } else {
                  // create top unknown parent and then add it
                  Event dummy = Event("","",  EventData(parentId, gDummyAccountPubkey, newTree.event.eventData.createdAt, 1, "Unknown parent event", [], [], [], [[]], {}), [""], "[json]");
                  Tree dummyTopNode = Tree.withoutStore(dummy, []);
                  dummyTopNode.children.add(newTree);
                  topPosts.add(dummyTopNode);
                }
            }
            break;
          case 4:
            // add kind 4 direct chat message event to its direct massage room
            String directRoomId = getDirectRoomId(newTree.event.eventData);
            //print("in insert events: got directRoomId = ${directRoomId}");

            if( directRoomId != "") {
              if( directRooms.containsKey(directRoomId)) {
                if( gDebug > 0) print("added event to direct room $directRoomId in insert event");
                addMessageToDirectRoom(directRoomId, newTree.event.eventData.id, allChildEventsMap, directRooms);
                newTree.event.eventData.isNotification = true; // highlight it too in next printing
                //print("   in from event: added it to a direct room");
                break;
              }
            }

            List<String> temp = [];
            temp.add(newTree.event.eventData.id);
            directRooms[directRoomId] = DirectMessageRoom(directRoomId, temp); // TODO sort it 

            break;

          case 42:
            newTree.event.eventData.isNotification = true; // highlight it too in next printing
            // add 42 chat message event id to its chat room
            String channelId = newTree.event.eventData.getParent();
            if( channelId != "") {
              if( chatRooms.containsKey(channelId)) {
                if( gDebug > 0) print("added event to chat room in insert event");
                addMessageToChannel(channelId, newTree.event.eventData.id, allChildEventsMap, chatRooms); // adds in order
                break;
              } else {
                chatRooms[channelId] = ChatRoom(channelId, "", "", "", []);
                addMessageToChannel(channelId, newTree.event.eventData.id, allChildEventsMap, chatRooms);
              }
            } 
            break;
          default: 
            break;
        }
      }
    });
    int totalTreeSize = 0;
    topPosts.forEach((element) {totalTreeSize += element.count();});
    if(gDebug > 0) print("In end of insertEvents: allChildEventsMap size = ${allChildEventsMap.length}; mainTree count = $totalTreeSize");
    if(gDebug > 0)  print("Returning ${newEventIdsSet.length} new notification-type events, which are ${newEventIdsSet.length < 10 ? newEventIdsSet: " <had more than 10 elements>"} ");
    return newEventIdsSet;
  } // end insertEvents()

  /***********************************************************************************************************************************/
  /*
   * @printNotifications Add the given events to the Tree, and print the events as notifications
   *                     It should be ensured that these are only kind 1 events
   */
  void printNotifications(Set<String> newEventIdsSet, String userName) {
    if( gDebug > 0) print("Info: in printNotifications: num new evetns = ${newEventIdsSet.length}");

    String strToWrite = "Notifications: ";
    int countNotificationEvents = 0;
    for( var newEventId in newEventIdsSet) {
      int k = (allChildEventsMap[newEventId]?.event.eventData.kind??-1);
      if( k == 7 || k == 1 || k == 42 || k == 40) {
        countNotificationEvents++;
      }

      if(  allChildEventsMap.containsKey(newEventId)) {
        if( gDebug > 0) print( "id = ${ (allChildEventsMap[newEventId]?.event.eventData.id??-1)}");
      } else {
        if( gDebug > 0) print( "Info: could not find event id in map."); // this wont later be processed
      }

    }

    if(gDebug > 0) print("Info: In printNotifications: newEventsId = $newEventIdsSet count17 = $countNotificationEvents");
    
    if( countNotificationEvents == 0) {
      strToWrite += "No new replies/posts.\n";
      stdout.write("${getNumDashes(strToWrite.length - 1)}\n$strToWrite");
      stdout.write("Total posts  : ${count()}\n");
      stdout.write("Signed in as : $userName\n\n");
      return;
    }
    // TODO call count() less
    strToWrite += "Number of new replies/posts = ${newEventIdsSet.length}\n";
    stdout.write("${getNumDashes(strToWrite.length -1 )}\n$strToWrite");
    stdout.write("Total posts  : ${count()}\n");
    stdout.write("Signed in as : $userName\n");
    stdout.write("\nHere are the threads with new replies or new likes: \n");
    
    List<Tree> topNotificationTree = []; // collect all top tress to display in this list. only unique tress will be displayed
    newEventIdsSet.forEach((eventID) { 
      
      Tree ?t = allChildEventsMap[eventID];
      if( t == null) {
        // ignore if not in Tree. Should ideally not happen. TODO write warning otherwise
        if( gDebug > 0) print("In printNotifications: Could not find event $eventID in tree");
        return;
      } else {
        switch(t.event.eventData.kind) {
          case 1:
            t.event.eventData.isNotification = true;
            Tree topTree = getTopTree(t);
            topNotificationTree.add(topTree);
            break;
          case 7:
            Event event = t.event;
            if(gDebug > 0) ("Got notification of type 7");
            String reactorId  = event.eventData.pubkey;
            int    lastEIndex = event.eventData.eTags.length - 1;
            String reactedTo  = event.eventData.eTags[lastEIndex];
            Event? reactedToEvent = allChildEventsMap[reactedTo]?.event;
            if( reactedToEvent != null) {
              Tree? reactedToTree = allChildEventsMap[reactedTo];
              if( reactedToTree != null) {
                if(event.eventData.content == "+" ) {
                  reactedToTree.event.eventData.newLikes.add( reactorId);
                  Tree topTree = getTopTree(reactedToTree);
                  topNotificationTree.add(topTree);
                } else if(event.eventData.content == "!" ) {
                  reactedToTree.event.eventData.isHidden = true;
                }
              } else {
                if(gDebug > 0) print("Could not find reactedTo tree");
              }
            } else {
              if(gDebug > 0) print("Could not find reactedTo event");
            }
            break;
          default:
            if(gDebug > 0) print("got an event thats not 1 or 7(reaction). its kind = ${t.event.eventData.kind} count17 = $countNotificationEvents");
            break;
        }
      }
    });

    // remove duplicate top trees
    Set ids = {};
    topNotificationTree.retainWhere((t) => ids.add(t.event.eventData.id));
    
    topNotificationTree.forEach( (t) { 
      t.printTree(0, DateTime(0)); 
      print("\n");
    });
    print("\n");
  }

   /***********************************************************************************************************************************/
  /* The main print tree function. Calls the reeSelector() for every node and prints it( and its children), only if it returns true. 
   */
  int printTree(int depth, DateTime newerThan, fTreeSelector treeSelector) {

    int numPrinted = 0;

    depth = depth - 1;
    topPosts.sort(sortTreeNewestReply); // sorting done only for top most threads. Lower threads aren't sorted so save cpu etc TODO improve top sorting

    for( int i = 0; i < topPosts.length; i++) {

      // continue if this children isn't going to get printed anyway; selector is only called for top most tree
      if( treeSelector(topPosts[i]) == false) {
        continue;
      } 

      // for top Store, only print the thread that are newer than the given parameter
      int newestChildTime = topPosts[i].getMostRecentTime(0);
      DateTime dTime = DateTime.fromMillisecondsSinceEpoch(newestChildTime *1000);
      if( dTime.compareTo(newerThan) < 0) {
        continue;
      }
      stdout.write("\n");  
      for( int i = 0; i < gapBetweenTopTrees; i++ )  { 
        stdout.write("\n"); 
      }
      
      String topPostLine = getDepthSpaces(depth+1);
      topPostLine += ("+\n");
      stdout.write(topPostLine);

      numPrinted += topPosts[i].printTree(depth+1, newerThan);
    }

    print("\n\nTotal posts/replies printed: $numPrinted for last $gNumLastDays days");
    return numPrinted;
  }
 
  /**
   * @printAllChennelsInfo Print one line information about all channels, which are type 40 events ( class ChatRoom)
   */
  void printAllChannelsInfo() {
    print("\n\nDirect messages inbox:");
    printUnderlined("      Channel Name                Num of Messages            Latest Message           ");
    chatRooms.forEach((key, value) {
      String name = "direct room name";
      if( value.chatRoomName == "") {
        name = value.chatRoomId.substring(0, 6);
      } else {
        name = "${value.chatRoomName} ( ${value.chatRoomId.substring(0, 6)})";
      }

      int numMessages = value.messageIds.length;
      stdout.write("${name} ${getNumSpaces(32-name.length)}          $numMessages${getNumSpaces(12- numMessages.toString().length)}"); 
      List<String> messageIds = value.messageIds;
      for( int i = messageIds.length - 1; i >= 0; i++) {
        if( allChildEventsMap.containsKey(messageIds[i])) {
          Event? e = allChildEventsMap[messageIds[i]]?.event;
          if( e!= null) {
            stdout.write("${e.eventData.getAsLine()}");
            break; // print only one event, the latest one
          }
        }
      }
      print("");
    });
  }

  /**
   * @printAllChennelsInfo Print one line information about all channels, which are type 40 events ( class ChatRoom)
   */
  void printDirectRoomInfo() {
    print("\n\nDirect messages inbox:");
    printUnderlined(" From                                    Num of Messages          Latest Message           ");
    directRooms.forEach((key, value) {
      String name = getAuthorName(key, 4);

      int numMessages = value.messageIds.length;
      stdout.write("${name} ${getNumSpaces(32-name.length)}          $numMessages${getNumSpaces(12- numMessages.toString().length)}"); 

      // print latest event in one line
      List<String> messageIds = value.messageIds;
      for( int i = messageIds.length - 1; i >= 0; i++) {
        if( allChildEventsMap.containsKey(messageIds[i])) {
          Event? e = allChildEventsMap[messageIds[i]]?.event;
          if( e!= null) {
            String line = e.eventData.getAsLine();
            stdout.write(line);
            break; // print only one event, the latest one
          }
        }
      }
      print("");
    });
  }

  // shows the given directRoomId, where directRoomId is prefix-id or pubkey of the other user. returns full id of other user.
  String showDirectRoom(String directRoomId, [int page = 1]) {
    //print("In show DirectRoom to show with id: $directRoomId");
    if( directRoomId.length > 64) { // TODO revisit  cause if name is > 64 should not return
      return "";
    }
    Set<String> lookedUpName = {};

    // TODO improve lookup logic. 
    directRooms.forEach((roomId, directRoom) {
      //print("looking up $directRoomId in  $roomId ${directRoom.otherPubkey}");
      if( directRoomId == roomId) {
        lookedUpName.add(roomId);
      }

      //print("directRoom.otherPubkey = ${directRoom.otherPubkey} len = ${directRoom.otherPubkey.length}");
      if( directRoom.otherPubkey.substring(0, directRoomId.length) == directRoomId){
        lookedUpName.add(roomId);
      }

      if( getAuthorName( directRoom.otherPubkey) == directRoomId){
        lookedUpName.add(roomId);
      }
    });

   
    if( lookedUpName.length == 1) {
      DirectMessageRoom? room = directRooms[lookedUpName.first];
      if( room != null) {
        printDirectMessageRoom(room, page);
        return lookedUpName.first;
      } else {
        if( isValidPubkey(lookedUpName.first)) {
          print("Could not find a conversation or room with the given id. Creating one with ${lookedUpName.first}");
          createDirectRoom(directRoomId);
          return directRoomId;
        }
      }
    } else {
      if( lookedUpName.length > 0)
      print("Got more than one public id for the name given, which are: ${lookedUpName.length}");
      else {
        if( isValidPubkey(directRoomId)) {
          print("Could not find a conversation or room with the given id. Creating one with $directRoomId");
          createDirectRoom(directRoomId);
          return directRoomId;
        } 
      }
      return "";
    }
    return "";
  }

  void createDirectRoom(String directRoomId) {
    if( !directRooms.containsKey(directRoomId))
      directRooms[directRoomId] = DirectMessageRoom(directRoomId, []); 
  }

  void printChannel(ChatRoom room, [int page = 1])  {
    if( page < 1) {
      if( gDebug > 0) log.info("In printChannel got page = $page");
      page = 1;
    }

    room.printOnePage(allChildEventsMap, page);
  }

  void printDirectMessageRoom(DirectMessageRoom directRoom, [int page = 1])  {
    if( page < 1) {
      if( gDebug > 0) log.info("In printChannel got page = $page");
      page = 1;
    }

    directRoom.printOnePage(allChildEventsMap, page);
  }


  // shows the given channelId, where channelId is prefix-id or channel name as mentioned in room.name. returns full id of channel.
  // looks for channelId in id first, then in names. 
  String showChannel(String channelId, [int page = 1]) {
    if( channelId.length > 64 ) {
      return "";
    }
    
    Set<String> fullChannelId = {};
    for( String key in chatRooms.keys) {
      if( key.substring(0, channelId.length) == channelId ) {
        fullChannelId.add(key);
      }
    }

    if(fullChannelId.length != 1) {
      for( String key in chatRooms.keys) {
          ChatRoom? room = chatRooms[key];
          if( room != null) {
            if( room.chatRoomName.length < channelId.length) {
              continue;
            }
            if( gDebug > 0) print("room = ${room.chatRoomName} channelId = $channelId");
            if( room.chatRoomName.substring(0, channelId.length) == channelId ) {
              fullChannelId.add(key);
            }
          }
      } // end for
    }

    if( fullChannelId.length == 1) {
      ChatRoom? room = chatRooms[fullChannelId.first];
      if( room != null) {
        printChannel(room, page);
      }
      return fullChannelId.first;
    } else {
      if( fullChannelId.length == 0) {
        print("Could not find the channel.");
      }
      else {
        print("Found more than 1 channel: $fullChannelId");
      }
    }

    return "";
  }

  // Write the tree's events to file as one event's json per line
  Future<void> writeEventsToFile(String filename) async {
    if( gDebug > 0) print("opening $filename to write to.");
    try {
      final File file         = File(filename);
      
      if( gOverWriteFile) {
        await  file.writeAsString("", mode: FileMode.write).then( (file) => file);
      }

      //await  file.writeAsString("", mode: FileMode.append).then( (file) => file);
      int        eventCounter = 0;
      String     nLinesStr    = "";
      int        countPosts   = 0;

      const int  numLinesTogether = 100; // number of lines to write in one write call
      int        linesWritten = 0;
      for( var tree in allChildEventsMap.values) {

        if( tree.event.eventData.isDeleted) { // dont write those deleted
          continue; 
        }

        if( gOverWriteFile == false) {
          if( tree.event.readFromFile) { // ignore those already in file; only the new ones are writen/appended to file
            continue;
          }
        }

        // only write if its not too old
        if( gDontWriteOldEvents) {
          if( tree.event.eventData.createdAt <  getSecondsDaysAgo(gDontSaveBeforeDays)) {
            continue;
          }
        }

        if( gDummyAccountPubkey == tree.event.eventData.pubkey) {
          continue; // dont write dummy events
        }

        //print("writing event ");
        //tree.event.printEvent(0); print("");
        String line = "${tree.event.originalJson}\n";
        nLinesStr += line;
        eventCounter++;
        if( tree.event.eventData.kind == 1) {
          countPosts++;
        }

        if( eventCounter % numLinesTogether == 0) {
          await  file.writeAsString(nLinesStr, mode: FileMode.append).then( (file) => file);
          nLinesStr = "";
          linesWritten += numLinesTogether;
        }
      } // end for

      if(  eventCounter > linesWritten) {
        //print("writing..");
        await  file.writeAsString(nLinesStr, mode: FileMode.append).then( (file) => file);
        nLinesStr = "";
      }

      if(gDebug > 0) log.info("finished writing eventCounter = ${eventCounter}.");
      print("Appended $eventCounter new events to file \"$gEventsFilename\" of which ${countPosts} are posts.");
    } on Exception catch (e) {
        print("Could not open file $filename.");
        if( gDebug > 0) print("Could not open file: $e");
    }      
    
    return;
  }

  /*
   * @getTagsFromEvent Searches for all events, and creates a json of e-tag type which can be sent with event
   *                   Also adds 'client' tag with application name.
   * @parameter replyToId First few letters of an event id for which reply is being made
   */
  String getTagStr(String replyToId, String clientName) {
    clientName = (clientName == "")? "nostr_console": clientName; // in case its empty 
    if( replyToId.isEmpty) {
      return '["client","$clientName"]';
    }

    String strTags = "";

    // find the latest event with the given id; needs to be done because we allow user to refer to events with as few as 3 or so first letters
    // and only the event that's latest is considered as the intended recipient ( this is not perfect, but easy UI)
    int latestEventTime = 0;
    String latestEventId = "";
    for(  String k in allChildEventsMap.keys) {
      if( k.length >= replyToId.length && k.substring(0, replyToId.length) == replyToId) {
        // ignore future events TODO

        if( ( allChildEventsMap[k]?.event.eventData.createdAt ?? 0) > latestEventTime ) {
          latestEventTime = allChildEventsMap[k]?.event.eventData.createdAt ?? 0;
          latestEventId = k;
        }
      }
    }

    // in case we are given valid length id, but we can't find the event in our internal db, then we just send the reply to given id
    if( latestEventId.isEmpty && replyToId.length == 64) {
      latestEventId = replyToId;  
    }
    if( latestEventId.isEmpty && replyToId.length != 64 && replyToId.length != 0) {
      return "";
    }

    // found the id of event we are replying to
    if( latestEventId.isNotEmpty) {
      String? pTagPubkey = allChildEventsMap[latestEventId]?.event.eventData.pubkey;
      if( pTagPubkey != null) {
        strTags += '["p","$pTagPubkey"],';
      }
      String relay = getRelayOfUser(userPublicKey, pTagPubkey??"");
      relay = (relay == "")? defaultServerUrl: relay;
      String rootEventId = "";

      // nip 10: first e tag should be the id of the top/parent event. 2nd ( or last) e tag should be id of the event being replied to.
      Tree? t = allChildEventsMap[latestEventId];
      if( t != null) {
        Tree topTree = getTopTree(t);
        rootEventId = topTree.event.eventData.id;
        if( rootEventId != latestEventId) { // if the reply is to a top/parent event, then only one e tag is sufficient
          strTags +=  '["e","$rootEventId"],';
        }
      }
      strTags +=  '["e","$latestEventId","$relay"],';
    }

    strTags += '["client","$clientName"]' ;
    return strTags;
  }

  // for any tree node, returns its top most parent
  Tree getTopTree(Tree tree) {
    while( true) {
      Tree? parent =  allChildEventsMap[ tree.event.eventData.getParent()];
      if( parent != null) {
        tree = parent;
      } else {
        break;
      }
    }
    return tree;
  }

  // get followers of given pubkey 
  List<String> getFollowers(String pubkey) {
    if( gDebug > 0) print("Finding followrs for $pubkey");
    List<String> followers = [];

    gKindONames.forEach((otherPubkey, userInfo) { 
        List<Contact>? contactList = userInfo.latestContactEvent?.eventData.contactList;
        if( contactList != null ) {
          for(int i = 0; i < contactList.length; i ++) {
            if( contactList[i].id == pubkey) {
              followers.add(otherPubkey);
              return;
            }
          }
        }
    });

    return followers;
  }

  // finds all your followers, and then finds which of them follow the otherPubkey
  void printSocialDistance(String otherPubkey, String otherName) {
    String otherName = getAuthorName(otherPubkey);

    Event? contactEvent = getContactEvent(userPublicKey);
    bool isFollow = false;
    int  numSecond = 0; // number of your follows who follow the other

    int numContacts =  0;
    if( contactEvent != null) {
      List<Contact> contacts = contactEvent.eventData.contactList;
      numContacts = contacts.length;
      for(int i = 0; i < contacts.length; i ++) {
        // check if you follow the other account
        if( contacts[i].id == otherPubkey) {
          isFollow = true;
        }
        // count the number of your contacts who know or follow the other account
        List<Contact> followContactList = [];
        Event? followContactEvent = getContactEvent(contacts[i].id);
        if( followContactEvent != null) {
          followContactList = followContactEvent.eventData.contactList;
          for(int j = 0; j < followContactList.length; j++) {
            if( followContactList[j].id == otherPubkey) {
              numSecond++;
              break;
            }
          }
        }
      }// end for loop through users contacts
      if( isFollow) {
        print("* You follow $otherName ");
      } else {
        print("* You don't follow $otherName");
      }
      print("* Of the $numContacts people you follow, $numSecond follow $otherName");

    } // end if contact event was found
  }

  int count() {
    int totalEvents = 0;
    for(int i = 0; i < topPosts.length; i++) {
      totalEvents += topPosts[i].count(); // calling tree's count.
    }
    return totalEvents;
  }

  static List<String> processDeleteEvent(Map<String, Tree> tempChildEventsMap, Event deleterEvent) {
    List<String> deletedEventIds = [];
    if( deleterEvent.eventData.kind == 5) {
      deleterEvent.eventData.tags.forEach((tag) { 
        if( tag.length < 2) {
          return;
        }
        if( tag[0] == "e") {
          String deletedEventId = tag[1];
          // look up that event and ensure its kind 1 etc, and then mark it deleted.
          Event? deletedEvent = tempChildEventsMap[deletedEventId]?.event;
          if( deletedEvent != null) {
            if( deletedEvent.eventData.kind == 1 && deletedEvent.eventData.pubkey == deleterEvent.eventData.pubkey) {
              deletedEvent.eventData.isDeleted = true;
              deletedEvent.eventData.content = gDeletedEventMessage;
              deletedEvent.eventData.evaluatedContent = "";
              EventData ed = deletedEvent.eventData;
              deletedEvent.originalJson = '["EVENT","none",{"id":${ed.id},"pubkey":${ed.pubkey},"createdAt":${ed.createdAt},"kind":1,"tags":[],"sig":"invalid","comment":"deleted"}]';
              deletedEventIds.add( deletedEvent.eventData.id);
            }          
          }
        }
      });
    } // end if
    return deletedEventIds;
  } // end processDeleteEvent

  static List<String> processDeleteEvents(Map<String, Tree> tempChildEventsMap) {
    List<String> deletedEventIds = [];
    tempChildEventsMap.forEach((key, tree) {
      Event deleterEvent = tree.event;
      if( deleterEvent.eventData.kind == 5) {
          List<String> tempIds = processDeleteEvent(tempChildEventsMap, deleterEvent);
          tempIds.forEach((tempId) { deletedEventIds.add(tempId); });
      }
    });
    return deletedEventIds;
  } // end processDeleteEvents

  Set<String> getEventEidFromPrefix(String eventId) {
    if( eventId.length > 64) {
      return {};
    }
    
    Set<String> foundEventIds = {};
    for(  String k in allChildEventsMap.keys) {
      if( k.length >= eventId.length && k.substring(0, eventId.length) == eventId) {
        foundEventIds.add(k);
      }
    }

    return foundEventIds;
  }

  // for the given reaction event of kind 7, will update the global gReactions appropriately, returns 
  // the reactedTo event's id, blank if invalid reaction etc
  static String processReaction(Event event, Map<String, Tree> tempChildEventsMap) {

    if(  gDebug > 0 && event.eventData.id == "e8a8a1f526af1023ba85ab3874d2310871e034eb8a0bcb3c289be671065ad03e")
        print("in processReaction: 0 got reaction e8a8a1f526af1023ba85ab3874d2310871e034eb8a0bcb3c289be671065ad03e");

    List<String> validReactionList = ["+", "!"]; // TODO support opposite reactions 
    List<String> opppositeReactions = ['-', "~"];

    if( event.eventData.kind == 7 
      && event.eventData.eTags.isNotEmpty) {

      if(gDebug > 1) ("Got event of type 7"); // this can be + or !, which means 'hide' event for me
      String reactorPubkey  = event.eventData.pubkey;
      String comment    = event.eventData.content;
      int    lastEIndex = event.eventData.eTags.length - 1;
      String reactedTo  = event.eventData.eTags[lastEIndex];

      if( gDebug > 0 && event.eventData.id == "e8a8a1f526af1023ba85ab3874d2310871e034eb8a0bcb3c289be671065ad03e")
         print("in processReaction: 1 got reaction e8a8a1f526af1023ba85ab3874d2310871e034eb8a0bcb3c289be671065ad03e");

      if( !validReactionList.any((element) => element == comment)) {
        return "";
      }

      // check if the reaction already exists by this user
      if( gReactions.containsKey(reactedTo)) {
        for( int i = 0; i < ((gReactions[reactedTo]?.length)??0); i++) {
          List<String> oldReaction = (gReactions[reactedTo]?[i])??[];
          if( oldReaction.length == 2) {
            //valid reaction
            if(oldReaction[0] == reactorPubkey && oldReaction[1] == comment) {
              
              return ""; // reaction by this user already exists so return
            }
          }
        }
        List<String> temp = [reactorPubkey, comment];
        gReactions[reactedTo]?.add(temp);
      } else {
        // first reaction to this event, create the entry in global map
        List<List<String>> newReactorList = [];
        List<String> temp = [reactorPubkey, comment];
        newReactorList.add(temp);
        gReactions[reactedTo] = newReactorList;
      }
      // set isHidden for reactedTo if it exists in map

      if(  gDebug > 0 && event.eventData.id == "e8a8a1f526af1023ba85ab3874d2310871e034eb8a0bcb3c289be671065ad03e")
         print("in processReaction: 2 got reaction e8a8a1f526af1023ba85ab3874d2310871e034eb8a0bcb3c289be671065ad03e");

      if( comment == "!" &&  event.eventData.pubkey == userPublicKey) {
        tempChildEventsMap[reactedTo]?.event.eventData.isHidden = true;
      }
      return reactedTo;
    } else {
      // case where its not a kind 7 event, or we can't find the reactedTo event due to absense of e tag.
    }

    return "";
  }

  // will go over the list of events, and update the global gReactions appropriately
  static void processReactions(Set<Event> events, Map<String, Tree> tempChildEventsMap) {
    //print("in processReactions");    
    for (Event event in events) {
      processReaction(event, tempChildEventsMap);
    }
    return;
  }

} //================================================================================================================================ end Store

void addMessageToChannel(String channelId, String messageId, Map<String, Tree> tempChildEventsMap, var chatRooms) {
  int newEventTime = (tempChildEventsMap[messageId]?.event.eventData.createdAt??0);
  if( gCheckEventId == messageId) {
    print("In addMessageToChannel: newEventTime= $newEventTime");
    //gDebug = 1;
  }

  if( chatRooms.containsKey(channelId)) {
    ChatRoom? room = chatRooms[channelId];
    if( room != null ) {
      if( room.messageIds.isEmpty) {
        if(gDebug> 0 ||  gCheckEventId == messageId) print("room is empty. adding new message and returning. ");
        room.messageIds.add(messageId);
        return;
      }
      
      if(gDebug> 0 ||  gCheckEventId == messageId) print("room has ${room.messageIds.length} messages already. adding new one to it. ");

      for(int i = 0; i < room.messageIds.length; i++) {
        int eventTime = (tempChildEventsMap[room.messageIds[i]]?.event.eventData.createdAt??0);
        if( newEventTime < eventTime) {
          // shift current i and rest one to the right, and put event Time here
          if(gDebug> 0 ||  gCheckEventId == messageId ) 
              print("In addMessageToChannel: inserted event $messageId at position $i to channel ${room.chatRoomId} ");
          room.messageIds.insert(i, messageId);
          return;
        }
      }
      if(gDebug> 0 ||  gCheckEventId == messageId) print("In addMessageToChannel: added to channel ${room.chatRoomId} at end");

      // insert at end
      room.messageIds.add(messageId);
      return;
    } else {
      print("In addMessageToChannel: could not find room");
    }
  } else {
    print("In addMessageToChannel: could not find channel id");
  }
  print("In addMessageToChannel: returning without inserting message");
}

void addMessageToDirectRoom(String directRoomId, String messageId, Map<String, Tree> tempChildEventsMap, var directRooms) {
  int newEventTime = (tempChildEventsMap[messageId]?.event.eventData.createdAt??0);

  if( directRooms.containsKey(directRoomId)) {
    DirectMessageRoom? room = directRooms[directRoomId];
    if( room != null ) {
      if( room.messageIds.isEmpty) {
        room.messageIds.add(messageId);
        return;
      }
      
      if(gDebug> 0) print("direct room has ${room.messageIds.length} messages already. adding new one to it. ");

      for(int i = 0; i < room.messageIds.length; i++) {
        int eventTime = (tempChildEventsMap[room.messageIds[i]]?.event.eventData.createdAt??0);
        if( newEventTime < eventTime) {
          // shift current i and rest one to the right, and put event Time here
          if(gDebug> 0) print("In addMessageToChannel: inserted in middle to channel ${room.otherPubkey} ");
          room.messageIds.insert(i, messageId);
          return;
        }
      }
      if(gDebug> 0) print("In addMessageToChannel: added to channel ${room.otherPubkey} ");

      // insert at end
      room.messageIds.add(messageId);
      return;
    } else {
      print("In addMessageToChannel: could not find room");
    }
  } else {
    print("In addMessageToChannel: could not find channel id");
  }
  print("In addMessageToChannel: returning without inserting message");
}

int ascendingTimeTree(Tree a, Tree b) {
  if(a.event.eventData.createdAt < b.event.eventData.createdAt) {
    return -1;
  } else {
    if( a.event.eventData.createdAt == b.event.eventData.createdAt) {
      return 0;
    }
  }
  return 1;
}

// sorter function that looks at the latest event in the whole tree including the/its children
int sortTreeNewestReply(Tree a, Tree b) {
  int aMostRecent = a.getMostRecentTime(0);
  int bMostRecent = b.getMostRecentTime(0);

  if(aMostRecent < bMostRecent) {
    return -1;
  } else {
    if( aMostRecent == bMostRecent) {
      return 0;
    } else {
        return 1;
    }
  }
}

/*
 * @function getTree Creates a Tree out of these received List of events. 
 *             Will remove duplicate events( which should not ideally exists because we have a set), 
 *             populate global names, process reactions, remove bots, translate, and then create main tree
 */
Store getTree(Set<Event> events) {
    if( events.isEmpty) {
      if(gDebug > 0) log.info("Warning: In printEventsAsTree: events length = 0");
      return Store([], {}, [], {}, {});
    }

    // remove all events other than kind 0 (meta data), 1(posts replies likes), 3 (contact list), 7(reactions), 40 and 42 (chat rooms)
    events.removeWhere( (event) => !Store.typesInEventMap.contains(event.eventData.kind));  

    // process kind 0 events about metadata 
    int totalKind0Processed = 0, notProcessed = 0;
    events.forEach( (event) =>  processKind0Event(event)? totalKind0Processed++: notProcessed++);
    if( gDebug > 0) print("In getTree: totalKind0Processed = $totalKind0Processed  notProcessed = $notProcessed gKindONames.length = ${gKindONames.length}"); 

    // process kind 3 events which is contact list. Update global info about the user (with meta data) 
    int totalKind3Processed = 0, notProcessed3 = 0;
    events.forEach( (event) =>  processKind3Event(event)? totalKind3Processed++: notProcessed3++);
    if( gDebug > 0) print("In getTree: totalKind3Processed = $totalKind3Processed  notProcessed = $notProcessed3 gKindONames.length = ${gKindONames.length}"); 

    // process kind 7 events or reactions
    //processReactions(events);

    // remove bot events
    events.removeWhere( (event) => gBots.contains(event.eventData.pubkey));

    // remove duplicate events
    Set ids = {};
    events.retainWhere((event) => ids.add(event.eventData.id));

    // translate and expand mentions for all
    events.forEach( (event) => event.eventData.translateAndExpandMentions());

    if( gDebug > 0) print("In getTree: after removing unwanted kind, number of events remaining: ${events.length}");

    // create tree from events
    Store node = Store.fromEvents(events);

    if(gDebug != 0) print("total number of posts/replies in main tree = ${node.count()}");
    return node;
}

//   returns the id of event since only one p is expected in an event ( for future: sort all participants by id; then create a large string with them together, thats the unique id for now)
String getDirectRoomId(EventData eventData) {

  List<String> participantIds = [];
  eventData.tags.forEach((tag) { 
    if( tag.length < 2) 
      return;

      if( tag[0] == 'p') {
        participantIds.add(tag[1]);
      }
  });

  participantIds.sort();
  String uniqueId = "";
  participantIds.forEach((element) {uniqueId += element;}); // TODO ensure its only one thats added s

  // send the other persons pubkey as identifier 
  if( eventData.pubkey == userPublicKey) {
    return uniqueId;
  } else { 
    return eventData.pubkey;
  }
}
