//
//  ZYJQNDnsManager.m
//  ZYJHappyDNS
//
//  Created by bailong on 15/6/23.
//  Copyright (c) 2015年 Qiniu Cloud Storage. All rights reserved.
//

#import "ZYJQNDnsManager.h"
#import "ZYJQNDomain.h"
#import "ZYJQNHosts.h"
#import "ZYJQNIP.h"
#import "ZYJQNLruCache.h"
#import "ZYJQNNetworkInfo.h"
#import "ZYJQNRecord.h"
#import "ZYJQNResolverDelegate.h"

#include "ZYJQNGetAddrInfo.h"

const int kZYJQNDomainHijackingCode = -7001;
const int kZYJQNDomainNotOwnCode = -7002;
const int kZYJQNDomainSeverError = -7003;

@interface ZYJQNDnsManager ()

@property (nonatomic, strong) ZYJQNLruCache *cache;
@property (atomic) ZYJQNNetworkInfo *curNetwork;
@property (nonatomic) NSArray *resolvers;
@property (atomic) UInt32 resolverStatus;
@property (nonatomic) ZYJQNHosts *hosts;
@property (nonatomic, strong) id<ZYJQNIpSorter> sorter;
@end

//static inline BOOL bits_isSet(UInt32 v, int index) {
//	return (v & (1 << index)) != 0;
//}

static inline UInt32 bits_set(UInt32 v, int bitIndex) {
    return v |= (1 << bitIndex);
}

static inline UInt32 bits_leadingZeros(UInt32 x) {
    UInt32 y;
    int n = 32;
    y = x >> 16;
    if (y != 0) {
        n = n - 16;
        x = y;
    }
    y = x >> 8;
    if (y != 0) {
        n = n - 8;
        x = y;
    }
    y = x >> 4;
    if (y != 0) {
        n = n - 4;
        x = y;
    }
    y = x >> 2;
    if (y != 0) {
        n = n - 2;
        x = y;
    }
    y = x >> 1;
    if (y != 0) {
        return n - 2;
    }
    return n - x;
}

static NSMutableArray *trimCname(NSArray *records) {
    NSMutableArray *array = [[NSMutableArray alloc] init];
    for (ZYJQNRecord *r in records) {
        if (r.type == kZYJQNTypeA || r.type == kZYJQNTypeAAAA) {
            [array addObject:r];
        }
    }
    return array;
}

static NSArray *records2Ips(NSArray *records) {
    NSMutableArray *array = [[NSMutableArray alloc] init];
    for (ZYJQNRecord *r in records) {
        [array addObject:r.value];
    }
    return array;
}

@interface ZYJDummySorter : NSObject <ZYJQNIpSorter>

@end

@implementation ZYJDummySorter

//sorted already
- (NSArray *)sort:(NSArray *)ips {
    return ips;
}

@end

@implementation ZYJQNDnsManager
- (NSArray <NSString *> *)query:(NSString *)domain {
    return [self queryWithDomain:[[ZYJQNDomain alloc] init:domain]];
}

- (NSArray <ZYJQNRecord *> *)queryRecords:(NSString *)domain{
    
    if (domain == nil) {
        return nil;
    }
    if ([ZYJQNIP mayBeIpV4:domain]) {
        return [NSArray arrayWithObject:domain];
    }
    NSArray *ips = [self queryInternalWithDomain:[[ZYJQNDomain alloc] init:domain] needRecordInfo:YES];
    return [_sorter sort:ips];
}

- (NSArray <NSString *> *)queryWithDomain:(ZYJQNDomain *)domain {
    if (domain.domain == nil) {
        return nil;
    }
    if ([ZYJQNIP mayBeIpV4:domain.domain]) {
        return [NSArray arrayWithObject:domain.domain];
    }
    NSArray *ips = [self queryInternalWithDomain:domain needRecordInfo:NO];
    return [_sorter sort:ips];
}

