/*!
 * Copyright 2017 Vpn.Email network security technology Canada Inc. All Rights Reserved.
 *
 * Vpn.Email network technolog Canada Ltd.
 * Licensed 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 * as crypto from 'crypto'
import * as Async from 'async'
import * as Stream from 'stream'
const EOF = Buffer.from ( '\r\n\r\n', 'utf8' )
import * as fs from 'fs'
export interface packetBuffer {
	command: number;
	uuid: string;
	buffer: Buffer;
	serial: number
}

export interface pairConnect {
	serverListen: string;
	clientListen: string;
}

export const encrypt = ( text: Buffer, masterkey: string, CallBack ) => {
	let salt = null
	Async.waterfall ([
		next => crypto.randomBytes ( 64, next ),
		( _salt, next ) => {
			salt = _salt
			crypto.pbkdf2 ( masterkey, salt, 2145, 32, 'sha512', next )
		}
	], ( err, derivedKey ) => {
		if ( err )
			return CallBack ( err )
		
		crypto.randomBytes ( 12, ( err1, iv ) => {
			if ( err1 )
				return CallBack ( err1 )
			
			const cipher = crypto.createCipheriv ( 'aes-256-gcm', derivedKey, iv );
		
			let _text = Buffer.concat ([ Buffer.alloc ( 4, 0 ) , text ])
			_text.writeUInt32BE ( text.length, 0 )
			if ( text.length < 500 ) {
				 _text = Buffer.concat ([ _text, Buffer.alloc ( 100 + Math.random () * 1000 )])
			}
			const encrypted = Buffer.concat ([ cipher.update ( _text ), cipher.final ()]);
			const ret = Buffer.concat ([ salt, iv, cipher.getAuthTag(), encrypted ])
			
			return CallBack ( null, ret )
		})
	})
}
/**
 * Decrypts text by given key
 * @param String base64 encoded input data
 * @param Buffer masterkey
 * @returns String decrypted (original) text
 */
export const decrypt =  ( data: Buffer, masterkey, CallBack ) => {
	if ( !data || !data.length )
		return CallBack ( new Error( 'null' ))
	try {
		// base64 decoding

		// convert data to buffers

		const salt = data.slice ( 0, 64 );
		const iv = data.slice ( 64, 76 );
		const tag = data.slice ( 76, 92 );
		const text = data.slice ( 92 );
		// derive key using; 32 byte key length
		crypto.pbkdf2 ( masterkey, salt , 2145, 32, 'sha512', ( err, derivedKey ) => {
			
			if ( err )
				return CallBack ( err )
			// AES 256 GCM Mode
			try {
				const decipher = crypto.createDecipheriv ( 'aes-256-gcm', derivedKey, iv )
				decipher.setAuthTag ( tag )
				const decrypted = Buffer.concat([decipher.update ( text ), decipher.final ( )]) 
				const leng = decrypted.slice( 4, 4 + decrypted.readUInt32BE(0))
				return CallBack ( null, leng )
			} catch ( ex ) {
				console.log ( `decrypt catch error [${ ex.message }]`)
			}
			

			
		})

	} catch ( e ) {
		return CallBack ( e )
	}

}

export const packetBuffer = ( bit0: number, _serial: number, id: string, buffer: Buffer ) => {
	
	const _buffer = new Buffer ( 6 )
	_buffer.fill ( 0 )
	_buffer.writeUInt8 ( bit0, 0 )
	_buffer.writeUInt32BE ( _serial, 1 )

	const uuid = new Buffer ( id, 'utf8' )
	_buffer.writeUInt8 ( id.length, 5 )
	if ( buffer && buffer.length )
		return Buffer.concat ([ _buffer, uuid, buffer ])
	return Buffer.concat ([ _buffer, uuid ])
}

export const openPacket = ( buffer: Buffer ) => {
	const idLength = buffer.readUInt8 ( 5 )
	return  {
		command: buffer.readUInt8 ( 0 ),
		serial: buffer.readUInt32BE ( 1 ),
		uuid: buffer.toString ( 'utf8', 6, 6 + idLength ),
		buffer: buffer.slice ( 6 + idLength )
	}
}
const HTTP_HEADER = Buffer.from (
	`HTTP/1.1 200 OK\r\nDate: ${ new Date ().toUTCString ()}\r\nContent-Type: text/html\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nVary: Accept-Encoding\r\n\r\n`, 'utf8')
const HTTP_EOF = Buffer.from ( '\r\n\r\n', 'utf8' )

