/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 package org.mangui.hls.utils {
    import flash.utils.getTimer;
    import flash.display.DisplayObject;
    import flash.utils.ByteArray;
    import flash.events.Event;

    /**
     * Contains Utility functions for AES-128 CBC Decryption
     */
    public class AES {
        private var _key : FastAESKey;
        //private var _keyArray : ByteArray;
        private var iv0 : uint;
        private var iv1 : uint;
        private var iv2 : uint;
        private var iv3 : uint;
        /* callback function upon decrypt progress */
        private var _progress : Function;
        /* callback function upon decrypt complete */
        private var _complete : Function;
        /** Byte data to be decrypt **/
        private var _data : ByteArray;
        /** read position **/
        private var _readPosition : uint;
        /** write position **/
        private var _writePosition : uint;
        /** chunk size to avoid blocking **/
        private static const CHUNK_SIZE : uint = 2048;
        /** is bytearray full ? **/
        private var _dataComplete : Boolean;
        /** display object used for ENTER_FRAME listener */
        private var _displayObject : DisplayObject;

        public function AES(displayObject : DisplayObject, key : ByteArray, iv : ByteArray, notifyprogress : Function, notifycomplete : Function) {
            // _keyArray = key;
            _key = new FastAESKey(key);
            iv.position = 0;
            iv0 = iv.readUnsignedInt();
            iv1 = iv.readUnsignedInt();
            iv2 = iv.readUnsignedInt();
            iv3 = iv.readUnsignedInt();
            _data = new ByteArray();
            _dataComplete = false;
            _progress = notifyprogress;
            _complete = notifycomplete;
            _readPosition = 0;
            _writePosition = 0;
            _displayObject = displayObject;
        }

        public function append(data : ByteArray) : void {
            // CONFIG::LOGGING {
            // Log.info("notify append");
            // }
            _data.position = _writePosition;
            _data.writeBytes(data);
            if (_writePosition == 0) {
                _displayObject.addEventListener(Event.ENTER_FRAME, _decryptTimer);
            }
            _writePosition += data.length;
        }

        public function notifycomplete() : void {
            // CONFIG::LOGGING {
            // Log.info("notify complete");
            // }
            _dataComplete = true;
        }

        public function cancel() : void {
            _displayObject.removeEventListener(Event.ENTER_FRAME, _decryptTimer);
        }

        private function _decryptTimer(e : Event) : void {
            var start_time : int = getTimer();
            var decrypted : Boolean;
            do {
                decrypted = _decryptChunk();
            // dont spend more than 10ms in the decrypt timer to avoid blocking/freezing video
            // if frame rate is 60fps, we have 1000/60 = 16.6ms budget total per frame
            } while (decrypted && (getTimer() - start_time) < 10);
        }

        /** decrypt a small chunk of packets each time to avoid blocking **/
        private function _decryptChunk() : Boolean {
            _data.position = _readPosition;
            var decryptdata : ByteArray;
            if (_data.bytesAvailable) {
                if (_data.bytesAvailable <= CHUNK_SIZE) {
                    if (_dataComplete) {
                        // CONFIG::LOGGING {
                        // Log.info("data complete, last chunk");
                        // }
                        _readPosition += _data.bytesAvailable;
                        decryptdata = _decryptCBC(_data, _data.bytesAvailable);
                        unpad(decryptdata);
                    } else {
                        // data not complete, and available data less than chunk size, return
                        return false;
                    }
                } else {
                    _readPosition += CHUNK_SIZE;
                    decryptdata = _decryptCBC(_data, CHUNK_SIZE);
                }
                _progress(decryptdata);
                return true;
            } else {
                if (_dataComplete) {
                    CONFIG::LOGGING {
                        Log.debug("AES:data+decrypt completed, callback");
                    }
                    // callback
                    _complete();
                    _displayObject.removeEventListener(Event.ENTER_FRAME, _decryptTimer);
                }
                return false;
            }
        }

        /* Cypher Block Chaining Decryption, refer to
         * http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher-block_chaining_
         * for algorithm description
         */
        private function _decryptCBC(crypt : ByteArray, len : uint) : ByteArray {
            var src : Vector.<uint> = new Vector.<uint>(4);
            var dst : Vector.<uint> = new Vector.<uint>(4);
            var decrypt : ByteArray = new ByteArray();
            decrypt.length = len;

            for (var i : uint = 0; i < len / 16; i++) {
                // read src byte array
                src[0] = crypt.readUnsignedInt();
                src[1] = crypt.readUnsignedInt();
                src[2] = crypt.readUnsignedInt();
                src[3] = crypt.readUnsignedInt();

                // AES decrypt src vector into dst vector
                _key.decrypt128(src, dst);

                // CBC : write output = XOR(decrypted,IV)
                decrypt.writeUnsignedInt(dst[0] ^ iv0);
                decrypt.writeUnsignedInt(dst[1] ^ iv1);
                decrypt.writeUnsignedInt(dst[2] ^ iv2);
                decrypt.writeUnsignedInt(dst[3] ^ iv3);

                // CBC : next IV = (input)
                iv0 = src[0];
                iv1 = src[1];
                iv2 = src[2];
                iv3 = src[3];
            }
            decrypt.position = 0;
            return decrypt;
        }

        public function unpad(a : ByteArray) : void {
            var c : uint = a.length % 16;
            if (c != 0) throw new Error("PKCS#5::unpad: ByteArray.length isn't a multiple of the blockSize");
            c = a[a.length - 1];
            for (var i : uint = c; i > 0; i--) {
                var v : uint = a[a.length - 1];
                a.length--;
                if (c != v) throw new Error("PKCS#5:unpad: Invalid padding value. expected [" + c + "], found [" + v + "]");
            }
        }

        public function destroy() : void {
            _key.dispose();
            // _key = null;
        }
    }
}
