/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#import "WXStreamModule.h"
#import "WXSDKManager.h"
#import "WXUtility.h"
#import "WXHandlerFactory.h"
#import "WXNetworkProtocol.h"
#import "WXURLRewriteProtocol.h"
#import "WXResourceLoader.h"
#import "WXSDKEngine.h"
#import "WXSDKInstance_performance.h"
#import "WXMonitor.h"

@implementation WXStreamModule

@synthesize weexInstance;

WX_EXPORT_METHOD(@selector(sendHttp:callback:))
WX_EXPORT_METHOD(@selector(fetch:callback:progressCallback:))
WX_EXPORT_METHOD(@selector(fetchWithArrayBuffer:options:callback:progressCallback:))

- (void)fetch:(NSDictionary *)options callback:(WXModuleKeepAliveCallback)callback progressCallback:(WXModuleKeepAliveCallback)progressCallback
{
    __block NSInteger received = 0;
    __block NSHTTPURLResponse *httpResponse = nil;
    __block NSMutableDictionary * callbackRsp =[[NSMutableDictionary alloc] init];
    __block NSString *statusText = @"ERR_CONNECT_FAILED";
    
    //build stream request
    WXResourceRequest * request = [self _buildRequestWithOptions:options callbackRsp:callbackRsp];
    if (!request) {
        if (callback) {
            callback(callbackRsp, NO);
        }
        // failed with some invaild inputs
        return ;
    }
    
    // notify to start request state
    if (progressCallback) {
        progressCallback(callbackRsp, TRUE);
    }
    
    WXResourceLoader *loader = [[WXResourceLoader alloc] initWithRequest:request];
    __weak typeof(self) weakSelf = self;
    loader.onResponseReceived = ^(const WXResourceResponse *response) {
        httpResponse = (NSHTTPURLResponse*)response;
        if (weakSelf) {
            [callbackRsp setObject:@{ @"HEADERS_RECEIVED" : @2 } forKey:@"readyState"];
            [callbackRsp setObject:[NSNumber numberWithInteger:httpResponse.statusCode] forKey:@"status"];
            [callbackRsp setObject:httpResponse.allHeaderFields forKey:@"headers"];
            statusText = [WXStreamModule _getStatusText:httpResponse.statusCode];
            [callbackRsp setObject:statusText forKey:@"statusText"];
            [callbackRsp setObject:[NSNumber numberWithInteger:received] forKey:@"length"];
            if (progressCallback) {
                progressCallback(callbackRsp, TRUE);
            }
        }
    };
    
    loader.onDataReceived = ^(NSData *data) {
        if (weakSelf) {
            [callbackRsp setObject:@{ @"LOADING" : @3 } forKey:@"readyState"];
            received += [data length];
            [callbackRsp setObject:[NSNumber numberWithInteger:received] forKey:@"length"];
            if (progressCallback) {
                progressCallback(callbackRsp, TRUE);
            }
        }
    };
    
    loader.onFinished = ^(const WXResourceResponse * response, NSData *data) {
        if (weakSelf && callback) {
             [weakSelf _loadFinishWithResponse:[response copy] data:data callbackRsp:callbackRsp];
             callback(callbackRsp, NO);
        }
    };
    
    loader.onFailed = ^(NSError *error) {
        if (weakSelf && callback) {
            [weakSelf _loadFailedWithError:error callbackRsp:callbackRsp];
            callback(callbackRsp, NO);
        }
    };
    
    [loader start];
}

- (void)fetchWithArrayBuffer:(id)arrayBuffer options:(NSDictionary *)options callback:(WXModuleKeepAliveCallback)callback progressCallback:(WXModuleKeepAliveCallback)progressCallback
{
    NSMutableDictionary *newOptions = [options mutableCopy];
    if([arrayBuffer isKindOfClass:[NSDictionary class]]){
        NSData *sendData = [WXUtility base64DictToData:arrayBuffer];
        if(sendData){
            [newOptions setObject:sendData forKey:@"body"];
        }
    }
    [self fetch:newOptions callback:callback progressCallback:progressCallback];
}

