//
//  AAILivenessWrapViewController.m
//  AAILivenessDemo
//
//  Created by Advance.ai on 2019/3/2.
//  Copyright © 2019 Advance.ai. All rights reserved.
//

#import "AAILivenessViewController.h"
#import "AAILoadingHud.h"
#import "AAIHUD.h"
#import "AAILivenessResultViewController.h"
#import <AVFoundation/AVFoundation.h>
#import "AAIBundle.h"
#import "View+ex.h"
#import "UIColor+ex.h"
@interface AAILivenessViewController ()<AAILivenessWrapDelegate>
{
    NSString *_pre_key;
    
    BOOL _isReady;
    BOOL _isRequestingAuth;
    BOOL _requestAuthSucceed;
    BOOL _isFlowFinished;
    
    CGRect _ellipseFrame;
}
@property(nonatomic) BOOL isRequestingAuth;
@property(nonatomic) BOOL requestAuthSucceed;
@property(nonatomic) BOOL requestAuthComplete;
@property(nonatomic) BOOL requestAuthCached;
@property(nonatomic) BOOL hasPortraitDirection;

@property(nonatomic) NSTimeInterval prepareStartTime;
@property(nonatomic) NSTimeInterval authRequestCostTime;
@end

@implementation AAILivenessViewController
@synthesize isRequestingAuth = _isRequestingAuth;
@synthesize requestAuthSucceed = _requestAuthSucceed;

- (void)livenessWrapViewDidLoad:(AAILivenessWrapView *)wrapView
{
    if (self.detectionActions != NULL) {
        wrapView.detectionActions = [self.detectionActions copy];
    }
    
    /*
    // Subclass can override this method to customize the UI
    wrapView.backgroundColor = [UIColor grayColor];
    wrapView.roundBorderView.layer.borderColor = [UIColor redColor].CGColor;
    wrapView.roundBorderView.layer.borderWidth = 2;
     
    //Custom corner radius and the shape of the preview area
    CGFloat cornerRadius = 20;
    wrapView.roundBorderView.layer.cornerRadius = cornerRadius;
    
    //Custom preview area margin top
    wrapView.configAvatarPreviewMarginTop = ^CGFloat(CGRect wrapViewFrame) {
        return 64;
    };
     
    //Custom preview area width
    wrapView.configAvatarPreviewWidth = ^CGFloat(CGRect wrapViewFrame) {
        return 300;
    };
    */
}

- (void)loadAdditionalUI
{
    UIView *sv = self.view;
    
    UIButton *backBtn = [UIButton buttonWithType:UIButtonTypeCustom];
    [backBtn setImage:[AAILivenessUtil imgWithName:@"arrow_back"] forState:UIControlStateNormal];
    [backBtn addTarget:self action:@selector(tapBackBtnAction_) forControlEvents:UIControlEventTouchUpInside];
    
    if (self.navigationController.isNavigationBarHidden) {
        [sv addSubview:backBtn];
        [backBtn.youme safe_margin:[[AAIMargin alloc]initWitht:@0 l:@8 b:nil r:nil]];
        [backBtn.youme width:@36 height:@36];
        _backBtn = backBtn;
    } else {
        self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:backBtn];
    }
    
    //Detect state label
    UILabel *stateLabel = [[UILabel alloc] init];
    stateLabel.font = [UIFont boldSystemFontOfSize:20];
    stateLabel.textColor = [UIColor blackColor];
    stateLabel.numberOfLines = 0;
    stateLabel.textAlignment = NSTextAlignmentCenter;
    _stateLabel = stateLabel;
    [self.wrapView addSubview:_stateLabel];
   
    //Action status imageView
    UIImageView *stateImgView = [[UIImageView alloc] init];
    stateImgView.contentMode = UIViewContentModeScaleAspectFit;
    [sv addSubview:stateImgView];
    _stateImgView = stateImgView;
    
    //Voice switch button
    UIButton *voiceBtn = [[UIButton alloc] init];
    [voiceBtn setImage:[AAILivenessUtil imgWithName:@"liveness_open_voice@2x.png"] forState:UIControlStateNormal];
    [voiceBtn setImage:[AAILivenessUtil imgWithName:@"liveness_close_voice@2x.png"] forState:UIControlStateSelected];
    [sv addSubview:voiceBtn];
    [voiceBtn addTarget:self action:@selector(tapVoiceBtnAction:) forControlEvents:UIControlEventTouchUpInside];
    
    if ([AAILivenessUtil isSilent]) {
        voiceBtn.selected = YES;
    }
    
    _voiceBtn = voiceBtn;
    
    //Timeout interval label
    NSTimeInterval actionTimeout = _actionTimeoutInterval;
    [AAILivenessSDK configActionTimeoutSeconds:actionTimeout];
    
    _timeLabel = [[UILabel alloc] init];
    _timeLabel.font = [UIFont systemFontOfSize:14];
    _timeLabel.textColor = [UIColor colorWithRed:(0x36/255.f) green:(0x36/255.f) blue:(0x36/255.f) alpha:1];
    _timeLabel.text = [NSString stringWithFormat:@"%.f S", actionTimeout];
    _timeLabel.textAlignment = NSTextAlignmentCenter;
    [sv addSubview:_timeLabel];
    
    _timeLabel.hidden = YES;
    _voiceBtn.hidden = YES;
}

