import { ChangeDetectorRef, Component, ElementRef, OnDestroy, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { marker } from '@biesbjerg/ngx-translate-extract-marker';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { CreateRoomDialogComponent } from 'src/app/shared/dialogs/create-room-dialog/create-room-dialog.component';
import { NewGroupDialogComponent } from 'src/app/shared/dialogs/new-group-dialog/new-group-dialog.component';
import {
  AppDragDropService,
  AppDragDropServiceHelper,
  AppDragMoveEvent,
} from 'src/app/shared/drag/app-drag-drop.service';
import { AuthService } from 'src/app/shared/server-services/auth.service';
import { ID } from 'src/app/shared/server-services/query-records/common-records';
import {
  SidebarGroup,
  SidebarResource,
} from 'src/app/shared/server-services/query-records/sidebar-records';
import { WorkspaceSubscriptionResourceGroupEventRecord } from 'src/app/shared/server-services/query-records/workspace-records';
import { RoomService } from 'src/app/shared/server-services/room.service';
import { SearchService } from 'src/app/shared/server-services/search-service';
import { SidebarService } from 'src/app/shared/server-services/sidebar.service';
import {
  SubscriptionServiceEvent,
  SubscriptionServiceEventType,
} from 'src/app/shared/server-services/subscription-event';
import { SubscriptionService } from 'src/app/shared/server-services/subscription.service';
import { recalculateNews } from 'src/app/shared/services/cache/cache-logic/response-merger/subscription-merger/sub-merger/news-handler/news-handler';
import { CacheService } from 'src/app/shared/services/cache/cache.service';
import { CordovaService } from 'src/app/shared/services/cordova.service';
import { DialogService } from 'src/app/shared/services/dialog.service';
import { RouterHandler } from 'src/app/shared/services/router-handler.service';
import { SnackBarService } from 'src/app/shared/services/snackbar.service';
import { showWorkspaceTabObserver } from 'src/app/shared/shared-observers';
import { OrderedList } from './ordered-list';

export let newsObserver = new Subject<number>();

@Component({
  selector: 'app-sidebar',
  templateUrl: './sidebar.component.html',
  styleUrls: ['./sidebar.component.scss'],
})
export class SidebarComponent implements OnDestroy {
  public ungroupedResources: SidebarResource[];
  public groups: SidebarGroup[];

  // group data structure
  public orderedGroups: OrderedList<SidebarGroup>;
  // reference of the showed list
  public showGroups: SidebarGroup[] = [];

  // rooms where you get an invitation
  public grantedRooms: ID[] = [];
  public rejectedRooms: ID[] = [];

  public newsList: SidebarResource[] = [];
  public newsNumber = 0;

  public workspaceLoaded: boolean = false;
  public activePanel: string = 'inorder';

  /**
   * We will collapse all of the group, when a drag happened on group level
   */
  public isGroupDragStarted: boolean = false;

  /**
   * It's a running time calculated height in px, this define the require space before
   * the elements, when a drag happening on group level. With this the scroller
   * won't jumping up or down.
   */
  public startPlaceFillerHeight = 0;

  /**
   * We fix the height of the container when drag happened on group level. This
   * compels the scroller to stay the same position
   */
  public expPanelHeight = 'auto';

  /**
   * We want to autoscroll, when the dragged element waits over the beginning or ending
   * of the container. This timer defines this waiting via window.setTimeOut
   */
  private isCursorOnScrollPositionTimer; // for settimeout

  /**
   * We only need the component level facilities of the API
   */
  public dragDropService: AppDragDropServiceHelper;

  /**
   * Height of the groups in pixel when it is collapsed.
   */
  static GROUP_BOX_HEIGHT = 53;

  /**
   * The space in pixel where the auto scroller is triggered when you drag an element over it
   */
  static SCROLL_DISTANCE_SENSITIVITY = 100;

  /**
   * The space in pixel over and below of the container, when you drag and scroll an element to
   * the container's edges. It must be 0 or higher.
   */
  static SCROLL_OVERFLOW = 30;

  /**
   * The scroll height in pixel, when you drag an element to the container's edges
   */
  static SCROLL_SPEED = 50;

  /**
   * Defined css animation speed in angular:  EXPANSION_PANEL_ANIMATION_TIMING
   * We can not change it, so we have to syncronize our animations to it.
   * Note: When you have to change this, make sure you changed it in the css as well:
   * resources-diebar.component.css: mat-accordio, .start-place-filler
   */
  static ANGULAR_EXPANSION_PANEL_ANIMATION_SPEED = 225;

  /**
   * is this the first time, the sidebar is loaded
   */
  private initLoad: boolean = true;

  @ViewChild('expPanelContainer') expPanelNode: ElementRef;
  @ViewChild('expScrollWindow') expScrollWindow: ElementRef;

  constructor(
    protected subscriptionService: SubscriptionService,
    public dialog: MatDialog,
    protected roomService: RoomService,
    protected dialogService: DialogService,
    protected sidebarService: SidebarService,
    protected appDragDropService: AppDragDropService,
    protected searchService: SearchService,
    protected routerHandler: RouterHandler,
    protected changeDetectionRef: ChangeDetectorRef,
    protected authService: AuthService,
    protected snackbarService: SnackBarService,
    protected cacheService: CacheService,
    protected deviceDetectorService: DeviceDetectorService,
    protected cordovaService: CordovaService
  ) {
    this.subscriptionService.subscribeWorkspaceLoaded(this.onWorkspaceLoaded);

    // subscribe to search
    this.routerHandler.subscribe('search', this.onRouterChangeSearch);
    if (this.routerHandler.isOnPage('search')) {
      this.onRouterChangeSearch(this.routerHandler.getRoute());
    }

    // subscriptions --------
    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.RESOURCE_GROUP_EVENT,
      SubscriptionServiceEventType.CREATE,
      this.onCreateResourceGroup
    );

    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.RESOURCE_GROUP_EVENT,
      SubscriptionServiceEventType.MODIFY,
      this.onEditResourceGroup
    );

    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.RESOURCE_GROUP_EVENT,
      SubscriptionServiceEventType.DELETE,
      this.onDeleteResourceGroup
    );

    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.ROOM_MESSAGE_EVENT,
      SubscriptionServiceEventType.CREATE,
      this.refreshNewsList
    );

    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT,
      SubscriptionServiceEventType.CREATE,
      this.refreshNewsList
    );

    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.ROOM_EVENT,
      SubscriptionServiceEventType.CREATE,
      this.refreshNewsAndGrantedRooms
    );

    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.ROOM_EVENT,
      SubscriptionServiceEventType.DELETE,
      this.refreshNewsAndGrantedRooms
    );

    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.ROOM_ACCOUNT_PERMISSION_EVENT,
      SubscriptionServiceEventType.CREATE,
      this.refreshNewsAndGrantedRooms
    );

    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.ROOM_ACCOUNT_PERMISSION_EVENT,
      SubscriptionServiceEventType.DELETE,
      this.refreshNewsAndGrantedRooms
    );

    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.ROOM_EVENT,
      SubscriptionServiceEventType.MODIFY,
      this.refreshNewsList
    );

    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.DIALOGUE_EVENT,
      SubscriptionServiceEventType.ALL,
      this.refreshNewsList
    );

    showWorkspaceTabObserver.subscribe(() => {
      if (this.activePanel !== 'inorder') {
        this.activePanel = 'inorder';
        this.changeDetectionRef.detectChanges();
      }
    });

    // init, after query done
    this.dragDropService = this.appDragDropService;
    this.dragDropService.subscribeDragMove(this.scrollMouseMoveHandler);
  }

  private onWorkspaceLoaded = (res) => {
    this.workspaceLoaded = false;

    if (!this.authService.isAnonym()) {
      this.sidebarService.getSidebarUngroupped().then((res) => {
        this.ungroupedResources = res;
      });

      this.sidebarService.getAllSidebarGroup().then((res) => {
        this.groups = res;

        this.orderedGroups = new OrderedList<SidebarGroup>(this.groups);
        this.showGroups = this.orderedGroups.getInorderedDataRef();
      });

      this.refreshNewsAndGrantedRooms();
    }

    this.workspaceLoaded = true;
  };

  private refreshNewsList = () => {
    return this.sidebarService.getSidebarNews().then((news) => {
      // TODO
      // The server should filter the deleted rooms from the granted rooms but for that
      // we need a server event streaming rework.
      this.newsNumber = news.news + (this.grantedRooms.length > 0 ? 1 : 0);

      // TODO find a good solution for newsObserver
      newsObserver.next(this.newsNumber);
      this.newsList = news.resources;
    });
  };

  private refreshGrantedRooms = () => {
    return Promise.all([
      this.sidebarService.getGrantedRooms().then((rooms) => {
        this.grantedRooms = rooms;
      }),

      //rejected rooms are moved to RejectedRoomsComponent
      // this.sidebarService.getRejectedRooms().then((rooms) => {
      //   this.rejectedRooms = rooms;
      // }),
    ]);
  };

  // make a callback which can subscribe/unsubscribe to events
  private refreshNewsAndGrantedRooms = () => {
    return this.refreshGrantedRooms().then(() => {
      this.refreshNewsList();
      this.triggerIOSGUIRefresh();
    });
  };

  ngOnDestroy(): void {
    this.subscriptionService.unsubscribeWorkspaceLoaded(this.onWorkspaceLoaded);

    this.routerHandler.unsubscribe('search', this.onRouterChangeSearch);

    // subscriptions --------
    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.RESOURCE_GROUP_EVENT,
      SubscriptionServiceEventType.CREATE,
      this.onCreateResourceGroup
    );

    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.RESOURCE_GROUP_EVENT,
      SubscriptionServiceEventType.MODIFY,
      this.onEditResourceGroup
    );

    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.RESOURCE_GROUP_EVENT,
      SubscriptionServiceEventType.DELETE,
      this.onDeleteResourceGroup
    );

    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.ROOM_MESSAGE_EVENT,
      SubscriptionServiceEventType.CREATE,
      this.refreshNewsList
    );

    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT,
      SubscriptionServiceEventType.CREATE,
      this.refreshNewsList
    );

    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.ROOM_EVENT,
      SubscriptionServiceEventType.CREATE,
      this.refreshNewsAndGrantedRooms
    );

    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.ROOM_EVENT,
      SubscriptionServiceEventType.DELETE,
      this.refreshNewsAndGrantedRooms
    );

    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.ROOM_ACCOUNT_PERMISSION_EVENT,
      SubscriptionServiceEventType.CREATE,
      this.refreshNewsAndGrantedRooms
    );

    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.ROOM_ACCOUNT_PERMISSION_EVENT,
      SubscriptionServiceEventType.DELETE,
      this.refreshNewsAndGrantedRooms
    );

    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.ROOM_EVENT,
      SubscriptionServiceEventType.MODIFY,
      this.refreshNewsList
    );

    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.DIALOGUE_EVENT,
      SubscriptionServiceEventType.ALL,
      this.refreshNewsList
    );

    // init, after query done
    this.dragDropService.unsubscribeDragMove(this.scrollMouseMoveHandler);
  }

  // dialogs and menu states ------------------------------
  openNewGroupDialog(): void {
    const dialogRef = this.dialog.open(NewGroupDialogComponent);

    dialogRef.afterClosed().subscribe((result) => {
      if (result) {
        //console.log('client>server, create group: name', name);
        this.sidebarService.createGroup(result).then((res) => {
          showWorkspaceTabObserver.next();
        });
      }
    });
  }

  openNewRoomDialog(): void {
    const dialogRef = this.dialog.open(CreateRoomDialogComponent);

    dialogRef.afterClosed().subscribe((result) => {
      if (!result) return;
      console.log('room create', result);
      this.roomService
        .newRoom(result)
        .then(() => {
          this.snackbarService.showSnackbar(marker('Room created successfully!'));
          showWorkspaceTabObserver.next();
        })
        .catch((error) => {
          if (error.message === 'Own room limit is reached (200)') {
            this.dialogService.openAlertDialog(
              marker('Room limit'),
              marker('Own room limit is reached.')
            );
          } else {
            this.dialogService.openAlertDialog(
              marker('Create room'),
              marker('Could not create room.')
            );
          }
          console.error('Room creation error', error);
        });
    });
  }

  showInorder() {
    this.activePanel = 'inorder';
  }

  showSearch() {
    this.activePanel = 'search';
  }

  showNews() {
    this.activePanel = 'news';
    recalculateNews(this.cacheService);
    this.refreshNewsList();
  }

  // group events
  private onCreateResourceGroup = (event: WorkspaceSubscriptionResourceGroupEventRecord) => {
    this.sidebarService.getGroup(event.id).then((group) => {
      this.orderedGroups.pushByOrdering(group);
    });
  };

  private onEditResourceGroup = (event: WorkspaceSubscriptionResourceGroupEventRecord) => {
    if (event.ordering) {
      // the cache layer changed the ordering values of the references
      this.orderedGroups.reorder();

      if (OrderedList.isPositionNeedToOptimize(event.ordering)) {
        let optimize = this.orderedGroups.getOptimizedOrdering();

        console.log('optimize groups', optimize);
        optimize.forEach((el) => {
          this.sidebarService.editGroupOrdering(el.element.id, el.ordering);
        });
      }
    }
    this.triggerIOSGUIRefresh();
  };

  private onDeleteResourceGroup = (event: WorkspaceSubscriptionResourceGroupEventRecord) => {
    this.orderedGroups.remove(event.id);
  };

  // drag n drop handlers ------------------------------------

  public dragStarted(event) {
    let container = this.expPanelNode.nativeElement;
    this.expPanelHeight = container.getBoundingClientRect().height + 'px';

    // calc the children DOM node before the dragged element
    let childNumberBefore = 0;
    let el: any = event.srcComponent.getDOMNode().previousSibling;
    while ((el = el.previousSibling) != null) {
      childNumberBefore++;
    }

    this.startPlaceFillerHeight =
      event.srcComponent.getDOMNode().offsetTop -
      childNumberBefore * SidebarComponent.GROUP_BOX_HEIGHT;
    //console.log("startplacefiller", this.startPlaceFillerHeight);

    this.isGroupDragStarted = true;
  }

  /**
   *
   * @param ev contains the new position of the group (from 0 to array length-1): ev.pos
   * @param group the current dragged group
   */
  public groupDragEnded(ev: AppDragMoveEvent, group: SidebarGroup) {
    this.isGroupDragStarted = false;

    this.startPlaceFillerHeight = 0;

    let ordering = this.orderedGroups.calcOrderingByPosition(group, ev.pos);
    this.sidebarService.editGroupOrdering(group.id, ordering);

    // TODO
    /*if (this.isPositionNeedToOptimize(ordering)) {
      this.optimizeGroupOrdering();
    }*/

    window.setTimeout(() => {
      this.expPanelHeight = 'auto';
    }, SidebarComponent.ANGULAR_EXPANSION_PANEL_ANIMATION_SPEED);
  }

  // this event subscribed to the dragMove
  /**
   * A handler connected to the dragMove event in AppDragDropService
   */
  private scrollMouseMoveHandler = (pos, comp) => {
    // this even can be triggered by mousemove, and window.settimeout, and we don't want to run it twice in the same time, so first of all, clear the timer
    clearTimeout(this.isCursorOnScrollPositionTimer);
    if (!this.dragDropService.isDragOn()) return;

    let cont = this.expScrollWindow.nativeElement;
    let bond = cont.getBoundingClientRect();

    let x = pos.x;
    let y = pos.y;

    if (x > bond.left && x < bond.left + bond.width) {
      if (y > bond.top && y < bond.top + SidebarComponent.SCROLL_DISTANCE_SENSITIVITY) {
        // check the top empty range, we dont want to scroll over the emptiness
        // you can overflow by 30 pixel, to see you reached the last element
        // only when you dragged a group
        if (
          comp.getDropZone() == 'sidebar-groups' &&
          cont.scrollTop <=
            this.startPlaceFillerHeight +
              (SidebarComponent.SCROLL_DISTANCE_SENSITIVITY - SidebarComponent.SCROLL_OVERFLOW)
        ) {
          cont.scrollTo({
            top: this.startPlaceFillerHeight - SidebarComponent.SCROLL_OVERFLOW,
            left: 0,
            behavior: 'smooth',
          });
        } else {
          // top edge
          cont.scrollBy({
            top: -1 * SidebarComponent.SCROLL_SPEED,
            left: 0,
            behavior: 'smooth',
          });
        }

        this.isCursorOnScrollPositionTimer = setTimeout(() => {
          this.scrollMouseMoveHandler(pos, comp);
        }, 100);
      } else if (
        y < bond.top + bond.height &&
        y > bond.top + bond.height - SidebarComponent.SCROLL_DISTANCE_SENSITIVITY
      ) {
        /**
         * Watch out! You have to reach the height of the parent container to start scrolling. So when you reached the scrollTop 100px, you are on the containerHeight+100px
         */
        if (
          comp.getDropZone() == 'sidebar-groups' &&
          cont.scrollTop >=
            this.startPlaceFillerHeight +
              (this.expPanelNode.nativeElement.childElementCount - 1) *
                SidebarComponent.GROUP_BOX_HEIGHT -
              bond.height -
              (SidebarComponent.SCROLL_DISTANCE_SENSITIVITY - SidebarComponent.SCROLL_OVERFLOW)
        ) {
          cont.scrollTo({
            top:
              this.startPlaceFillerHeight +
              (this.expPanelNode.nativeElement.childElementCount - 1) *
                SidebarComponent.GROUP_BOX_HEIGHT -
              bond.height +
              SidebarComponent.SCROLL_OVERFLOW,
            left: 0,
            behavior: 'smooth',
          });
        } else {
          // bottom edge
          cont.scrollBy({
            top: SidebarComponent.SCROLL_SPEED,
            left: 0,
            behavior: 'smooth',
          });
        }

        this.isCursorOnScrollPositionTimer = setTimeout(() => {
          this.scrollMouseMoveHandler(pos, comp);
        }, 100);
      }
    }
  };

  public onRouterChangeSearch = (res) => {
    this.showSearch();
  };

  private triggerIOSGUIRefresh() {
    setTimeout(() => {
      /**
       * In some position (rel+abs) the reverse flexbox ui renderer in safari is buggy
       * we have to trigger a view redraw with some viewport matrix or layout change (css hacks would also work)
       */
      if (
        this.deviceDetectorService.browser == 'Safari' ||
        (this.cordovaService.isOnApp() && this.cordovaService.getDevicePlatform() == 'iOS')
      ) {
        this.expScrollWindow.nativeElement.scrollBy(0, -1);
        this.expScrollWindow.nativeElement.scrollBy(0, 1);
      }
    }, 125 + 10); // wait for the group open/close animation
  }
}
