// Copyright 2016 wkh237@github. All rights reserved.
// Use of this source code is governed by a MIT-style license that can be
// found in the LICENSE file.

import RNFetchBlob from '../index.js'
import XMLHttpRequestEventTarget from './XMLHttpRequestEventTarget.js'
import Log from '../utils/log.js'
import Blob from './Blob.js'
import ProgressEvent from './ProgressEvent.js'
import URIUtil from '../utils/uri'

const log = new Log('XMLHttpRequest')

log.disable()
// log.level(3)

const UNSENT = 0
const OPENED = 1
const HEADERS_RECEIVED = 2
const LOADING = 3
const DONE = 4

export default class XMLHttpRequest extends XMLHttpRequestEventTarget{

  _onreadystatechange : () => void;

  upload : XMLHttpRequestEventTarget = new XMLHttpRequestEventTarget();
  static binaryContentTypes : Array<string> = [
    'image/', 'video/', 'audio/'
  ];

  // readonly
  _readyState : number = UNSENT;
  _uriType : 'net' | 'file' = 'net';
  _response : any = '';
  _responseText : any = '';
  _responseHeaders : any = {};
  _responseType : '' | 'arraybuffer' | 'blob'  | 'json' | 'text' = '';
  // TODO : not suppoted ATM
  _responseURL : null = '';
  _responseXML : null = '';
  _status : number = 0;
  _statusText : string = '';
  _timeout : number = 60000;
  _sendFlag : boolean = false;
  _uploadStarted : boolean = false;
  _increment : boolean = false;

  // RNFetchBlob compatible data structure
  _config : RNFetchBlobConfig = {};
  _url : any;
  _method : string;
  _headers: any = {
    'Content-Type' : 'text/plain'
  };
  _cleanUp : () => void = null;
  _body: any;

  // RNFetchBlob promise object, which has `progress`, `uploadProgress`, and
  // `cancel` methods.
  _task: any;

  // constants
  get UNSENT() { return UNSENT }
  get OPENED() { return OPENED }
  get HEADERS_RECEIVED() { return HEADERS_RECEIVED }
  get LOADING() { return LOADING }
  get DONE() { return DONE }

  static get UNSENT() {
    return UNSENT
  }

  static get OPENED() {
    return OPENED
  }

  static get HEADERS_RECEIVED() {
    return HEADERS_RECEIVED
  }

  static get LOADING() {
    return LOADING
  }

  static get DONE() {
    return DONE
  }

  static setLog(level:number) {
    if(level === -1)
      log.disable()
    else
      log.level(level)
  }

  static addBinaryContentType(substr:string) {
    for(let i in XMLHttpRequest.binaryContentTypes) {
      if(new RegExp(substr,'i').test(XMLHttpRequest.binaryContentTypes[i])) {
        return
      }
    }
    XMLHttpRequest.binaryContentTypes.push(substr)

  }

  static removeBinaryContentType(val) {
    for(let i in XMLHttpRequest.binaryContentTypes) {
      if(new RegExp(substr,'i').test(XMLHttpRequest.binaryContentTypes[i])) {
        XMLHttpRequest.binaryContentTypes.splice(i,1)
        return
      }
    }
  }

  constructor() {
    log.verbose('XMLHttpRequest constructor called')
    super()
  }