- (WXResourceRequest*)_buildRequestWithOptions:(NSDictionary*)options callbackRsp:(NSMutableDictionary*)callbackRsp
{
    // parse request url
    NSString *urlStr = [options objectForKey:@"url"];
    if (![urlStr isKindOfClass:[NSString class]]) {
        if (callbackRsp) {
            [callbackRsp setObject:@(-1) forKey:@"status"];
            [callbackRsp setObject:@NO forKey:@"ok"];
        }
        return nil;
    }
    
    NSString *newURL = [urlStr copy];
    WX_REWRITE_URL(urlStr, WXResourceTypeLink, self.weexInstance)
    urlStr = newURL;
    
    if (!options || [WXUtility isBlankString:urlStr]) {
        [callbackRsp setObject:@(-1) forKey:@"status"];
        [callbackRsp setObject:@NO forKey:@"ok"];
        
        return nil;
    }
    
    if (self.weexInstance && !weexInstance.isJSCreateFinish) {
        self.weexInstance.performance.fsReqNetNum++;
    }
    
    WXResourceRequest *request = [WXResourceRequest requestWithURL:[NSURL URLWithString:urlStr] resourceType:WXResourceTypeOthers referrer:nil cachePolicy:NSURLRequestUseProtocolCachePolicy];
    
    // parse http method
    NSString *method = [options objectForKey:@"method"];
    if ([WXUtility isBlankString:method]) {
        // default HTTP method is GET
        method = @"GET";
    }
    request.HTTPMethod = method;
    
    //parse responseType
    NSString *responseType = [options objectForKey:@"type"];
    if ([responseType isKindOfClass:[NSString class]]) {
        [callbackRsp setObject:responseType? responseType.lowercaseString:@"" forKey:@"responseType"];
    }
    
    //parse timeout
    if ([options valueForKey:@"timeout"]){
        //the time unit is ms
        [request setTimeoutInterval:([[options valueForKey:@"timeout"] floatValue])/1000];
    }
    
    //install client userAgent
    request.userAgent = [WXUtility userAgent];
    
    // parse custom http headers
    NSDictionary *headers = [options objectForKey:@"headers"];
    for (NSString *header in headers) {
        NSString *value = [headers objectForKey:header];
        [request setValue:value forHTTPHeaderField:header];
    }
    
    //parse custom body
    if ([options objectForKey:@"body"]) {
        NSData * body = nil;
        if ([[options objectForKey:@"body"] isKindOfClass:[NSString class]]) {
            // compatible with the string body
            body = [[options objectForKey:@"body"] dataUsingEncoding:NSUTF8StringEncoding];
        }
        if ([[options objectForKey:@"body"] isKindOfClass:[NSDictionary class]]) {
            body = [[WXUtility JSONString:[options objectForKey:@"body"]] dataUsingEncoding:NSUTF8StringEncoding];
        }
        if (!body) {
            [callbackRsp setObject:@(-1) forKey:@"status"];
            [callbackRsp setObject:@NO forKey:@"ok"];
            return nil;
        }
        
        [request setHTTPBody:body];
    }
    
    [callbackRsp setObject:@{ @"OPENED": @1 } forKey:@"readyState"];
    
    return request;
}

- (void)_loadFailedWithError:(NSError*)error callbackRsp:(NSMutableDictionary*)callbackRsp
{
    [callbackRsp removeObjectForKey:@"readyState"];
    [callbackRsp removeObjectForKey:@"length"];
    [callbackRsp removeObjectForKey:@"keepalive"];
    [callbackRsp removeObjectForKey:@"responseType"];
    
    [callbackRsp setObject:@(-1) forKey:@"status"];
    [callbackRsp setObject:[NSString stringWithFormat:@"%@(%ld)",[error localizedDescription], (long)[error code]] forKey:@"data"];
    NSString * statusText = @"";
    
    switch ([error code]) {
        case -1000:
        case -1002:
        case -1003:
            statusText = @"ERR_INVALID_REQUEST";
            break;
        default:
            break;
    }
    [callbackRsp setObject:statusText forKey:@"statusText"];
    
}
- (void)_loadFinishWithResponse:(WXResourceResponse*)response data:(NSData*)data callbackRsp:(NSMutableDictionary*)callbackRsp
{
    [callbackRsp removeObjectForKey:@"readyState"];
    [callbackRsp removeObjectForKey:@"length"];
    [callbackRsp removeObjectForKey:@"keepalive"];
    
    [callbackRsp setObject:((NSHTTPURLResponse*)response).statusCode >= 200 && ((NSHTTPURLResponse*)response).statusCode <= 299 ? @YES : @NO forKey:@"ok"];
    
    NSString *responseData = [self _stringfromData:data encode:((NSHTTPURLResponse*)response).textEncodingName];
    NSString * responseType = [callbackRsp objectForKey:@"responseType"];
    [callbackRsp removeObjectForKey:@"responseType"];
    if ([responseType isEqualToString:@"json"] || [responseType isEqualToString:@"jsonp"]) {
        //handle json format
        if ([responseType isEqualToString:@"jsonp"]) {
            //TODO: to be more elegant
            NSUInteger start = [responseData rangeOfString:@"("].location + 1 ;
            NSUInteger end = [responseData rangeOfString:@")" options:NSBackwardsSearch].location;
            if (end < [responseData length] && end > start) {
                responseData = [responseData substringWithRange:NSMakeRange(start, end-start)];
            }
        }
        id jsonObj = [self _JSONObjFromData:[responseData dataUsingEncoding:NSUTF8StringEncoding]];
        if (jsonObj) {
            [callbackRsp setObject:jsonObj forKey:@"data"];
        }
        
    } else {
        // return original Data
        if (responseData) {
            [callbackRsp setObject:responseData forKey:@"data"];
        }
    }
}