- (void)layoutAdditionalUI
{
    CGSize size = self.view.frame.size;
    
    //top
    CGFloat top = 0, marginLeft = 20, marginTop = 20;
    if (@available(iOS 11, *)) {
        top = self.view.safeAreaInsets.top;
    } else {
        if (self.navigationController.navigationBarHidden) {
            top = [UIApplication sharedApplication].statusBarFrame.size.height;
        } else {
            top = self.navigationController.navigationBar.frame.size.height + [UIApplication sharedApplication].statusBarFrame.size.height;
        }
    }
    
    //State image
    CGFloat stateImgViewWidth = 32;
    _stateImgView.frame = CGRectMake((size.width-stateImgViewWidth)/2, CGRectGetMaxY(_roundViewFrame) + 104, stateImgViewWidth, stateImgViewWidth);
    
    //Time label
    CGFloat timeLabelCenterY = 0;
    CGSize timeLabelSize = CGSizeMake(40, 24);
    if (_backBtn) {
        timeLabelCenterY = _backBtn.center.y;
    } else {
        timeLabelCenterY = top + marginTop + timeLabelSize.height/2;
    }
    _timeLabel.bounds = CGRectMake(0, 0, timeLabelSize.width, timeLabelSize.height);
    _timeLabel.center = CGPointMake(size.width - marginLeft - 20, timeLabelCenterY);
    _timeLabel.layer.cornerRadius = 12;
    _timeLabel.layer.borderWidth = 1;
    _timeLabel.layer.borderColor = _timeLabel.textColor.CGColor;
    
    _voiceBtn.bounds = CGRectMake(0, 0, 32, 32);
    _voiceBtn.center = CGPointMake(_timeLabel.center.x, CGRectGetMaxY(_timeLabel.frame)+20);
}

- (void)livenessWrapViewWillLayout:(AAILivenessWrapView *)wrapView
{
    /*
    // Custom preview frame and avatar preview area
    CGRect rect = self.view.frame;
    wrapView.currPreviewFrame = CGRectMake(0, 0, rect.size.width, rect.size.height);
    
    CGFloat marginLeft = 20;
    CGFloat marginTop = 50;
    CGFloat avatarPreviewWidth = (rect.size.width - 2 * marginLeft);
    wrapView.currAvatarPreviewArea = CGRectMake(marginLeft, marginTop, avatarPreviewWidth, avatarPreviewWidth);
    */
    
    /*
    // Hide the default viewfinder
    wrapView.roundBorderView.hidden = YES;
    // Configure your own viewfinder view or layer
     */
}

