text: //
  //  UIViewController+JCPresentQueue.m
  //  JCPresentController
  //
  //  Created by HJaycee on 2017/4/4.
  //  Copyright © 2017年 HJaycee. All rights reserved.
  //
  
  #import "UIViewController+JCPresentQueue.h"
  #import <objc/runtime.h>
  
  NSString * const JCPresentControllersAllDismissedNotification = @"JCPresentControllersAllDismissedNotification";
  
  static NSMutableArray *_stackControllers;
  static NSOperationQueue *_operationQueue;
  static dispatch_queue_t _jc_present_queue;
  
  @implementation UIViewController (JCPresentQueue)
  
  + (void)load {
      SEL oldSel = @selector(viewDidDisappear:);
      SEL newSel = @selector(jc_viewDidDisappear:);
      Method oldMethod = class_getInstanceMethod([self class], oldSel);
      Method newMethod = class_getInstanceMethod([self class], newSel);
      
      BOOL didAddMethod = class_addMethod(self, oldSel, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
      if (didAddMethod) {
          class_replaceMethod(self, newSel, method_getImplementation(oldMethod), method_getTypeEncoding(oldMethod));
      } else {
          method_exchangeImplementations(oldMethod, newMethod);
      }
      
      _stackControllers = ({
          static NSMutableArray *stackControllers = nil;
          static dispatch_once_t onceToken;
          dispatch_once(&onceToken, ^{
              stackControllers = [NSMutableArray array];
          });
          stackControllers;
      });
      
      _operationQueue = ({
          static NSOperationQueue *operationQueue = nil;
          static dispatch_once_t onceToken;
          dispatch_once(&onceToken, ^{
              _jc_present_queue = dispatch_queue_create("jc_present_queue", DISPATCH_QUEUE_SERIAL);
              operationQueue = [NSOperationQueue new];
              operationQueue.underlyingQueue = _jc_present_queue;
          });
          operationQueue;
      });
  }
  
  - (void)jc_viewDidDisappear:(BOOL)animated {
      [self jc_viewDidDisappear:animated];
      
      if ([self getDeallocCompletion] && ![self isTemporarilyDismissed]) {
          [self getDeallocCompletion]();
      }
  }
  
  - (void)setDeallocCompletion:(void (^)(void))completion {
      objc_setAssociatedObject(self, @selector(getDeallocCompletion), completion, OBJC_ASSOCIATION_COPY_NONATOMIC);
  }
  
  - (void (^)(void))getDeallocCompletion {
      return objc_getAssociatedObject(self, _cmd);
  }
  
  - (void)setDismissCompletion:(void (^)(void))completion {
      objc_setAssociatedObject(self, @selector(getDismissCompletion), completion, OBJC_ASSOCIATION_COPY_NONATOMIC);
  }
  
  - (void (^)(void))getDismissCompletion {
      return objc_getAssociatedObject(self, _cmd);
  }
  
  - (void)setPresentCompletion:(void (^)(void))completion {
      objc_setAssociatedObject(self, @selector(getPresentCompletion), completion, OBJC_ASSOCIATION_COPY_NONATOMIC);
  }
  
  - (void (^)(void))getPresentCompletion {
      return objc_getAssociatedObject(self, _cmd);
  }
  
  - (void)setCurrentPresentType:(JCPresentType)presentType {
      objc_setAssociatedObject(self, @selector(getCurrentPresentType), @(presentType), OBJC_ASSOCIATION_COPY_NONATOMIC);
  }
  
  - (JCPresentType)getCurrentPresentType {
      NSNumber *type = objc_getAssociatedObject(self, _cmd);
      return [type integerValue];
  }
  
  - (void)setTemporarilyDismissed:(BOOL)temporarilyDismissed {
      objc_setAssociatedObject(self, @selector(isTemporarilyDismissed), @(temporarilyDismissed), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  }
  
  - (BOOL)isTemporarilyDismissed {
      NSNumber *num = objc_getAssociatedObject(self, _cmd);
      return [num boolValue];
  }
  
  - (void)setDismissing:(BOOL)dismissing {
      objc_setAssociatedObject(self, @selector(isDismissing), @(dismissing), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  }
  
  - (BOOL)isDismissing {
      NSNumber *num = objc_getAssociatedObject(self, _cmd);
      return [num boolValue];
  }
  
  - (void)jc_presentViewController:(UIViewController *)controller presentType:(JCPresentType)presentType presentCompletion:(void (^)(void))presentCompletion dismissCompletion:(void (^)(void))dismissCompletion {
      if ([_operationQueue operations].count > 0 && presentType != [self getCurrentPresentType]) {
          [NSException raise:@"This is dangerous." format:@"%@'s present queue is confused. Dont't use LIFO and FIFO two together.", self];
      }
      [self setCurrentPresentType:presentType];
      
      if (presentType == JCPresentTypeLIFO) {
          [self lifoPresentViewController:controller presentCompletion:presentCompletion dismissCompletion:dismissCompletion];
      } else {
          [self fifoPresentViewController:controller presentCompletion:presentCompletion dismissCompletion:dismissCompletion];
      }
  }
  
  #pragma mark - present controller with LIFO
  - (void)lifoPresentViewController:(UIViewController *)controller presentCompletion:(void (^)(void))presentCompletion dismissCompletion:(void (^)(void))dismissCompletion {
      
      dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
      
      // put in stack
      NSMutableArray *stackControllers = _stackControllers;
      if (![stackControllers containsObject:controller]) {
          [stackControllers addObject:controller];
      }
      
      NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
          __weak typeof(controller) weakController = controller;
          [controller setPresentCompletion:presentCompletion];
          [controller setDismissCompletion:dismissCompletion];
          [controller setDeallocCompletion:^{
              if (dismissCompletion) {
                  dismissCompletion();
              }
              
              // fetch new next controller if exists, because button action after dismiss completion
              [weakController setDismissing:YES];
              dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                  [weakController setDismissing:NO];
                  // if the dismiss controller is the last one
                  if (stackControllers.lastObject == weakController) {
                      [stackControllers removeObject:weakController];
                      
                      // is there any previous controllers
                      if (stackControllers.count > 0) {
                          UIViewController *preController = [stackControllers lastObject];
                          [self lifoPresentViewController:preController presentCompletion:[preController getPresentCompletion] dismissCompletion:[preController getDismissCompletion]];
                      } else {
                          [[NSNotificationCenter defaultCenter] postNotificationName:JCPresentControllersAllDismissedNotification object:nil];
                      }
                  } else {
                      NSUInteger index = [stackControllers indexOfObject:weakController];
                      [stackControllers removeObject:weakController];
                      
                      // is there any next controllers
                      NSArray *nextControllers = [stackControllers objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(index, stackControllers.count - index)]];
                      for (UIViewController *nextController in nextControllers) {
                          [self lifoPresentViewController:nextController presentCompletion:[nextController getPresentCompletion] dismissCompletion:[nextController getDismissCompletion]];
                      }
                  }
              });
          }];
          
          // if the previous controller is dismissing, wait it's completion
          if (stackControllers.count > 1) {
              for (UIViewController *preController in stackControllers) {
                  if ([preController isDismissing]) {
                      return ;
                  }
              }
          }
          
          // present a new controller before dismissing the presented controller if exists
          dispatch_async(dispatch_get_main_queue(), ^{
              if (self.presentedViewController) {
                  [self.presentedViewController temporarilyDismissViewControllerAnimated:YES completion:^{
                      [self presentViewController:controller animated:YES completion:^{
                          dispatch_semaphore_signal(semaphore);
                      }];
                  }];
              } else {
                  [self presentViewController:controller animated:YES completion:^{
                      dispatch_semaphore_signal(semaphore);
                      if (presentCompletion) {
                          presentCompletion();
                      }
                  }];
              }
          });
          
          dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
      }];
      
      // put in queue
      if (_operationQueue.operations.lastObject) {
          [operation addDependency:_operationQueue.operations.lastObject];
      }
      [_operationQueue addOperation:operation];
  }
  
  // dismiss controller temporarily
  - (void)temporarilyDismissViewControllerAnimated: (BOOL)flag completion: (void (^ __nullable)(void))completion {
      [self setTemporarilyDismissed:YES];
      [self dismissViewControllerAnimated:flag completion:^{
          [self setTemporarilyDismissed:NO];
          if (completion) {
              completion();
          }
      }];
  }
  
  #pragma mark - present controller with FIFO
  - (void)fifoPresentViewController:(UIViewController *)controller presentCompletion:(void (^)(void))presentCompletion dismissCompletion:(void (^)(void))dismissCompletion {
      dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
      NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
          [controller setDeallocCompletion:^{
              if (dismissCompletion) {
                  dismissCompletion();
              }
              
              if (_operationQueue.operations.count <= 1) {
                  [[NSNotificationCenter defaultCenter] postNotificationName:JCPresentControllersAllDismissedNotification object:nil];
              }
              
              // got to next operation
              dispatch_semaphore_signal(semaphore);
          }];
          dispatch_async(dispatch_get_main_queue(), ^{
              [self presentViewController:controller animated:YES completion:presentCompletion];
          });
          dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
      }];
      
      // put in queue
      if (_operationQueue.operations.lastObject) {
          [operation addDependency:_operationQueue.operations.lastObject];
      }
      [_operationQueue addOperation:operation];
  }
  
  @end
  