- (NSString*)_stringfromData:(NSData *)data encode:(NSString *)encoding
{
    NSMutableString *responseData = nil;
    if (data) {
        if (!encoding) {
            encoding = @"utf-8";
        }
        CFStringEncoding cfStrEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)encoding);
        if (cfStrEncoding == kCFStringEncodingInvalidId) {
            WXLogError(@"not supported encode");
        } else {
            NSStringEncoding encoding = CFStringConvertEncodingToNSStringEncoding(cfStrEncoding);
            responseData = [[NSMutableString alloc]initWithData:data encoding:encoding];
        }
    }
    return responseData;
}

- (id)_JSONObjFromData:(NSData *)data
{
    NSError * error = nil;
    id jsonObj = [WXUtility JSONObject:data error:&error];
    if (error) {
        WXLogDebug(@"%@", [error description]);
    }
    return jsonObj;
}

+ (NSString*)_getStatusText:(NSInteger)code
{    
    switch (code) {
        case -1:
            return @"ERR_INVALID_REQUEST";
        case 100:
            return @"Continue";
        case 101:
            return @"Switching Protocol";
        case 102:
            return @"Processing";
            
        case 200:
            return @"OK";
        case 201:
            return @"Created";
        case 202:
            return @"Accepted";
        case 203:
            return @"Non-Authoritative Information";
        case 204:
            return @"No Content";
        case 205:
            return @" Reset Content";
        case 206:
            return @"Partial Content";
        case 207:
            return @"Multi-Status";
        case 208:
            return @"Already Reported";
        case 226:
            return @"IM Used";
            
        case 300:
            return @"Multiple Choices";
        case 301:
            return @"Moved Permanently";
        case 302:
            return @"Found";
        case 303:
            return @"See Other";
        case 304:
            return @"Not Modified";
        case 305:
            return @"Use Proxy";
        case 306:
            return @"Switch Proxy";
        case 307:
            return @"Temporary Redirect";
        case 308:
            return @"Permanent Redirect";
            
        case 400:
            return @"Bad Request";
        case 401:
            return @"Unauthorized";
        case 402:
            return @"Payment Required";
        case 403:
            return @"Forbidden";
        case 404:
            return @"Not Found";
        case 405:
            return @"Method Not Allowed";
        case 406:
            return @"Not Acceptable";
        case 407:
            return @"Proxy Authentication Required";
        case 408:
            return @"Request Timeout";
        case 409:
            return @"Conflict";
        case 410:
            return @"Gone";
        case 411:
            return @"Length Required";
        case 412:
            return @"Precondition Failed";
        case 413:
            return @"Payload Too Large";
        case 414:
            return @"URI Too Long";
        case 415:
            return @"Unsupported Media Type";
        case 416:
            return @"Range Not Satisfiable";
        case 417:
            return @"Expectation Failed";
        case 418:
            return @"I'm a teapot";
        case 421:
            return @"Misdirected Request";
        case 422:
            return @"Unprocessable Entity";
        case 423:
            return @"Locked";
        case 424:
            return @"Failed Dependency";
        case 426:
            return @"Upgrade Required";
        case 428:
            return @"Precondition Required";
        case 429:
            return @"Too Many Requests";
        case 431:
            return @"Request Header Fields Too Large";
        case 451:
            return @"Unavailable For Legal Reasons";
            
        case 500:
            return @"Internal Server Error";
        case 501:
            return @"Not Implemented";
        case 502:
            return @"Bad Gateway";
        case 503:
            return @"Service Unavailable";
        case 504:
            return @"Gateway Timeout";
        case 505:
            return @"HTTP Version Not Supported";
        case 506:
            return @"Variant Also Negotiates";
        case 507:
            return @"Insufficient Storage";
        case 508:
            return @"Loop Detected";
        case 510:
            return @"Not Extended";
        case 511:
            return @"Network Authentication Required";
        default:
            break;
    }
    
    return @"Unknown";
}

#pragma mark - Deprecated

- (void)sendHttp:(NSDictionary*)param callback:(WXModuleKeepAliveCallback)callback
{
    NSString* method = [param objectForKey:@"method"];
    NSString* urlStr = [param objectForKey:@"url"];
    NSDictionary* headers = [param objectForKey:@"header"];
    NSString* body = [param objectForKey:@"body"];
    
    NSURL *url = [NSURL URLWithString:urlStr];
    
    //TODO:referrer
    WXResourceRequest *request = [WXResourceRequest requestWithURL:url resourceType:WXResourceTypeOthers referrer:nil cachePolicy:NSURLRequestUseProtocolCachePolicy];
    request.HTTPMethod = method;
    request.timeoutInterval = 60.0;
    request.userAgent = [WXUtility userAgent];
    
    for (NSString *key in headers) {
        NSString *value = [headers objectForKey:key];
        [request setValue:value forHTTPHeaderField:key];
    }
    [request setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];
    
    WXResourceLoader *loader = [[WXResourceLoader alloc] initWithRequest:request];
    loader.onFinished = ^(const WXResourceResponse * response, NSData *data) {
        NSString* responseData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        callback(responseData,NO);
    };
    
    loader.onFailed = ^(NSError *error) {
        if (callback) {
            callback(nil,NO);
        }
    };

    [loader start];
}

@end
