//
//  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 "UIView+AAILD.h"
#import "UIColor+AAILD.h"

#define AAI_LD_UI_ENABLE_LOG 0
#if AAI_LD_UI_ENABLE_LOG == 1
#define AAI_LD_LOG(xx, ...)  NSLog(xx, ##__VA_ARGS__)
#else
#define AAI_LD_LOG(xx, ...)
#endif

@interface AAILDDefaultOrientationTransitionCtx : NSObject<AAIOrientationTransitionContext>
{
    CGSize _toSize;
    UIDeviceOrientation _toOrientation;
}
@end
@implementation AAILDDefaultOrientationTransitionCtx

- (CGSize)toSize { return _toSize; }

- (UIDeviceOrientation)toOrientation { return _toOrientation; }

- (void)setToSize:(CGSize)toSize { _toSize = toSize; }

- (void)setToOrientation:(UIDeviceOrientation)toOrientation { _toOrientation = toOrientation; }

@end

@interface AAILivenessViewController ()<AAILivenessWrapDelegate>
{
    NSString *_pre_key;
    
    BOOL _isReady;
    BOOL _isFlowFinished;
    
    CGRect _ellipseFrame;
    
    AAILivenessConfig *_livenessConfig;
    
    double _timeoutIntervalInSecForAction;
    AAILivenessStage _latestLivenessStage;
    AAILivenessStage _preLivenessStage;
}
@property(nonatomic) NSTimeInterval prepareStartTime;
@end

@implementation AAILivenessViewController