- (NSArray *)queryInternalWithDomain:(ZYJQNDomain *)domain needRecordInfo:(BOOL)needRecordInfo {
    if (domain.hostsFirst) {
        NSArray *ret = [_hosts query:domain networkInfo:_curNetwork];
        if (ret != nil && ret.count != 0) {
            if (needRecordInfo) {
                NSMutableArray *retP = [NSMutableArray array];
                for (NSString *host in ret) {
                    ZYJQNRecord *record = [[ZYJQNRecord alloc] init:host ttl:self.defaultTtl type:kZYJQNTypeA source:ZYJQNRecordSourceUnknown];
                    [retP addObject:record];
                }
                return [retP copy];
            } else {
                return ret;
            }
        }
    }
    NSMutableArray *result;

    if ([_curNetwork isEqualToInfo:[ZYJQNNetworkInfo normal]] && [ZYJQNNetworkInfo isNetworkChanged]) {
        @synchronized(_cache) {
            [_cache removeAllObjects];
        }
        _resolverStatus = 0;
    } else {
        @synchronized(_cache) {
            result = [_cache objectForKey:domain.domain];
            if (result != nil && result.count > 1) {
                ZYJQNRecord *first = [result firstObject];
                [result removeObjectAtIndex:0];
                [result addObject:first];
            }
        }
    }
    @synchronized(_cache) {
        if (result != nil && result.count > 0) {
            ZYJQNRecord *record = [result objectAtIndex:0];
            if (![record expired:[[NSDate date] timeIntervalSince1970]]) {
                if (needRecordInfo) {
                    return result;
                } else {
                    return records2Ips(result);
                }
            }
        }
    }

    NSArray *records = nil;
    NSError *error = nil;
    int firstOk = 32 - bits_leadingZeros(_resolverStatus);
    for (int i = 0; i < _resolvers.count; i++) {
        int pos = (firstOk + i) % _resolvers.count;
        id<ZYJQNResolverDelegate> resolver = [_resolvers objectAtIndex:pos];
        ZYJQNNetworkInfo *previousNetwork = _curNetwork;
        NSString *previousIp = [ZYJQNNetworkInfo getIp];
        records = [resolver query:domain networkInfo:previousNetwork error:&error];
        if (error != nil) {
            NSError *tmp = error;
            error = nil;
            if (tmp.code == kZYJQNDomainNotOwnCode) {
                continue;
            }
            
            if (self.queryErrorHandler) {
                self.queryErrorHandler(error, domain.domain);
            }
        }

        if (records == nil || records.count == 0) {
            if (_curNetwork == previousNetwork && [previousIp isEqualToString:[ZYJQNNetworkInfo getIp]]) {
                _resolverStatus = bits_set(_resolverStatus, pos);
            }
        } else {
            NSMutableArray *ret = trimCname(records);
            if (_curNetwork == previousNetwork && [previousIp isEqualToString:[ZYJQNNetworkInfo getIp]]) {
                @synchronized(_cache) {
                    [_cache setObject:ret forKey:domain.domain];
                }
            }
            if (needRecordInfo) {
                return ret;
            } else {
                return records2Ips(ret);
            }
        }
    }

    if (!domain.hostsFirst) {
        NSArray *ret = [_hosts query:domain networkInfo:_curNetwork];
        if (needRecordInfo) {
            NSMutableArray *retP = [NSMutableArray array];
            for (NSString *host in ret) {
                ZYJQNRecord *record = [[ZYJQNRecord alloc] init:host ttl:self.defaultTtl type:kZYJQNTypeA source:ZYJQNRecordSourceUnknown];
                [retP addObject:record];
            }
            return [retP copy];
        } else {
            return ret;
        }
    }
    return nil;
}

- (instancetype)init:(NSArray *)resolvers networkInfo:(ZYJQNNetworkInfo *)netInfo {
    return [self init:resolvers networkInfo:netInfo sorter:nil];
}