export class encryptStream extends Stream.Transform {
	private salt: Buffer
	private iv: Buffer
	public ERR: Error = null
	private first = true
	public derivedKey: Buffer = null

	private BlockBuffer ( _buf: Buffer ) {
		return Buffer.from( _buf.length.toString( 16 ).toUpperCase() + '\r\n', 'utf8' )
	}

	constructor ( private password: string, private random: number, private httpHeader : ( str: string ) => Buffer, CallBack ) {
		super ()
		Async.waterfall ([
			next => crypto.randomBytes ( 64, next ),
			( _salt, next ) => {
				this.salt = _salt
				crypto.randomBytes ( 12, next )
			},
			( _iv, next ) => {
				this.iv = _iv
				crypto.pbkdf2 ( password, this.salt, 2145, 32, 'sha512', next )
			}
		], ( err, derivedKey ) => {
			if ( err ) 
				return this.ERR = err
			this.derivedKey = derivedKey
			return CallBack ( err )
		})
	}
	
	public _transform ( chunk: Buffer, encode, cb ) {

		const cipher = crypto.createCipheriv ( 'aes-256-gcm', this.derivedKey, this.iv )

		let _text = Buffer.concat ([ Buffer.alloc ( 4, 0 ) , chunk ])

		_text.writeUInt32BE ( chunk.length, 0 )

		if ( chunk.length < this.random ) {
			_text = Buffer.concat ([ _text, Buffer.allocUnsafe ( Math.random() * 1000 )])
		}

		const _buf = Buffer.concat ([ cipher.update ( _text ), cipher.final ()])
		const _buf1 = Buffer.concat ([ cipher.getAuthTag (), _buf ])
		
		if ( this.first ) {
			this.first = false
			const black = Buffer.concat ([ this.salt, this.iv, _buf1 ]).toString ( 'base64' )
			if ( ! this.httpHeader ) {
				const _buf4 = Buffer.from ( black, 'utf8')
				return cb ( null, Buffer.concat ([ HTTP_HEADER, this.BlockBuffer ( _buf4 ), _buf4, EOF ]))
			}
				
			return cb ( null, this.httpHeader ( black ))

		}
		
		const _buf2 = _buf1.toString( 'base64' )
		if ( this.httpHeader ) {
			return cb ( null, this.httpHeader ( _buf2 ))
		}
		const _buf3 = Buffer.from ( _buf2, 'utf8' )
		return cb ( null, Buffer.concat ([ this.BlockBuffer ( _buf3 ), _buf3, EOF ]))
	}
}

export class decryptStream extends Stream.Transform {
	private first = true
	private salt: Buffer
	private iv: Buffer

	private derivedKey: Buffer = null
	private decipher: crypto.Decipher = null

	public firstProcess (  chunk: Buffer, CallBack: ( err?: Error, text?: Buffer ) => void ) {
		this.first = false
		this.salt = chunk.slice ( 0, 64 );
		this.iv = chunk.slice ( 64, 76 );
		return crypto.pbkdf2 ( this.password, this.salt , 2145, 32, 'sha512', ( err, derivedKey ) => {
			if ( err ) {
				console.log ( `decryptStream crypto.pbkdf2 ERROR: ${ err.message }` )
				return CallBack ( err )
			}
			this.derivedKey = derivedKey
			const _buf = chunk.slice ( 76 )
			this.decipher = crypto.createDecipheriv ( 'aes-256-gcm', this.derivedKey, this.iv )
			this.decipher.setAuthTag ( _buf.slice ( 0, 16 ))
			try {
				const _Buf = Buffer.concat ([ this.decipher.update ( _buf.slice ( 16 )) , this.decipher.final () ])
				return  CallBack ( null, _Buf.slice( 4, 4 + _Buf.readUInt32BE ( 0 )))
			} catch ( e ) {
				return CallBack ( e )
			}

		})
	}

	constructor ( private password: string ) {
		super ()

	}

	public _transform ( chunk: Buffer, encode, cb ) {
		if ( this.first ) {
			return this.firstProcess ( chunk, cb )
		}
		this.decipher = crypto.createDecipheriv ( 'aes-256-gcm', this.derivedKey, this.iv )
		this.decipher.setAuthTag ( chunk.slice ( 0, 16 ))
		try {
			const _Buf = Buffer.concat ([ this.decipher.update ( chunk.slice ( 16 )) , this.decipher.final () ])
			return cb ( null, _Buf.slice( 4, 4 + _Buf.readUInt32BE ( 0 )))
		} catch ( e ) {
			console.log ( 'class decryptStream _decrypt error:', e.message )
			return cb ()
		}
		
	}
}