- (void)willPlayAudio:(NSString *)audioName
{
    if (_playAudio) {
        [_util playAudio:audioName lprojName:_language];
    }
}

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        _showHUD = YES;
        _detectPhonePortraitDirection = NO;
        _playAudio = NO;
        _showAnimationImg = YES;
        _recordUserGiveUp = NO;
        _ellipseFrame = CGRectZero;
        _prepareTimeoutInterval = [AAILivenessSDK additionalConfig].prepareTimeoutInterval;
        _hudBrandColor = [UIColor aai_brandColor];
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    _pre_key = nil;
    _isReady = NO;
    _isRequestingAuth = NO;
    _requestAuthSucceed = NO;
    _requestAuthComplete = NO;
    _requestAuthCached = NO;
    _isFlowFinished = NO;
    
    _util = [[AAILivenessUtil alloc] init];
    
    // Config model file path
    NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"AAIModel.bundle" ofType:NULL];
    if (modelPath == NULL) {
        modelPath = [AAIBundle modelBundlePath];
    }
    if (modelPath == NULL) {
        NSLog(@"ERROR: AAIModel.bundle Not Found!");
        return;
    }
    [AAILivenessSDK configModelBundlePath: modelPath];
    
    UIView *sv = self.view;
    AAILivenessWrapView *wrapView = [[AAILivenessWrapView alloc] init];
    [sv addSubview:wrapView];
    wrapView.wrapDelegate = self;
    _wrapView = wrapView;
    [_wrapView.youme margin:[AAIMargin zero]];
    
    [self livenessWrapViewDidLoad:wrapView];
    
    [self loadAdditionalUI];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(restartDetection) name:@"kAAIRestart" object:nil];
    [[AVAudioSession sharedInstance] addObserver:self forKeyPath:@"outputVolume" options:NSKeyValueObservingOptionNew context:nil];
    
    [_util saveCurrBrightness];
    
    [self startCamera];
}

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    
    // Do not modify begin
    [self livenessWrapViewWillLayout:_wrapView];
    [_wrapView setNeedsLayout];
    [_wrapView layoutIfNeeded];
    
    CGRect tmpFrame = _wrapView.roundBorderView.frame;
    _roundViewFrame = [_wrapView.roundBorderView.superview convertRect:tmpFrame toView:self.view];
    // Do not modify end
    
    [self layoutAdditionalUI];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [_util graduallySetBrightness:1];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    [_util graduallyResumeBrightness];
}

- (void)updateStateLabel:(NSString * _Nullable)state key:(NSString * _Nullable)key
{
    CGRect frame = _ellipseFrame;
    CGFloat w = _roundViewFrame.size.width;
    
    CGFloat marginTop = 8;
    if (state) {
        _stateLabel.text = state;
        CGSize size = [_stateLabel sizeThatFits:CGSizeMake(w, 1000)];
        CGFloat x = (self.view.frame.size.width - size.width)/2;
        if (x < 0) {
            x = -x;
        }
        if (CGRectEqualToRect(frame, CGRectZero) && [key isEqualToString:@"pls_hold_phone_v"]) {
            frame = _roundViewFrame;
        }
        _stateLabel.frame = CGRectMake(x, CGRectGetMinY(frame) - size.height - marginTop, size.width, size.height);
    } else {
        _stateLabel.text = nil;
        _stateLabel.frame = CGRectMake(frame.origin.x, CGRectGetMinY(frame) - 30 - marginTop, frame.size.width, 30);
    }
}

- (void)showImgWithType:(AAIDetectionType)detectionType
{
    if (!_showAnimationImg) {
        return;
    }
    
    switch (detectionType) {
        case AAIDetectionTypeBlink:
        case AAIDetectionTypeMouth:
        case AAIDetectionTypePosYaw: {
            [_stateImgView stopAnimating];
            NSArray *array = [AAILivenessUtil stateImgWithType:detectionType];
            _stateImgView.animationImages = array;
            _stateImgView.animationDuration = array.count * 1/5.f;
            [_stateImgView startAnimating];
            break;
        }
        default:
            break;
    }
}

- (void)setPrepareTimeoutInterval:(NSInteger)prepareTimeoutInterval
{
    _prepareTimeoutInterval = prepareTimeoutInterval;
    [AAILivenessSDK additionalConfig].prepareTimeoutInterval = prepareTimeoutInterval;
}