- (void)livenessWrapViewDidLoad:(AAILivenessWrapView *)wrapView
{
    /*
    // 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.accessibilityIdentifier = @"AAIDefaultBackBtn";
    [backBtn setImage:[AAILivenessUtil imgWithName:@"arrow_back"] forState:UIControlStateNormal];
    [backBtn addTarget:self action:@selector(tapBackBtnAction_) forControlEvents:UIControlEventTouchUpInside];
    
    if (self.navigationController.isNavigationBarHidden) {
        [sv addSubview:backBtn];
        [backBtn aaild_safeMarginToView:sv t:@0 l:@8 b:nil r:nil];
        [backBtn aaild_width:@36 height:@36];
        _backBtn = backBtn;
    } else {
        self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:backBtn];
    }
    
    //Detect state label
    UILabel *stateLabel = [[UILabel alloc] init];
    stateLabel.font = self.font ? [self.font fontWithSize:20] : [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"] forState:UIControlStateNormal];
    [voiceBtn setImage:[AAILivenessUtil imgWithName:@"liveness_close_voice"] forState:UIControlStateSelected];
    [sv addSubview:voiceBtn];
    [voiceBtn addTarget:self action:@selector(tapVoiceBtnAction:) forControlEvents:UIControlEventTouchUpInside];
    
    if ([AAILivenessUtil isSilent]) {
        voiceBtn.selected = YES;
    }
    
    _voiceBtn = voiceBtn;
    
    //Timeout interval label
    _timeLabel = [[UILabel alloc] init];
    _timeLabel.font = self.font ? [self.font fontWithSize:14] : [UIFont systemFontOfSize:14];
    _timeLabel.textColor = [UIColor colorWithRed:(0x36/255.f) green:(0x36/255.f) blue:(0x36/255.f) alpha:1];
    _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;
        }
    }
    
    // Update Time label frame
    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;
        _playAudio = NO;
        _showAnimationImg = YES;
        _recordUserGiveUp = NO;
        _ellipseFrame = CGRectZero;

        _hudBrandColor = [UIColor aaild_brandColor];
        _livenessConfig = [AAILivenessConfig defaultConfig];
        _preLivenessStage = AAILivenessStageNone;
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    _pre_key = nil;
    _isReady = NO;
    _isFlowFinished = NO;
    
    _util = [[AAILivenessUtil alloc] init];
    
    UIView *sv = self.view;
    AAILivenessWrapView *wrapView = [[AAILivenessWrapView alloc] init];
    [sv addSubview:wrapView];
    wrapView.wrapDelegate = self;
    _wrapView = wrapView;
    [_wrapView aaild_marginToView:sv t:@0 l:@0 b:@0 r:@0];
    
    [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)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
    UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation;
    AAILDDefaultOrientationTransitionCtx *orientationTransitionCtx = [[AAILDDefaultOrientationTransitionCtx alloc] init];
    [orientationTransitionCtx setToSize:size];
    [orientationTransitionCtx setToOrientation:deviceOrientation];
    [_wrapView orientationWillTransition:orientationTransitionCtx];
}

- (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;
        }
        _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)updateImgWhenStageChange:(AAILivenessStage)fromLivenessStage to:(AAILivenessStage)toLivenessStage
{
    if (!_showAnimationImg) {
        return;
    }
    
    switch (toLivenessStage) {
        case AAILivenessStageBlink:
        case AAILivenessStageMouth:
        case AAILivenessStagePosYaw: {
            NSArray<UIImage *> *array = [AAILivenessUtil stateImgWithType:toLivenessStage];
            NSCAssert(array != nil, @"Could not found image for liveness stage: %lu", toLivenessStage);
            [_stateImgView stopAnimating];
            _stateImgView.image = nil;
            _stateImgView.animationImages = array;
            _stateImgView.animationDuration = array.count * 1/5.f;
            [self updateStateImgViewFrame];
            [_stateImgView startAnimating];
            break;
        }
        default:
            break;
    }
    
    if (fromLivenessStage == AAILivenessStageDistantNear && toLivenessStage == AAILivenessStageDetectionSuccess) {
        // Pure 3D Detection success
        // Set the avatar border color to the same as the highlight color when the detection is successful.
        [_livenessConfig setHighlightAvatarBorderColor:_livenessConfig.highlightAvatarBorderColor];
        
        UIImage *image = [AAILivenessUtil imgWithName:@"complete"];
        _stateImgView.image = image;
        _stateImgView.animationImages = nil;
        [self updateStateImgViewFrame];
    }
}

- (void)updateStateImgViewFrame
{
    CGSize size = self.view.frame.size;
    CGSize imgSize = CGSizeZero;
    if (_stateImgView.image != nil) {
        imgSize = _stateImgView.image.size;
    } else if (_stateImgView.animationImages != nil) {
        imgSize = _stateImgView.animationImages.firstObject.size;
    }
    
    CGFloat margin = 40;
    CGFloat originY = 0;
    if (CGRectIsEmpty(_ellipseFrame)) {
        originY = CGRectGetMaxY(_roundViewFrame) + margin;
    } else {
        CGFloat roundViewMaxY = CGRectGetMaxY(_roundViewFrame);
        CGFloat ellipseFrameMaxY = CGRectGetMaxY(_ellipseFrame);
        originY = MAX(roundViewMaxY, ellipseFrameMaxY) + margin;
    }
    
    _stateImgView.frame = CGRectMake((size.width-imgSize.width)/2, originY, imgSize.width, imgSize.height);
}

- (CGRect)roundViewFrame
{
    return _roundViewFrame;
}

- (CGRect)ellipseFrame
{
    return _ellipseFrame;
}

- (AAILivenessStage)livenessStage
{
    return _latestLivenessStage;
}

#pragma mark - Network

- (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
{
    // Always reset state label
    [self updateStateLabel:nil key:nil];
    
    [_wrapView startRunningWithConfig:_livenessConfig];
}

- (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 stopRunning];
        _isFlowFinished = YES;
        
        NSString *giveUpKey = nil;
        AAILivenessStage currStage = _latestLivenessStage;
        if (currStage == AAILivenessStageAuth) {
            giveUpKey = @"auth_give_up";
        } else if (currStage == AAILivenessStageUploadImageData) {
            giveUpKey = @"upload_image_data_give_up";
        } else {
            giveUpKey = @"user_give_up";
        }
        NSDictionary *errorInfo = @{
            @"key": giveUpKey,
            @"state": @"User gives up",
            @"eventId": [_wrapView latestEventId],
            @"stage": @(currStage)
        };
        AAILivenessFailureResult *failedResult = [AAILivenessFailureResult resultWithErrorInfo:errorInfo];
        [self detectionDidStopWithFailure:failedResult];
    } 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;
    _preLivenessStage = AAILivenessStageNone;
    _latestLivenessStage = AAILivenessStageNone;
}

- (void)restartDetection
{
    if (_isFlowFinished == YES) {
        [self resetViewState];
        _isFlowFinished = NO;
        
        [self startCamera];
    } else {
        AAI_LD_LOG(@"[AAILivenessUI] Do not call `restartDetection` before detection completes.");
    }
}

- (void)stopDetection
{
    [_wrapView stopRunning];
    _isFlowFinished = YES;
}

- (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)onBeforeStartDetection
{
    AAI_LD_LOG(@"[AAILivenessUI] onBeforeStartDetection");
    [self beforeStartDetection];
}

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

- (void)onDetectionResultChanged:(AAIDetectionResultContext *)resultContext
{
    AAI_LD_LOG(@"[AAILivenessUI] onDetectionResultChanged: [result] %ld, [stage] %ld", resultContext.result, resultContext.livenessStage);
    AAIDetectionResult result = resultContext.result;
    // Avoid repeated call
    if (_isFlowFinished) {
        return;
    }
    
    NSString *key = nil;
    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: {
            AAILivenessStage livenessStage = resultContext.livenessStage;
            if (livenessStage == AAILivenessStageBlink) {
                key = @"pls_blink";
            } else if (livenessStage == AAILivenessStagePosYaw) {
                key = @"pls_turn_head";
            } else if (livenessStage == AAILivenessStageMouth) {
                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";
            break;
        }
        case AAIDetectionResultWarnMouthOpen: {
            key = @"warn_mouth_open";
            break;
        }
        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];
    }
}

- (void)onLivenessStageChanged:(AAILivenessStage)livenessStage
{
    AAI_LD_LOG(@"[AAILivenessUI] onLivenessStageChanged: %ld", livenessStage);
    // Avoid repeated call
    if (_isFlowFinished) {
        return;
    }
    _latestLivenessStage = livenessStage;
    
    NSString *key = nil;
    NSString *state = nil;
    switch (livenessStage) {
        case AAILivenessStageBlink: {
            key = @"pls_blink";
            [self willPlayAudio:@"action_blink.mp3"];
            break;
        }
        case AAILivenessStageMouth: {
            key = @"pls_open_mouth";
            [self willPlayAudio:@"action_open_mouth.mp3"];
            break;
        }
        case AAILivenessStagePosYaw: {
            key = @"pls_turn_head";
            [self willPlayAudio:@"action_turn_head.mp3"];
            break;
        }
        case AAILivenessStageOpenCamera: {
            // Some devices may open camera very slow, so we need to show the loading hud to
            // avoid user think the app is frozen.
            [self livenessViewBeginRequest:self.wrapView];
            break;
        }
        case AAILivenessStageViewReady: {
            // Close the loading hud when wrapView ready.
            [self livenessView:self.wrapView endRequest:nil];
            break;
        }
        default:
            break;
    }
    
    if (livenessStage == AAILivenessStageDetectionSuccess) {
        // Detection success
        [self updateImgWhenStageChange:_preLivenessStage to:livenessStage];
    } else {
        if (key) {
            state = [AAILivenessUtil localStrForKey:key lprojName:_language];
            [self updateStateLabel:state key:key];
            [self updateImgWhenStageChange:_preLivenessStage to:livenessStage];
        }
    }
    
    // Liveness stage changed callback
    if (self.livenessStageChangedBlk) {
        NSMutableDictionary *info = [[NSMutableDictionary alloc] initWithCapacity:2];
        if (key) {
            info[@"key"] = key;
        }
        if (state) {
            info[@"state"] = state;
        }
        self.livenessStageChangedBlk(self, livenessStage, info);
    }
    
    _preLivenessStage = livenessStage;
}

- (void)onFinalDetectionSuccess:(AAILivenessSuccessResult *)successResult
{
    AAI_LD_LOG(@"[AAILivenessUI] onFinalDetectionSuccess: %@", successResult);
    // Avoid repeated call
    if (_isFlowFinished) {
        return;
    }
    _isFlowFinished = YES;
    
    [self detectionDidStopWithSuccess:successResult];
}

- (void)onFinalDetectionFailure:(AAILivenessFailureResult *)failureResult
{
    AAI_LD_LOG(@"[AAILivenessUI] onFinalDetectionFailure: %@", failureResult);
    // Avoid repeated call
    if (_isFlowFinished) {
        return;
    }
    _isFlowFinished = YES;
    
    // Reset
    _pre_key = nil;
    
    NSString *errorCode = failureResult.errorCode;
    NSString *key = nil;
    if ([errorCode isEqualToString:@"ACTION_TIMEOUT"]) {
        key = @"fail_reason_timeout";
    } else if ([errorCode isEqualToString:@"MUTIPLE_FACE"]) {
        key = @"fail_reason_muti_face";
    } else if ([errorCode isEqualToString:@"FACE_MISSING"]) {
        if (failureResult.livenessStage == AAILivenessStagePosYaw) {
            key = @"fail_reason_facemiss_pos_yaw";
        } else {
            key = @"fail_reason_facemiss_blink_mouth";
        }
    } else if ([errorCode isEqualToString:@"MUCH_ACTION"]) {
        key = @"fail_reason_much_action";
    } else if ([errorCode isEqualToString:@"NO_CAMERA_PERMISSION"]) {
        key = @"no_camera_permission";
    } else if ([errorCode isEqualToString:@"DEVICE_NOT_SUPPORT"]) {
        key = @"device_not_support";
    }
    
    if (key) {
        NSString *state = [AAILivenessUtil localStrForKey:key lprojName:_language];
        [self updateStateLabel:state key:key];
        
        failureResult.rawErrorCode = key;
        failureResult.errorMsg = state;
    }
    
    // Detection failed callback
    [self detectionDidStopWithFailure:failureResult];
}

- (void)onDetectionRemainingTimeChanged:(NSInteger)remainingTime forLivenessStage:(AAILivenessStage)livenessStage
{
    AAI_LD_LOG(@"[AAILivenessUI] onDetectionRemainingTimeChanged: %ld forLivenessStage: %ld", remainingTime, livenessStage);
    // Avoid repeated call
    if (_isFlowFinished) {
        return;
    }
    
    _timeLabel.hidden = NO;
    _voiceBtn.hidden = NO;
    _timeLabel.text = [NSString stringWithFormat:@"%ld S", remainingTime];
    
    // Detection remaining time callback
    if (self.detectionRemainingTimeBlk) {
        self.detectionRemainingTimeBlk(self, livenessStage, remainingTime);
    }
}

- (void)livenessView:(AAILivenessWrapView *)param ellipseWillTransition:(AAIEllipseTrasitionContext * _Nonnull)context
{
    AAI_LD_LOG(@"[AAILivenessUI] livenessView:ellipseWillTransition: %@", NSStringFromCGRect(context.toFrame));
    _ellipseFrame = context.toFrame;
    
    [self ellipseWillTransition:context];
}

- (void)livenessViewBeginRequest:(AAILivenessWrapView * _Nonnull)param
{
    AAI_LD_LOG(@"[AAILivenessUI] livenessViewBeginRequest, latestStage: %ld", (long)_latestLivenessStage);
    // Show HUD
    if (_showHUD) {
        NSString *msg = [AAILivenessUtil localStrForKey:@"loading" lprojName:_language];
        AAILoadingHud *hud = [[AAILoadingHud alloc] initWithText:msg
                                                      brandColor:_hudBrandColor
                                                            font:self.font];
        [hud updateTitleLabelAccId:[NSString stringWithFormat:@"loading-%ld", (long)_latestLivenessStage]];
        [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
{
    AAI_LD_LOG(@"[AAILivenessUI] livenessView:endRequest: %@", 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);
    }
}

#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)detectionDidStopWithSuccess:(AAILivenessSuccessResult *)successResult
{
    [self willPlayAudio:@"detection_success.mp3"];
    [AAILocalizationUtil stopMonitor];
    [_stateImgView stopAnimating];
    
    NSString *state = [AAILivenessUtil localStrForKey:@"detection_success" lprojName:_language];
    [self updateStateLabel:state key:@"detection_success"];
    _pre_key = nil;
    
    // Detection success and complete callback
    if (self.detectionSuccessBlk) {
        self.detectionSuccessBlk(self, successResult);
    } else {
        AAILivenessResultViewController *resultVC = [[AAILivenessResultViewController alloc] initWithResultInfo:@{}];
        resultVC.language = _language;
        [self.navigationController pushViewController:resultVC animated:YES];
    }
}

- (void)detectionDidStopWithFailure:(AAILivenessFailureResult *)failureResult
{
    [self willPlayAudio:@"detection_failed.mp3"];
    [AAILocalizationUtil stopMonitor];
    [_stateImgView stopAnimating];
        
    // Detection failed callback
    if (self.detectionFailureBlk) {
        self.detectionFailureBlk(self, failureResult);
    } else {
        // AUTH_GIVE_UP, USER_GIVE_UP, UPLOAD_PICTURE_GIVE_UP
        if ([failureResult.errorCode containsString:@"GIVE_UP"]) {
            // Close SDK page
            [self closeSDKPage];
        } else {
            NSString *resultState = failureResult.errorMsg;
            AAILivenessResultViewController *resultVC = [[AAILivenessResultViewController alloc] initWithResult:NO resultState:resultState];
            resultVC.language = _language;
            [self.navigationController pushViewController:resultVC animated:YES];
        }
    }
}

#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"];
        
        [_wrapView releaseResources];
    }
}

@end