class encode extends Stream.Transform {
	constructor () { super ()}
	private kk = null
	public _transform ( chunk: Buffer, encode, cb ) {
		let start = chunk.slice (0)
		while ( start.length ) {
			const point = start.indexOf ( 0x0a )
			if ( point < 0 ) {
				this.push ( start )
				break
			}
			const _buf = start.slice ( 0, point )
			this.push ( _buf )
			start = start.slice ( point + 1 )
		}
		return cb ()
	}

}

class encodeHex extends Stream.Transform {
	constructor () { super ()}
	public _transform ( chunk: Buffer, encode, cb ) {
		
		return cb ( null, chunk.toString ('utf8'))
	}
}

export class getDecryptClientStreamFromHttp extends Stream.Transform {

	private first = true
	private text = ''
	constructor () { super ()}
	public getBlock ( block: string ) {
		const uu = block.split ('\r\n')
		if ( uu.length !== 2 ) {
			return null
		}
		const length = parseInt ( uu[0], 16 )
		const text = uu [1]
		if ( length === text.length ) {
			return text
		}
		console.log (`length[${length}] !== text.length[${text.length}]`)
		return null
	}

	public _transform ( chunk: Buffer, encode, cb ) {
		this.text += chunk.toString ( 'utf8' )
		const line = this.text.split ( '\r\n\r\n' )

		while ( this.first && line.length > 1 || !this.first && line.length ) {

			if ( this.first ) {
				this.first = false
				line.shift()
			}
			const _text = line.shift ()
			if ( ! _text.length )
				continue
			const text = this.getBlock ( _text )
			if ( ! text ) {
				//			middle data can't get block
				if ( line.length ) {
					console.log ( 'getDecryptStreamFromHttp have ERROR:\n*****************************\n' )
					console.log ( text )
					return this.unpipe()
				}
				this.text = _text
				return cb ()
			}
			
			this.push ( Buffer.from ( text, 'base64' ))
		}
		this.text = ''
		return cb ()
	}
}

export class getDecrypGatwayStreamFromHttp extends Stream.Transform {

	private text = ''

	private formatErr ( text: string ) {
		const log = 'getDecryptRequestStreamFromHttp format ERROR:\n*****************************\n' + text + '\r\n'
		console.log ( log )
		this.saveLog ( log )
	}

	constructor ( private saveLog: ( str: string ) => void ) { super ()}

	public _transform ( chunk: Buffer, encode, cb ) {
		
		this.text += chunk.toString ( 'utf8' )
		
		const block = this.text.split ( '\r\n\r\n' )
		
		while ( block.length > 1 ) {

			const blockText = block.shift ()

			if ( ! blockText.length )
				continue
				
			if ( /^GET /i.test ( blockText )) {
				
				const _line = blockText.split ( '\r\n' )[ 0 ]
				
				const _url = _line.split ( ' ' )
				
				if ( _url.length < 2 ) {
					if ( block.length > 1 ) {
						this.formatErr ( blockText )
						return this.unpipe ()
					}
					this.text = blockText
					return cb ()
				}
				const text = Buffer.from ( _url[1].slice ( 1 ), 'base64' )
				this.push ( text )
				continue
			}

			if ( /^POST /i.test ( blockText )) {

				if ( block.length > 0 ) {
					const header = blockText.split ( '\r\n' )

					const _length = header.findIndex ( n => {
						return /^Content-Length: /i.test ( n )
					})
					
					if ( _length === -1 ) {
						this.formatErr ( blockText )
						return this.unpipe ()
					}

					const lengthString = header [ _length ].split ( ' ' )
					if ( lengthString.length !== 2 ) {
						this.formatErr ( blockText )
						return this.unpipe ()
					}

					const length = parseInt ( lengthString[ 1 ])
					if ( ! length ) {
						this.formatErr ( blockText )
						return this.unpipe ()
					}

					const _text = block.shift ()
					if ( length !== _text.length ) {
						const log = `${ blockText }\r\n\r\n${ _text }`
						if ( block.length > 0 ) {
							this.formatErr ( log )
							return this.unpipe ()
						}
						this.text = log
						return cb ()
					}

					this.push ( Buffer.from ( _text, 'base64' ))
					continue
				}

				this.text = blockText
				return cb ()
			}
		}
		
		this.text = block[0]
		return cb ()
	}
}