- (void)setTimeoutDurationOf3DMode:(NSInteger)timeoutDurationOf3DMode
{
    [AAILivenessSDK additionalConfig].prepareTimeoutInterval = timeoutDurationOf3DMode;
}

- (NSInteger)timeoutDurationOf3DMode
{
    return [AAILivenessSDK additionalConfig].prepareTimeoutInterval;
}

- (CGRect)roundViewFrame
{
    return _roundViewFrame;
}

- (CGRect)ellipseFrame
{
    return _ellipseFrame;
}

#pragma mark - Network

- (BOOL)isRequestingAuth
{
    return _isRequestingAuth;
}

- (BOOL)requestAuthSucceed
{
    return _requestAuthSucceed;
}

- (NSDictionary *)parseRequestError:(NSError *)error
{
    NSString *transactionId = @"";
    NSString *rawCode = @"";
    if (error.userInfo) {
        transactionId = error.userInfo[@"transactionId"];
        rawCode = error.userInfo[@"code"];
    }
    if (!transactionId) {
        transactionId = @"";
    }
    if (!rawCode) {
        rawCode = @"";
    }
    
    NSDictionary *errorInfo = @{
        @"message": error.localizedDescription,
        @"code": @(error.code),
        @"rawCode": rawCode,
        @"transactionId": transactionId,
        @"eventId": [_wrapView latestEventId]
    };
    return errorInfo;
}

#pragma mark - UserAction

- (void)startCamera
{
    __weak typeof(self) weakSelf = self;
    [_wrapView checkCameraPermissionWithCompletionBlk:^(BOOL authed) {
        if (!weakSelf) return;
        
        if (weakSelf.cameraPermissionDeniedBlk) {
            weakSelf.cameraPermissionDeniedBlk(weakSelf);
        }
        //Alert no permission
        if (weakSelf.showHUD) {
            [AAIHUD showMsg:[AAILivenessUtil localStrForKey:@"no_camera_permission" lprojName:weakSelf.language] onView:weakSelf.view duration:1.5];
        }
    }];
}

- (void)requestAuth
{
    _isRequestingAuth = YES;
    _isReady = NO;
    _timeLabel.hidden = YES;
    
    __weak typeof(self) weakSelf = self;
    NSTimeInterval authStartTime = [[NSDate date] timeIntervalSince1970];
    [_wrapView startAuthWithCompletionBlk:^(NSError * _Nullable error) {
        
        // Dismiss HUD
        __strong AAILivenessViewController *strongSelf = weakSelf;
        if (strongSelf) {
            strongSelf.isRequestingAuth = NO;
            strongSelf.requestAuthComplete = YES;
            strongSelf.authRequestCostTime = [[NSDate date] timeIntervalSince1970] - authStartTime;

            if (error) {
                strongSelf.requestAuthSucceed = NO;
            } else {
                strongSelf.requestAuthCached = YES;
                strongSelf.requestAuthSucceed = YES;
            }
            
        }
        
    }];
}

- (void)tapVoiceBtnAction:(UIButton *)btn
{
    btn.selected = !btn.selected;
    if (btn.selected) {
        //Close
        [_util configVolume:0];
    } else {
        //Open
        [_util configVolume:0.5];
    }
}

- (void)tapBackBtnAction_
{
    if (_recordUserGiveUp && _isFlowFinished == NO) {
        [_wrapView stopRunningWithReason:@"user_give_up"];
        [self detectionDidFailed:@{@"key": @"user_give_up", @"state": @"User gives up", @"eventId": [_wrapView latestEventId]}];
    } else {
        [self tapBackBtnAction];
    }
}

- (void)tapBackBtnAction
{
    [self closeSDKPage];
}