  /**
   * XMLHttpRequest.open, always async, user and password not supported. When
   * this method invoked, headers should becomes empty again.
   * @param  {string} method Request method
   * @param  {string} url Request URL
   * @param  {true} async Always async
   * @param  {any} user NOT SUPPORTED
   * @param  {any} password NOT SUPPORTED
   */
  open(method:string, url:string, async:true, user:any, password:any) {
    log.verbose('XMLHttpRequest open ', method, url, async, user, password)
    this._method = method
    this._url = url
    this._headers = {}
    this._increment = URIUtil.isJSONStreamURI(this._url)
    this._url = this._url.replace(/^JSONStream\:\/\//, '')
    this._dispatchReadStateChange(XMLHttpRequest.OPENED)
  }

  /**
   * Invoke this function to send HTTP request, and set body.
   * @param  {any} body Body in RNfetchblob flavor
   */
  send(body) {

    this._body = body

    if(this._readyState !== XMLHttpRequest.OPENED)
      throw 'InvalidStateError : XMLHttpRequest is not opened yet.'
    let promise = Promise.resolve()
    this._sendFlag = true
    log.verbose('XMLHttpRequest send ', body)
    let {_method, _url, _headers } = this
    log.verbose('sending request with args', _method, _url, _headers, body)
    log.verbose(typeof body, body instanceof FormData)

    if(body instanceof Blob) {
      log.debug('sending blob body', body._blobCreated)
      promise = new Promise((resolve, reject) => {
          body.onCreated((blob) => {
            // when the blob is derived (not created by RN developer), the blob
            // will be released after XMLHttpRequest sent
            if(blob.isDerived) {
              this._cleanUp = () => {
                blob.close()
              }
            }
            log.debug('body created send request')
            body = RNFetchBlob.wrap(blob.getRNFetchBlobRef())
            resolve()
          })
        })
    }
    else if(typeof body === 'object') {
      body = JSON.stringify(body)
      promise = Promise.resolve()
    }
    else {
      body = body ? body.toString() : body
      promise = Promise.resolve()
    }

    promise.then(() => {
      log.debug('send request invoke', body)
      for(let h in _headers) {
        _headers[h] = _headers[h].toString()
      }

      this._task = RNFetchBlob
                    .config({
                      auto: true,
                      timeout : this._timeout,
                      increment : this._increment,
                      binaryContentTypes : XMLHttpRequest.binaryContentTypes
                    })
                    .fetch(_method, _url, _headers, body)
      this._task
          .stateChange(this._headerReceived)
          .uploadProgress(this._uploadProgressEvent)
          .progress(this._progressEvent)
          .catch(this._onError)
          .then(this._onDone)

    })
  }

  overrideMimeType(mime:string) {
    log.verbose('XMLHttpRequest overrideMimeType', mime)
    this._headers['Content-Type'] = mime
  }

  setRequestHeader(name, value) {
    log.verbose('XMLHttpRequest set header', name, value)
    if(this._readyState !== OPENED || this._sendFlag) {
      throw `InvalidStateError : Calling setRequestHeader in wrong state  ${this._readyState}`
    }
    // UNICODE SHOULD NOT PASS
    if(typeof name !== 'string' || /[^\u0000-\u00ff]/.test(name)) {
      throw 'TypeError : header field name should be a string'
    }
    //
    let invalidPatterns = [
      /[\(\)\>\<\@\,\:\\\/\[\]\?\=\}\{\s\ \u007f\;\t\0\v\r]/,
      /tt/
    ]
    for(let pattern of invalidPatterns) {
      if(pattern.test(name) || typeof name !== 'string') {
        throw `SyntaxError : Invalid header field name ${name}`
      }
    }
    this._headers[name] = value
  }

  abort() {
    log.verbose('XMLHttpRequest abort ')
    if(!this._task)
      return
    this._task.cancel((err) => {
      let e = {
        timeStamp : Date.now(),
      }
      if(this.onabort)
        this.onabort()
      if(err) {
        e.detail = err
        e.type = 'error'
        this.dispatchEvent('error', e)
      }
      else {
        e.type = 'abort'
        this.dispatchEvent('abort', e)
      }
    })
  }

  getResponseHeader(field:string):string | null {
    log.verbose('XMLHttpRequest get header', field, this._responseHeaders)
    if(!this._responseHeaders)
      return null
    return (this._responseHeaders[field] || this._responseHeaders[field.toLowerCase()]) || null

  }

  getAllResponseHeaders():string | null {
    log.verbose('XMLHttpRequest get all headers', this._responseHeaders)
    if(!this._responseHeaders)
      return ''
    let result = ''
    let respHeaders = this.responseHeaders
    for(let i in respHeaders) {
      result += `${i}: ${respHeaders[i]}${String.fromCharCode(0x0D,0x0A)}`
    }
    return result.substr(0, result.length-2)
  }

  _headerReceived = (e) => {
    log.debug('header received ', this._task.taskId, e)
    this.responseURL = this._url
    if(e.state === "2" && e.taskId === this._task.taskId) {
      this._responseHeaders = e.headers
      this._statusText = e.status
      this._status = Math.floor(e.status)
      this._dispatchReadStateChange(XMLHttpRequest.HEADERS_RECEIVED)
    }
  }

  _uploadProgressEvent = (send:number, total:number) => {
    if(!this._uploadStarted) {
      this.upload.dispatchEvent('loadstart')
      this._uploadStarted = true
    }
    if(send >= total)
      this.upload.dispatchEvent('load')
    this.upload.dispatchEvent('progress', new ProgressEvent(true, send, total))
  }

  _progressEvent = (send:number, total:number, chunk:string) => {
    log.verbose(this.readyState)
    if(this._readyState === XMLHttpRequest.HEADERS_RECEIVED)
      this._dispatchReadStateChange(XMLHttpRequest.LOADING)
    let lengthComputable = false
    if(total && total >= 0)
        lengthComputable = true
    let e = new ProgressEvent(lengthComputable, send, total)

    if(this._increment) {
      this._responseText += chunk
    }
    this.dispatchEvent('progress', e)
  }

  _onError = (err) => {
    let statusCode = Math.floor(this.status)
    if(statusCode >= 100 && statusCode !== 408) {
      return
    }
    log.debug('XMLHttpRequest error', err)
    this._statusText = err
    this._status = String(err).match(/\d+/)
    this._status = this._status ? Math.floor(this.status) : 404
    this._dispatchReadStateChange(XMLHttpRequest.DONE)
    if(err && String(err.message).match(/(timed\sout|timedout)/) || this._status == 408) {
      this.dispatchEvent('timeout')
    }
    this.dispatchEvent('loadend')
    this.dispatchEvent('error', {
      type : 'error',
      detail : err
    })
    this.clearEventListeners()
  }

  _onDone = (resp) => {
    log.debug('XMLHttpRequest done', this._url, resp, this)
    this._statusText = this._status
    let responseDataReady = () => {
      log.debug('request done state = 4')
      this.dispatchEvent('load')
      this.dispatchEvent('loadend')
      this._dispatchReadStateChange(XMLHttpRequest.DONE)
      this.clearEventListeners()
    }
    if(resp) {
      let info = resp.respInfo || {}
      log.debug(this._url, info, info.respType)
      switch(this._responseType) {
        case 'blob' :
          resp.blob().then((b) => {
            this._responseText = resp.text()
            this._response = b
            responseDataReady()
          })
        break;
        case 'arraybuffer':
          // TODO : to array buffer
        break
        case 'json':
          this._response = resp.json()
          this._responseText = resp.text()
        break
        default :
          this._responseText = resp.text()
          this._response = this.responseText
          responseDataReady()
        break;
      }
    }

  }

  _dispatchReadStateChange(state) {
    this._readyState = state
    if(typeof this._onreadystatechange === 'function')
      this._onreadystatechange()
  }

  set onreadystatechange(fn:() => void) {
    log.verbose('XMLHttpRequest set onreadystatechange', fn)
    this._onreadystatechange = fn
  }

  get onreadystatechange() {
    return this._onreadystatechange
  }

  get readyState() {
    log.verbose('get readyState', this._readyState)
    return this._readyState
  }

  get status() {
    log.verbose('get status', this._status)
    return this._status
  }

  get statusText() {
    log.verbose('get statusText', this._statusText)
    return this._statusText
  }

  get response() {
    log.verbose('get response', this._response)
    return this._response
  }

  get responseText() {
    log.verbose('get responseText', this._responseText)
    return this._responseText
  }

  get responseURL() {
    log.verbose('get responseURL', this._responseURL)
    return this._responseURL
  }

  get responseHeaders() {
    log.verbose('get responseHeaders', this._responseHeaders)
    return this._responseHeaders
  }

  set timeout(val) {
    this._timeout = val*1000
    log.verbose('set timeout', this._timeout)
  }

  get timeout() {
    log.verbose('get timeout', this._timeout)
    return this._timeout
  }

  set responseType(val) {
    log.verbose('set response type', this._responseType)
    this._responseType = val
  }

  get responseType() {
    log.verbose('get response type', this._responseType)
    return this._responseType
  }

  static get isRNFBPolyfill() {
    return true
  }

}