content: /*
   * Copyright (C) 2014-2015 Actor LLC. <https://actor.im>
   */
  
  package im.actor.core.modules.groups;
  
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  
  import im.actor.core.api.ApiAdminSettings;
  import im.actor.core.api.ApiGroupOutPeer;
  import im.actor.core.api.ApiGroupType;
  import im.actor.core.api.ApiMember;
  import im.actor.core.api.ApiOutPeer;
  import im.actor.core.api.ApiPeerType;
  import im.actor.core.api.ApiUserOutPeer;
  import im.actor.core.api.rpc.RequestCreateGroup;
  import im.actor.core.api.rpc.RequestDeleteGroup;
  import im.actor.core.api.rpc.RequestDismissUserAdmin;
  import im.actor.core.api.rpc.RequestEditGroupAbout;
  import im.actor.core.api.rpc.RequestEditGroupShortName;
  import im.actor.core.api.rpc.RequestEditGroupTitle;
  import im.actor.core.api.rpc.RequestEditGroupTopic;
  import im.actor.core.api.rpc.RequestGetGroupInviteUrl;
  import im.actor.core.api.rpc.RequestGetIntegrationToken;
  import im.actor.core.api.rpc.RequestInviteUser;
  import im.actor.core.api.rpc.RequestJoinGroup;
  import im.actor.core.api.rpc.RequestJoinGroupByPeer;
  import im.actor.core.api.rpc.RequestKickUser;
  import im.actor.core.api.rpc.RequestLeaveAndDelete;
  import im.actor.core.api.rpc.RequestLeaveGroup;
  import im.actor.core.api.rpc.RequestLoadAdminSettings;
  import im.actor.core.api.rpc.RequestLoadMembers;
  import im.actor.core.api.rpc.RequestMakeUserAdminObsolete;
  import im.actor.core.api.rpc.RequestRevokeIntegrationToken;
  import im.actor.core.api.rpc.RequestRevokeInviteUrl;
  import im.actor.core.api.rpc.RequestSaveAdminSettings;
  import im.actor.core.api.rpc.RequestShareHistory;
  import im.actor.core.api.rpc.RequestTransferOwnership;
  import im.actor.core.api.rpc.ResponseIntegrationToken;
  import im.actor.core.api.rpc.ResponseInviteUrl;
  import im.actor.core.api.rpc.ResponseVoid;
  import im.actor.core.entity.Group;
  import im.actor.core.entity.GroupMember;
  import im.actor.core.entity.GroupMembersSlice;
  import im.actor.core.entity.GroupPermissions;
  import im.actor.core.entity.Peer;
  import im.actor.core.entity.PeerType;
  import im.actor.core.entity.User;
  import im.actor.core.events.PeerChatOpened;
  import im.actor.core.events.PeerInfoOpened;
  import im.actor.core.modules.AbsModule;
  import im.actor.core.modules.ModuleContext;
  import im.actor.core.modules.api.ApiSupportConfiguration;
  import im.actor.core.modules.groups.router.GroupRouterInt;
  import im.actor.core.modules.profile.avatar.GroupAvatarChangeActor;
  import im.actor.core.util.RandomUtils;
  import im.actor.core.viewmodel.GroupAvatarVM;
  import im.actor.core.viewmodel.GroupVM;
  import im.actor.runtime.Storage;
  import im.actor.runtime.actors.ActorRef;
  import im.actor.runtime.actors.messages.Void;
  import im.actor.runtime.eventbus.BusSubscriber;
  import im.actor.runtime.eventbus.Event;
  import im.actor.runtime.function.Function;
  import im.actor.runtime.mvvm.MVVMCollection;
  import im.actor.runtime.promise.Promise;
  import im.actor.runtime.promise.Promises;
  import im.actor.runtime.storage.KeyValueEngine;
  
  import static im.actor.runtime.actors.ActorSystem.system;
  
  public class GroupsModule extends AbsModule implements BusSubscriber {
  
      // Workaround for j2objc bug
      private static final Void DUMB = null;
      private static final ResponseVoid DUMB2 = null;
  
      private final KeyValueEngine<Group> groups;
      private final MVVMCollection<Group, GroupVM> collection;
      private final HashMap<Integer, GroupAvatarVM> avatarVMs;
      private final ActorRef avatarChangeActor;
      private final GroupRouterInt groupRouterInt;
  
      public GroupsModule(final ModuleContext context) {
          super(context);
  
          collection = Storage.createKeyValue(STORAGE_GROUPS, GroupVM.CREATOR, Group.CREATOR);
          groups = collection.getEngine();
  
          groupRouterInt = new GroupRouterInt(context);
  
          avatarVMs = new HashMap<>();
          avatarChangeActor = system().actorOf("actor/avatar/group", () -> new GroupAvatarChangeActor(context));
      }
  
      public void run() {
          context().getEvents().subscribe(this, PeerChatOpened.EVENT);
          context().getEvents().subscribe(this, PeerInfoOpened.EVENT);
      }
  
      //
      // Storage
      //
  
      public KeyValueEngine<Group> getGroups() {
          return groups;
      }
  
      public GroupAvatarVM getAvatarVM(int gid) {
          synchronized (avatarVMs) {
              if (!avatarVMs.containsKey(gid)) {
                  avatarVMs.put(gid, new GroupAvatarVM(gid));
              }
              return avatarVMs.get(gid);
          }
      }
  
      public MVVMCollection<Group, GroupVM> getGroupsCollection() {
          return collection;
      }
  
      public GroupRouterInt getRouter() {
          return groupRouterInt;
      }
  
      //
      // Actions
      //
  
      public Promise<Integer> createGroup(String title, String avatarDescriptor, int[] uids) {
          return createGroup(title, avatarDescriptor, uids, ApiGroupType.GROUP);
      }
  
      public Promise<Integer> createChannel(String title, String avatarDescriptor) {
          return createGroup(title, avatarDescriptor, new int[0], ApiGroupType.CHANNEL);
      }
  
      private Promise<Integer> createGroup(String title, String avatarDescriptor, int[] uids,
                                           ApiGroupType groupType) {
          long rid = RandomUtils.nextRid();
          return Promise.success(uids)
                  .map((Function<int[], List<ApiUserOutPeer>>) ints -> {
                      ArrayList<ApiUserOutPeer> peers = new ArrayList<>();
                      for (int u : uids) {
                          User user = users().getValue(u);
                          if (user != null) {
                              peers.add(new ApiUserOutPeer(u, user.getAccessHash()));
                          }
                      }
                      return peers;
                  })
                  .flatMap(apiUserOutPeers ->
                          api(new RequestCreateGroup(rid, title, apiUserOutPeers,
                                  groupType, ApiSupportConfiguration.OPTIMIZATIONS)))
                  .chain(r -> updates().applyRelatedData(r.getUsers(), r.getGroup()))
                  .chain(r -> updates().waitForUpdate(r.getSeq()))
                  .map(r -> r.getGroup().getId())
                  .then(integer -> {
                      if (avatarDescriptor != null) {
                          changeAvatar(integer, avatarDescriptor);
                      }
                  });
      }
  
      public Promise<Void> addMember(final int gid, final int uid) {
          final long rid = RandomUtils.nextRid();
          return Promises.tuple(getGroups().getValueAsync(gid), users().getValueAsync(uid))
                  .flatMap(groupUserTuple2 ->
                          api(new RequestInviteUser(
                                  new ApiGroupOutPeer(gid, groupUserTuple2.getT1().getAccessHash()),
                                  rid,
                                  new ApiUserOutPeer(uid, groupUserTuple2.getT2().getAccessHash()),
                                  ApiSupportConfiguration.OPTIMIZATIONS)))
                  .flatMap(r -> updates().waitForUpdate(r.getSeq()));
      }
  
      public Promise<Void> kickMember(int gid, int uid) {
          final long rid = RandomUtils.nextRid();
          return Promises.tuple(getGroups().getValueAsync(gid), users().getValueAsync(uid))
                  .flatMap(groupUserTuple2 ->
                          api(new RequestKickUser(
                                  new ApiGroupOutPeer(gid, groupUserTuple2.getT1().getAccessHash()),
                                  rid,
                                  new ApiUserOutPeer(uid, groupUserTuple2.getT2().getAccessHash()),
                                  ApiSupportConfiguration.OPTIMIZATIONS)))
                  .flatMap(r -> updates().waitForUpdate(r.getSeq()));
      }
  
      public Promise<Void> leaveGroup(int gid) {
          final long rid = RandomUtils.nextRid();
          return getGroups().getValueAsync(gid)
                  .flatMap(group ->
                          api(new RequestLeaveGroup(
                                  new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()),
                                  rid,
                                  ApiSupportConfiguration.OPTIMIZATIONS)))
                  .flatMap(r -> updates().waitForUpdate(r.getSeq()));
      }
  
      public Promise<Void> leaveAndDeleteGroup(int gid) {
          return getGroups().getValueAsync(gid)
                  .flatMap(group ->
                          api(new RequestLeaveAndDelete(
                                  new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()))))
                  .flatMap(r -> updates().waitForUpdate(r.getSeq()));
      }
  
      public Promise<Void> deleteGroup(int gid) {
          return getGroups().getValueAsync(gid)
                  .flatMap(group ->
                          api(new RequestDeleteGroup(new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()))))
                  .flatMap(r -> updates().waitForUpdate(r.getSeq()));
      }
  
      public Promise<Void> shareHistory(int gid) {
          return getGroups().getValueAsync(gid)
                  .flatMap(group ->
                          api(new RequestShareHistory(new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()))))
                  .flatMap(r -> updates().waitForUpdate(r.getSeq()));
      }
  
      public Promise<Void> makeAdmin(final int gid, final int uid) {
          return Promises.tuple(getGroups().getValueAsync(gid), users().getValueAsync(uid))
                  .flatMap(groupUserTuple2 -> api(new RequestMakeUserAdminObsolete(
                          new ApiGroupOutPeer(gid, groupUserTuple2.getT1().getAccessHash()),
                          new ApiUserOutPeer(uid, groupUserTuple2.getT2().getAccessHash()))))
                  .flatMap(r -> updates().waitForUpdate(r.getSeq()));
      }
  
      public Promise<Void> revokeAdmin(final int gid, final int uid) {
          return Promises.tuple(getGroups().getValueAsync(gid), users().getValueAsync(uid))
                  .flatMap(groupUserTuple2 -> api(new RequestDismissUserAdmin(
                          new ApiGroupOutPeer(gid, groupUserTuple2.getT1().getAccessHash()),
                          new ApiUserOutPeer(uid, groupUserTuple2.getT2().getAccessHash()))))
                  .flatMap(r -> updates().waitForUpdate(r.getSeq()));
      }
  
      public Promise<Void> transferOwnership(final int gid, final int uid) {
          return Promises.tuple(getGroups().getValueAsync(gid), users().getValueAsync(uid))
                  .flatMap(groupUserTuple2 -> api(new RequestTransferOwnership(
                          new ApiGroupOutPeer(gid, groupUserTuple2.getT1().getAccessHash()),
                          new ApiUserOutPeer(uid, groupUserTuple2.getT2().getAccessHash()))))
                  .flatMap(r -> updates().waitForUpdate(r.getSeq()));
      }
  
      public Promise<Void> editTitle(final int gid, final String name) {
          final long rid = RandomUtils.nextRid();
          return getGroups().getValueAsync(gid)
                  .flatMap(group ->
                          api(new RequestEditGroupTitle(
                                  new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()),
                                  rid, name,
                                  ApiSupportConfiguration.OPTIMIZATIONS)))
                  .flatMap(r -> updates().waitForUpdate(r.getSeq()));
      }
  
      public Promise<Void> editTheme(final int gid, final String theme) {
          final long rid = RandomUtils.nextRid();
          return getGroups().getValueAsync(gid)
                  .flatMap(group ->
                          api(new RequestEditGroupTopic(
                                  new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()),
                                  rid, theme,
                                  ApiSupportConfiguration.OPTIMIZATIONS)))
                  .flatMap(r -> updates().waitForUpdate(r.getSeq()));
      }
  
      public Promise<Void> editAbout(final int gid, final String about) {
          final long rid = RandomUtils.nextRid();
          return getGroups().getValueAsync(gid)
                  .flatMap(group ->
                          api(new RequestEditGroupAbout(
                                  new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()),
                                  rid, about, ApiSupportConfiguration.OPTIMIZATIONS)))
                  .flatMap(r -> updates().waitForUpdate(r.getSeq()));
      }
  
      public Promise<Void> editShortName(final int gid, final String shortName) {
          return getGroups().getValueAsync(gid)
                  .flatMap(group ->
                          api(new RequestEditGroupShortName(new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()),
                                  shortName)))
                  .flatMap(r -> updates().waitForUpdate(r.getSeq()));
      }
  
      public Promise<GroupPermissions> loadAdminSettings(int gid) {
          return getGroups().getValueAsync(gid)
                  .flatMap(group -> api(new RequestLoadAdminSettings(new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()))))
                  .map(r -> new GroupPermissions(r.getSettings()));
      }
  
      public Promise<Void> saveAdminSettings(int gid, GroupPermissions adminSettings) {
          return getGroups().getValueAsync(gid)
                  .flatMap(group -> api(new RequestSaveAdminSettings(
                          new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()),
                          adminSettings.getApiSettings())))
                  .map(r -> null);
      }
  
      public Promise<GroupMembersSlice> loadMembers(int gid, int limit, byte[] next) {
          return getGroups().getValueAsync(gid)
                  .flatMap(group ->
                          api(new RequestLoadMembers(
                                  new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()),
                                  limit, next)))
                  .chain(r -> updates().loadRequiredPeers(r.getUsers(), new ArrayList<>()))
                  .map(r -> {
                      ArrayList<GroupMember> members = new ArrayList<>();
                      for (ApiMember p : r.getMembers()) {
                          boolean isAdmin = p.isAdmin() != null ? p.isAdmin() : false;
                          members.add(new GroupMember(p.getUid(),
                                  p.getInviterUid(), p.getInviterUid(), isAdmin));
                      }
                      return new GroupMembersSlice(members, r.getNext());
                  });
      }
  
      public void changeAvatar(int gid, String descriptor) {
          avatarChangeActor.send(new GroupAvatarChangeActor.ChangeAvatar(gid, descriptor));
      }
  
      public void removeAvatar(int gid) {
          avatarChangeActor.send(new GroupAvatarChangeActor.RemoveAvatar(gid));
      }
  
  
      //
      // Join
      //
  
      public Promise<String> requestInviteLink(final int gid) {
          return getGroups().getValueAsync(gid)
                  .flatMap(group ->
                          api(new RequestGetGroupInviteUrl(
                                  new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()))))
                  .map(ResponseInviteUrl::getUrl);
      }
  
      public Promise<String> requestRevokeLink(final int gid) {
          return getGroups().getValueAsync(gid)
                  .flatMap(group ->
                          api(new RequestRevokeInviteUrl(
                                  new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()))))
                  .map(ResponseInviteUrl::getUrl);
      }
  
      public Promise<Integer> joinGroupByToken(final String token) {
          return api(new RequestJoinGroup(token, ApiSupportConfiguration.OPTIMIZATIONS))
                  .chain(responseJoinGroup -> updates().loadRequiredPeers(responseJoinGroup.getUserPeers(), new ArrayList<>()))
                  .chain(r -> updates().waitForUpdate(r.getSeq()))
                  .map(responseJoinGroup -> responseJoinGroup.getGroup().getId());
      }
  
      public Promise<Void> joinGroup(int gid) {
          return getGroups().getValueAsync(gid)
                  .flatMap(group ->
                          api(new RequestJoinGroupByPeer(
                                  new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()))))
                  .chain(r -> updates().waitForUpdate(r.getSeq()))
                  .map(r -> null);
      }
  
      //
      // Integration Token
      //
  
      public Promise<String> requestIntegrationToken(final int gid) {
          return getGroups().getValueAsync(gid)
                  .flatMap(group ->
                          api(new RequestGetIntegrationToken(
                                  new ApiOutPeer(
                                          ApiPeerType.GROUP,
                                          group.getGroupId(),
                                          group.getAccessHash()))))
                  .map(ResponseIntegrationToken::getUrl);
      }
  
      public Promise<String> revokeIntegrationToken(final int gid) {
          return getGroups().getValueAsync(gid)
                  .flatMap(group ->
                          api(new RequestRevokeIntegrationToken(
                                  new ApiOutPeer(
                                          ApiPeerType.GROUP,
                                          group.getGroupId(),
                                          group.getAccessHash()))))
                  .map(ResponseIntegrationToken::getUrl);
      }
  
  
      public void resetModule() {
          groups.clear();
      }
  
      @Override
      public void onBusEvent(Event event) {
          if (event instanceof PeerChatOpened) {
              Peer peer = ((PeerChatOpened) event).getPeer();
              if (peer.getPeerType() == PeerType.GROUP) {
                  getRouter().onFullGroupNeeded(peer.getPeerId());
              }
          } else if (event instanceof PeerInfoOpened) {
              Peer peer = ((PeerInfoOpened) event).getPeer();
              if (peer.getPeerType() == PeerType.GROUP) {
                  getRouter().onFullGroupNeeded(peer.getPeerId());
              }
          }
      }
  }