- (void)closeSDKPage
{
    UINavigationController *navc = self.navigationController;
    
    if (navc && [navc.viewControllers containsObject:self]) {
        if (navc.viewControllers.firstObject == self) {
            [navc.presentingViewController dismissViewControllerAnimated:YES completion:nil];
        } else {
            [navc popViewControllerAnimated:YES];
        }
    } else {
        [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
    }
}

- (void)resetViewState
{
    if (_stateLabel) {
        _stateLabel.text = nil;
    }
    _stateImgView.animationImages = nil;
    _stateImgView.image = nil;
    _isReady = NO;
    _timeLabel.hidden = YES;
    _voiceBtn.hidden = YES;
    _pre_key = nil;
}

- (void)resetAuthState
{
    _requestAuthCached = NO;
    _requestAuthSucceed = NO;
    _requestAuthComplete = NO;
    _isRequestingAuth = NO;
}

- (void)restartDetection
{
    [self resetViewState];
    [_wrapView resetUI];
    _hasPortraitDirection = NO;
    _requestAuthComplete = NO;
    _isFlowFinished = NO;
    
    [self startCamera];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"outputVolume"]) {
        float volume = [change[NSKeyValueChangeNewKey] floatValue];
        [_util configPlayerVolume:volume];
        if (volume == 0) {
            if (_voiceBtn.selected == NO) {
                _voiceBtn.selected = YES;
            }
        } else {
            if (_voiceBtn.selected == YES) {
                _voiceBtn.selected = NO;
            }
        }
    }
}

#pragma mark - WrapViewDelegate

- (void)onDetectionReady:(AAIDetectionType)detectionType
{
    _isReady = YES;
    _timeLabel.hidden = NO;
    
    NSString *key = nil;
    if (detectionType == AAIDetectionTypeBlink) {
        key = @"pls_blink";
        [self willPlayAudio:@"action_blink.mp3"];
    } else if (detectionType == AAIDetectionTypeMouth) {
        key = @"pls_open_mouth";
        [self willPlayAudio:@"action_open_mouth.mp3"];
    } else if (detectionType == AAIDetectionTypePosYaw) {
        key = @"pls_turn_head";
        [self willPlayAudio:@"action_turn_head.mp3"];
    }
    
    if (key) {
        NSString *state = [AAILivenessUtil localStrForKey:key lprojName:_language];
        _stateLabel.text = state;
        [self showImgWithType:detectionType];
        
        // Detection ready callback
        if (self.detectionReadyBlk) {
            self.detectionReadyBlk(self, detectionType, @{@"key": key, @"state": state});
        }
    }
}

- (void)onDetectionFailed:(AAIDetectionResult)detectionResult forDetectionType:(AAIDetectionType)detectionType
{
    [self willPlayAudio:@"detection_failed.mp3"];
    [AAILocalizationUtil stopMonitor];
    
    //Reset
    _pre_key = nil;
    
    NSString *key = nil;
    switch (detectionResult) {
        case AAIDetectionResultTimeout:
            if (detectionType == AAIDetectionTypePrepare) {
                key = @"fail_reason_prepare_timeout";
            } else {
                key = @"fail_reason_timeout";
            }
            break;
        case AAIDetectionResultErrorMutipleFaces:
            key = @"fail_reason_muti_face";
            break;
        case AAIDetectionResultErrorFaceMissing: {
            switch (detectionType) {
                case AAIDetectionTypePosYaw:
                    key = @"fail_reason_facemiss_pos_yaw";
                    break;
                default:
                    key = @"fail_reason_facemiss_blink_mouth";
                    break;
            }
            break;
        }
        case AAIDetectionResultErrorMuchMotion:
            key = @"fail_reason_much_action";
            break;
        default:
            break;
    }
    
    //Show result page
    if (key) {
        NSString *state = [AAILivenessUtil localStrForKey:key lprojName:_language];
        [self updateStateLabel:state key:key];
        
        [_stateImgView stopAnimating];
        
        // Detection failed callback
        [self detectionDidFailed: @{@"key": key, @"state": state, @"eventId": [_wrapView latestEventId]}];
    }
}