- (instancetype)init:(NSArray *)resolvers networkInfo:(ZYJQNNetworkInfo *)netInfo sorter:(id<ZYJQNIpSorter>)sorter {
    if (self = [super init]) {
        _cache = [[ZYJQNLruCache alloc] init:1024];
        _curNetwork = netInfo;
        _resolvers = [[NSArray alloc] initWithArray:resolvers];
        _hosts = [[ZYJQNHosts alloc] init];
        if (sorter == nil) {
            _sorter = [[ZYJDummySorter alloc] init];
        } else {
            _sorter = sorter;
        }
    }
    return self;
}

- (void)onNetworkChange:(ZYJQNNetworkInfo *)netInfo {
    @synchronized(_cache) {
        [_cache removeAllObjects];
    }
    _curNetwork = netInfo;
}

- (instancetype)putHosts:(NSString *)domain ip:(NSString *)ip {
    [_hosts put:domain ip:ip];
    return self;
}

- (instancetype)putHosts:(NSString *)domain ip:(NSString *)ip provider:(int)provider {
    [_hosts put:domain ip:ip provider:provider];
    return self;
}

- (NSURL *)queryAndReplaceWithIP:(NSURL *)url {
    NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:YES];
    if (!urlComponents) {
        return nil;
    }

    NSString *host = urlComponents.host;
    NSArray *ips = [self query:host];

    NSURL *URL = nil;
    if (ips && ips.count > 0) {
        urlComponents.host = [ZYJQNIP ipHost:ips[0]];
    }

    URL = urlComponents.URL;
    return URL;
}

static ZYJQNGetAddrInfoCallback getAddrInfoCallback = nil;
static ZYJQN_ips_ret *dns_callback_internal(const char *host) {
    if (getAddrInfoCallback == nil) {
        return NULL;
    }
    NSString *s = [[NSString alloc] initWithUTF8String:host];
    if (s == nil) {
        return NULL;
    }
    NSArray *ips = getAddrInfoCallback(s);
    if (ips == nil) {
        return NULL;
    }
    ZYJQN_ips_ret *ret = calloc(sizeof(char *), ips.count + 1);
    for (int i = 0; i < ips.count; i++) {
        NSString *ip = ips[i];
        char *ip2 = strdup([ip cStringUsingEncoding:NSUTF8StringEncoding]);
        ret->ips[i] = ip2;
    }
    return ret;
}
static ZYJQN_ips_ret *dns_callback(const char *host) {
    ZYJQN_ips_ret *ret = dns_callback_internal(host);
    if (ret == NULL) {
        //only for compatible
        ZYJQN_ips_ret *ret = calloc(sizeof(char *), 2);
        ret->ips[0] = strdup(host);
    }
    return ret;
}

static ZYJQNIpStatusCallback ipStatusCallback = nil;
static void ip_status_callback(const char *ip, int code, int time_ms) {
    if (ipStatusCallback == nil) {
        return;
    }
    NSString *s = [[NSString alloc] initWithUTF8String:ip];
    if (s == nil) {
        return;
    }
    ipStatusCallback(s, code, time_ms);
}

+ (void)setGetAddrInfoBlock:(ZYJQNGetAddrInfoCallback)block {
    if ([ZYJQNIP isIpV6FullySupported] || ![ZYJQNIP isV6]) {
        getAddrInfoCallback = block;
        ZYJQN_set_dns_callback(dns_callback);
    }
}

+ (void)setDnsManagerForGetAddrInfo:(ZYJQNDnsManager *)dns {
    [ZYJQNDnsManager setGetAddrInfoBlock:^NSArray *(NSString *host) {
        return [dns query:host];
    }];
}

+ (void)setIpStatusCallback:(ZYJQNIpStatusCallback)block {
    ipStatusCallback = block;
    ZYJQN_set_ip_report_callback(ip_status_callback);
}

+ (BOOL)needHttpDns {
    NSTimeZone *timeZone = [NSTimeZone localTimeZone];
    NSString *tzName = [timeZone name];
    return [tzName isEqual:@"Asia/Shanghai"] || [tzName isEqual:@"Asia/Chongqing"] || [tzName isEqual:@"Asia/Harbin"] || [tzName isEqual:@"Asia/Urumqi"];
}
@end