- (void)detectionDidFailed:(NSDictionary *)errorInfo
{
    // Avoid repeated call
    if (_isFlowFinished) {
        return;
    }
    _isFlowFinished = YES;
    
    [self didStopDetection];
    
    // Reset
    [self resetAuthState];
    
    // Detection failed callback
    if (self.detectionFailedBlk) {
        self.detectionFailedBlk(self, errorInfo);
    } else {
        NSString *resultState = errorInfo[@"key"];
        if (resultState == nil) {
            resultState = errorInfo[@"message"];
        } else {
            if ([resultState isEqualToString:@"user_give_up"]) {
                // Close SDK page
                [self closeSDKPage];
                return;
            }
        }
        AAILivenessResultViewController *resultVC = [[AAILivenessResultViewController alloc] initWithResult:NO resultState:resultState];
        resultVC.language = _language;
        [self.navigationController pushViewController:resultVC animated:YES];
    }
}

- (void)onOpenCameraFailed:(NSError *)error
{
    NSDictionary *errorInfo = @{
        @"key": @"device_not_support",
        @"state": error.localizedDescription,
        @"eventId": [_wrapView latestEventId]
    };
    if (self.detectionFailedBlk) {
        self.detectionFailedBlk(self, errorInfo);
    } else {
        AAILivenessResultViewController *resultVC = [[AAILivenessResultViewController alloc] initWithResult:NO resultState:errorInfo[@"key"]];
        resultVC.language = _language;
        [self.navigationController pushViewController:resultVC animated:YES];
    }
}

- (void)onBeforeStartDetection
{
    [self beforeStartDetection];
}

- (BOOL)shouldDetect
{
    if (_hasPortraitDirection == NO) {
        if (_detectPhonePortraitDirection) {
            _hasPortraitDirection = [AAILocalizationUtil isPortraitDirection];
        } else {
            _hasPortraitDirection = YES;
        }
        
        if (_hasPortraitDirection) {
            if (_requestAuthCached == NO && _isRequestingAuth == NO && _requestAuthComplete == NO) {
                dispatch_sync(dispatch_get_main_queue(), ^{
                    if (_requestAuthCached == NO && _isRequestingAuth == NO && _requestAuthComplete == NO) {
                        [self requestAuth];
                    }
                });
            }
            return _requestAuthCached;
        } else {
            dispatch_async(dispatch_get_main_queue(), ^{
                if (self->_isReady == NO) {
                    self->_timeLabel.hidden = YES;
                    self->_voiceBtn.hidden = YES;
                    self->_stateImgView.animationImages = nil;
                }
                NSString *state = [AAILivenessUtil localStrForKey:@"pls_hold_phone_v" lprojName:self.language];
                [self updateStateLabel:state key:@"pls_hold_phone_v"];
                self->_pre_key = @"pls_hold_phone_v";
            });
        }

        return NO;
    } else {
        if (_requestAuthCached == NO && _isRequestingAuth == NO  && _requestAuthComplete == NO) {
            dispatch_sync(dispatch_get_main_queue(), ^{
                if (_requestAuthCached == NO && _isRequestingAuth == NO  && _requestAuthComplete == NO) {
                    [self updateStateLabel:nil key:nil];
                    [self requestAuth];
                }
            });
        }
        return _requestAuthCached;
    }
    return YES;
}

- (void)onOriginalFrameReceived:(AAILivenessFrame *)frame
{
    [self didReceivedOriginFrame:frame];
}

- (void)onFrameDetected:(AAIDetectionResult)result status:(AAIActionStatus)status forDetectionType:(AAIDetectionType)detectionType
{
    // Avoid repeated call
    if (_isFlowFinished) {
        return;
    }
    
    NSString *key = nil;
    if (_isReady == NO && _detectPhonePortraitDirection && [AAILocalizationUtil isPortraitDirection] == NO) {
        key = @"pls_hold_phone_v";
    } else {
        switch (result) {
            case AAIDetectionResultFaceMissing:
                key = @"no_face";
                break;
            case AAIDetectionResultFaceLarge:
                key = @"move_further";
                break;
            case AAIDetectionResultFaceSmall:
                key = @"move_closer";
                break;
            case AAIDetectionResultFaceNotCenter:
                key = @"move_center";
                break;
            case AAIDetectionResultFaceNotFrontal:
                key = @"frontal";
                break;
            case AAIDetectionResultFaceNotStill:
            case AAIDetectionResultFaceCapture:
                key = @"stay_still";
                break;
            case AAIDetectionResultFaceInAction: {
                if (detectionType == AAIDetectionTypeBlink) {
                    key = @"pls_blink";
                } else if (detectionType == AAIDetectionTypePosYaw) {
                    key = @"pls_turn_head";
                } else if (detectionType == AAIDetectionTypeMouth) {
                    key = @"pls_open_mouth";
                }
            }
                break;
            case AAIDetectionResultWarnMouthOcclusion: {
                key = @"face_occ";
                break;
            }
            case AAIDetectionResultWarnEyeOcclusion: {
                key = @"pls_open_eye";
                break;
            }
            case AAIDetectionResultWarnWeakLight: {
                key = @"low_light";
                break;
            }
            case AAIDetectionResultWarnTooLight: {
                key = @"high_light";
                break;
            }
            case AAIDetectionResultWarnFaceBiasRight: {
                key = @"face_move_left";
                break;
            }
            case AAIDetectionResultWarnFaceBiasLeft: {
                key = @"face_move_right";
                break;
            }
            case AAIDetectionResultWarnFaceBiasBottom: {
                key = @"face_move_upper";
                break;
            }
            case AAIDetectionResultWarnFaceBiasUp: {
                key = @"face_move_down";
                break;
            }
            case AAIDetectionResultWarnPleaseBlink: {
                key = @"pls_blink";
                break;
            }
            case AAIDetectionResultWarnMutipleFaces: {
                key = @"warn_muti_face";
            }
            default:
                break;
        }
    }
    
    if (key) {
        if ([key isEqualToString:_pre_key]) {
            return;
        }
        _pre_key = key;
        
        NSString *state = [AAILivenessUtil localStrForKey:key lprojName:_language];
        [self updateStateLabel:state key:key];
        
        // Frame detected callback
        if (self.frameDetectedBlk) {
            self.frameDetectedBlk(self, detectionType, status, result, @{@"key": key, @"state": state});
        }
    }
}

- (void)onDetectionTypeChanged:(AAIDetectionType)toDetectionType
{
    // Avoid repeated call
    if (_isFlowFinished) {
        return;
    }
    
    NSString *key = nil;
    if (toDetectionType == AAIDetectionTypeBlink) {
        key = @"pls_blink";
        [self willPlayAudio:@"action_blink.mp3"];
    } else if (toDetectionType == AAIDetectionTypeMouth) {
        key = @"pls_open_mouth";
        [self willPlayAudio:@"action_open_mouth.mp3"];
    } else if (toDetectionType == AAIDetectionTypePosYaw) {
        key = @"pls_turn_head";
        [self willPlayAudio:@"action_turn_head.mp3"];
    }
    
    if (key) {
        NSString *state = [AAILivenessUtil localStrForKey:key lprojName:_language];
        [self updateStateLabel:state key:key];
        [self showImgWithType:toDetectionType];
        
        // Detection type changed callback
        if (self.detectionTypeChangedBlk) {
            self.detectionTypeChangedBlk(self, toDetectionType, @{@"key": key, @"state": state});
        }
    }
}

- (void)onDetectionSuccess
{
    // Set the ellipse border color to the same as the highlight color when the detection is successful.
    [AAILivenessSDK.additionalConfig setEllipseBorderCol3D: [AAILivenessSDK additionalConfig].ellipseBorderCol3D];
    
    if (_showAnimationImg) {
        _stateImgView.image = [AAILivenessUtil imgWithName:@"complete"];
    }
}

- (void)onDetectionComplete:(AAILivenessResult *)resultInfo
{
    // Avoid repeated call
    if (_isFlowFinished) {
        return;
    }
    _isFlowFinished = YES;
    
    [self didStopDetection];
    
    [self willPlayAudio:@"detection_success.mp3"];
    [AAILocalizationUtil stopMonitor];
    NSString *state = [AAILivenessUtil localStrForKey:@"detection_success" lprojName:_language];
    [self updateStateLabel:state key:@"detection_success"];
    [_stateImgView stopAnimating];
    _pre_key = nil;
    
    // Reset
    [self resetAuthState];
    
    // Detection success and complete callback
    if (self.detectionSuccessBlk) {
        self.detectionSuccessBlk(self, resultInfo);
    } else {
        AAILivenessResultViewController *resultVC = [[AAILivenessResultViewController alloc] initWithResultInfo:@{}];
        resultVC.language = _language;
        [self.navigationController pushViewController:resultVC animated:YES];
    }
}

- (void)onDetectionRemainingTime:(NSTimeInterval)remainingTime forDetectionType:(AAIDetectionType)detectionType
{
    // Avoid repeated call
    if (_isFlowFinished) {
        return;
    }
    
    if (_isReady) {
        _timeLabel.hidden = NO;
        _voiceBtn.hidden = NO;
        _timeLabel.text = [NSString stringWithFormat:@"%.f S", remainingTime];
        
        // Detection remaining time callback
        if (self.detectionRemainingTimeBlk) {
            self.detectionRemainingTimeBlk(self, detectionType, remainingTime);
        }
    }
}

- (void)livenessView:(AAILivenessWrapView *)param ellipseWillTransition:(AAIEllipseTrasitionContext * _Nonnull)context
{
    _ellipseFrame = context.toFrame;
    
    [self ellipseWillTransition:context];
}

- (void)livenessViewBeginRequest:(AAILivenessWrapView * _Nonnull)param
{
    // Show HUD
    if (_showHUD) {
        NSString *msg = [AAILivenessUtil localStrForKey:@"loading" lprojName:_language];
        AAILoadingHud *hud = [[AAILoadingHud alloc] initWithText:msg brandColor:_hudBrandColor];
        [hud showInView:self.view];
        
        // We place the _backBtn in front of the HUD to allow users to
        // tap the back button to abort if loading takes too long.
        if (self.navigationController.isNavigationBarHidden) {
            [self.view bringSubviewToFront:_backBtn];
        }
    }
    
    [self updateStateLabel:nil key:nil];
    [_stateImgView stopAnimating];
    
    // Begin request callback
    if (self.beginRequestBlk) {
        self.beginRequestBlk(self);
    }
}

- (void)livenessView:(AAILivenessWrapView *)param endRequest:(NSError * _Nullable)error
{
    // Dismiss HUD
    if (_showHUD) {
        [AAILoadingHud dismissInView:self.view];
    }
    
    // End request callback
    NSDictionary *errorInfo = nil;
    if (error) {
        errorInfo = [self parseRequestError:error];
    }
    
    if (self.endRequestBlk) {
        self.endRequestBlk(self, errorInfo);
    }
    
    // Detection failed callback
    if (error) {
        [self detectionDidFailed: errorInfo];
    }
}

#pragma mark -
- (void)beforeStartDetection
{
    // Subclass can override this method
}

- (void)didReceivedOriginFrame:(AAILivenessFrame *)originFrame
{
    // Subclass can override this method
}

- (void)ellipseWillTransition:(AAIEllipseTrasitionContext *)context
{
    if (!CGRectIsEmpty(context.fromFrame) && !CGRectIsEmpty(context.toFrame)) {
        CGFloat offsetY = context.toFrame.origin.y - context.fromFrame.origin.y;
        
        CGRect toFrame = _stateLabel.frame;
        toFrame.origin.y += offsetY;
        
        [UIView animateWithDuration:context.animationDuration delay:0 options:context.animationOptions animations:^{
            self->_stateLabel.frame = toFrame;
        } completion:nil];
    }
}

- (void)didStopDetection
{
    // Subclass can override this method
}

#pragma mark -

- (void)dealloc
{
    //If `viewDidLoad` method not called, we do nothing.
    if (_util != nil) {
        [AAILocalizationUtil stopMonitor];
        [_util removeVolumeView];
        
        [[NSNotificationCenter defaultCenter] removeObserver:self name:@"kAAIRestart" object:nil];
        [[AVAudioSession sharedInstance] removeObserver:self forKeyPath:@"outputVolume"];
    }
}

@end
