/**
 * Cesium - https://github.com/CesiumGS/cesium
 *
 * Copyright 2011-2020 Cesium Contributors
 *
 * 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.
 *
 * Columbus View (Pat. Pend.)
 *
 * Portions licensed separately.
 * See https://github.com/CesiumGS/cesium/blob/master/LICENSE.md for full licensing details.
 */
define(['./when-8d13db60', './Check-70bec281', './RuntimeError-ba10bc3e', './createTaskProcessorWorker', './pako_inflate-8ea163f9'], function (when, Check, RuntimeError, createTaskProcessorWorker, pako_inflate) { 'use strict';

    var compressedMagic = 0x7468dead;
        var compressedMagicSwap = 0xadde6874;

        /**
         * Decodes data that is received from the Google Earth Enterprise server.
         *
         * @param {ArrayBuffer} key The key used during decoding.
         * @param {ArrayBuffer} data The data to be decoded.
         *
         * @private
         */
        function decodeGoogleEarthEnterpriseData(key, data) {
            if (decodeGoogleEarthEnterpriseData.passThroughDataForTesting) {
                return data;
            }

            //>>includeStart('debug', pragmas.debug);
            Check.Check.typeOf.object('key', key);
            Check.Check.typeOf.object('data', data);
            //>>includeEnd('debug');

            var keyLength = key.byteLength;
            if (keyLength === 0 || (keyLength % 4) !== 0) {
                throw new RuntimeError.RuntimeError('The length of key must be greater than 0 and a multiple of 4.');
            }

            var dataView = new DataView(data);
            var magic = dataView.getUint32(0, true);
            if (magic === compressedMagic || magic === compressedMagicSwap) {
                // Occasionally packets don't come back encoded, so just return
                return data;
            }

            var keyView = new DataView(key);

            var dp = 0;
            var dpend = data.byteLength;
            var dpend64 = dpend - (dpend % 8);
            var kpend = keyLength;
            var kp;
            var off = 8;

            // This algorithm is intentionally asymmetric to make it more difficult to
            // guess. Security through obscurity. :-(

            // while we have a full uint64 (8 bytes) left to do
            // assumes buffer is 64bit aligned (or processor doesn't care)
            while (dp < dpend64) {
                // rotate the key each time through by using the offets 16,0,8,16,0,8,...
                off = (off + 8) % 24;
                kp = off;

                // run through one key length xor'ing one uint64 at a time
                // then drop out to rotate the key for the next bit
                while ((dp < dpend64) && (kp < kpend)) {
                    dataView.setUint32(dp, dataView.getUint32(dp, true) ^ keyView.getUint32(kp, true), true);
                    dataView.setUint32(dp + 4, dataView.getUint32(dp + 4, true) ^ keyView.getUint32(kp + 4, true), true);
                    dp += 8;
                    kp += 24;
                }
            }

            // now the remaining 1 to 7 bytes
            if (dp < dpend) {
                if (kp >= kpend) {
                    // rotate the key one last time (if necessary)
                    off = (off + 8) % 24;
                    kp = off;
                }

                while (dp < dpend) {
                    dataView.setUint8(dp, dataView.getUint8(dp) ^ keyView.getUint8(kp));
                    dp++;
                    kp++;
                }
            }
        }

        decodeGoogleEarthEnterpriseData.passThroughDataForTesting = false;

    /**
         * @private
         */
        function isBitSet(bits, mask) {
            return ((bits & mask) !== 0);
        }

    // Bitmask for checking tile properties
        var childrenBitmasks = [0x01, 0x02, 0x04, 0x08];
        var anyChildBitmask = 0x0F;
        var cacheFlagBitmask = 0x10; // True if there is a child subtree
        var imageBitmask = 0x40;
        var terrainBitmask = 0x80;

        /**
         * Contains information about each tile from a Google Earth Enterprise server
         *
         * @param {Number} bits Bitmask that contains the type of data and available children for each tile.
         * @param {Number} cnodeVersion Version of the request for subtree metadata.
         * @param {Number} imageryVersion Version of the request for imagery tile.
         * @param {Number} terrainVersion Version of the request for terrain tile.
         * @param {Number} imageryProvider Id of imagery provider.
         * @param {Number} terrainProvider Id of terrain provider.
         *
         * @private
         */
        function GoogleEarthEnterpriseTileInformation(bits, cnodeVersion, imageryVersion, terrainVersion, imageryProvider, terrainProvider) {
            this._bits = bits;
            this.cnodeVersion = cnodeVersion;
            this.imageryVersion = imageryVersion;
            this.terrainVersion = terrainVersion;
            this.imageryProvider = imageryProvider;
            this.terrainProvider = terrainProvider;
            this.ancestorHasTerrain = false; // Set it later once we find its parent
            this.terrainState = undefined;
        }

        /**
         * Creates GoogleEarthEnterpriseTileInformation from an object
         *
         * @param {Object} info Object to be cloned
         * @param {GoogleEarthEnterpriseTileInformation} [result] The object onto which to store the result.
         * @returns {GoogleEarthEnterpriseTileInformation} The modified result parameter or a new GoogleEarthEnterpriseTileInformation instance if none was provided.
         */
        GoogleEarthEnterpriseTileInformation.clone = function(info, result) {
            if (!when.defined(result)) {
                result = new GoogleEarthEnterpriseTileInformation(info._bits, info.cnodeVersion, info.imageryVersion, info.terrainVersion,
                    info.imageryProvider, info.terrainProvider);
            } else {
                result._bits = info._bits;
                result.cnodeVersion = info.cnodeVersion;
                result.imageryVersion = info.imageryVersion;
                result.terrainVersion = info.terrainVersion;
                result.imageryProvider = info.imageryProvider;
                result.terrainProvider = info.terrainProvider;
            }
            result.ancestorHasTerrain = info.ancestorHasTerrain;
            result.terrainState = info.terrainState;

            return result;
        };

        /**
         * Sets the parent for the tile
         *
         * @param {GoogleEarthEnterpriseTileInformation} parent Parent tile
         */
        GoogleEarthEnterpriseTileInformation.prototype.setParent = function(parent) {
            this.ancestorHasTerrain = parent.ancestorHasTerrain || this.hasTerrain();
        };

        /**
         * Gets whether a subtree is available
         *
         * @returns {Boolean} true if subtree is available, false otherwise.
         */
        GoogleEarthEnterpriseTileInformation.prototype.hasSubtree = function() {
            return isBitSet(this._bits, cacheFlagBitmask);
        };

        /**
         * Gets whether imagery is available
         *
         * @returns {Boolean} true if imagery is available, false otherwise.
         */
        GoogleEarthEnterpriseTileInformation.prototype.hasImagery = function() {
            return isBitSet(this._bits, imageBitmask);
        };

        /**
         * Gets whether terrain is available
         *
         * @returns {Boolean} true if terrain is available, false otherwise.
         */
        GoogleEarthEnterpriseTileInformation.prototype.hasTerrain = function() {
            return isBitSet(this._bits, terrainBitmask);
        };

        /**
         * Gets whether any children are present
         *
         * @returns {Boolean} true if any children are available, false otherwise.
         */
        GoogleEarthEnterpriseTileInformation.prototype.hasChildren = function() {
            return isBitSet(this._bits, anyChildBitmask);
        };

        /**
         * Gets whether a specified child is available
         *
         * @param {Number} index Index of child tile
         *
         * @returns {Boolean} true if child is available, false otherwise
         */
        GoogleEarthEnterpriseTileInformation.prototype.hasChild = function(index) {
            return isBitSet(this._bits, childrenBitmasks[index]);
        };

        /**
         * Gets bitmask containing children
         *
         * @returns {Number} Children bitmask
         */
        GoogleEarthEnterpriseTileInformation.prototype.getChildBitmask = function() {
            return this._bits & anyChildBitmask;
        };

    // Datatype sizes
    var sizeOfUint16 = Uint16Array.BYTES_PER_ELEMENT;
    var sizeOfInt32 = Int32Array.BYTES_PER_ELEMENT;
    var sizeOfUint32 = Uint32Array.BYTES_PER_ELEMENT;

    var Types = {
        METADATA : 0,
        TERRAIN : 1,
        DBROOT : 2
    };

    Types.fromString = function(s) {
        if (s === 'Metadata') {
            return Types.METADATA;
        } else if (s === 'Terrain') {
            return Types.TERRAIN;
        } else if (s === 'DbRoot') {
            return Types.DBROOT;
        }
    };

    function decodeGoogleEarthEnterprisePacket(parameters, transferableObjects) {
        var type = Types.fromString(parameters.type);
        var buffer = parameters.buffer;
        decodeGoogleEarthEnterpriseData(parameters.key, buffer);

        var uncompressedTerrain = uncompressPacket(buffer);
        buffer = uncompressedTerrain.buffer;
        var length = uncompressedTerrain.length;

        switch (type) {
            case Types.METADATA:
                return processMetadata(buffer, length, parameters.quadKey);
            case Types.TERRAIN:
                return processTerrain(buffer, length, transferableObjects);
            case Types.DBROOT:
                transferableObjects.push(buffer);
                return {
                    buffer : buffer
                };
        }

    }

    var qtMagic = 32301;

    function processMetadata(buffer, totalSize, quadKey) {
        var dv = new DataView(buffer);
        var offset = 0;
        var magic = dv.getUint32(offset, true);
        offset += sizeOfUint32;
        if (magic !== qtMagic) {
            throw new RuntimeError.RuntimeError('Invalid magic');
        }

        var dataTypeId = dv.getUint32(offset, true);
        offset += sizeOfUint32;
        if (dataTypeId !== 1) {
            throw new RuntimeError.RuntimeError('Invalid data type. Must be 1 for QuadTreePacket');
        }

        // Tile format version
        var quadVersion = dv.getUint32(offset, true);
        offset += sizeOfUint32;
        if (quadVersion !== 2) {
            throw new RuntimeError.RuntimeError('Invalid QuadTreePacket version. Only version 2 is supported.');
        }

        var numInstances = dv.getInt32(offset, true);
        offset += sizeOfInt32;

        var dataInstanceSize = dv.getInt32(offset, true);
        offset += sizeOfInt32;
        if (dataInstanceSize !== 32) {
            throw new RuntimeError.RuntimeError('Invalid instance size.');
        }

        var dataBufferOffset = dv.getInt32(offset, true);
        offset += sizeOfInt32;

        var dataBufferSize = dv.getInt32(offset, true);
        offset += sizeOfInt32;

        var metaBufferSize = dv.getInt32(offset, true);
        offset += sizeOfInt32;

        // Offset from beginning of packet (instances + current offset)
        if (dataBufferOffset !== (numInstances * dataInstanceSize + offset)) {
            throw new RuntimeError.RuntimeError('Invalid dataBufferOffset');
        }

        // Verify the packets is all there header + instances + dataBuffer + metaBuffer
        if (dataBufferOffset + dataBufferSize + metaBufferSize !== totalSize) {
            throw new RuntimeError.RuntimeError('Invalid packet offsets');
        }

        // Read all the instances
        var instances = [];
        for (var i = 0; i < numInstances; ++i) {
            var bitfield = dv.getUint8(offset);
            ++offset;

            ++offset; // 2 byte align

            var cnodeVersion = dv.getUint16(offset, true);
            offset += sizeOfUint16;

            var imageVersion = dv.getUint16(offset, true);
            offset += sizeOfUint16;

            var terrainVersion = dv.getUint16(offset, true);
            offset += sizeOfUint16;

            // Number of channels stored in the dataBuffer
            offset += sizeOfUint16;

            offset += sizeOfUint16; // 4 byte align

            // Channel type offset into dataBuffer
            offset += sizeOfInt32;

            // Channel version offset into dataBuffer
            offset += sizeOfInt32;

            offset += 8; // Ignore image neighbors for now

            // Data providers
            var imageProvider = dv.getUint8(offset++);
            var terrainProvider = dv.getUint8(offset++);
            offset += sizeOfUint16; // 4 byte align

            instances.push(new GoogleEarthEnterpriseTileInformation(bitfield, cnodeVersion,
                imageVersion, terrainVersion, imageProvider, terrainProvider));
        }

        var tileInfo = [];
        var index = 0;

        function populateTiles(parentKey, parent, level) {
            var isLeaf = false;
            if (level === 4) {
                if (parent.hasSubtree()) {
                    return; // We have a subtree, so just return
                }

                isLeaf = true; // No subtree, so set all children to null
            }
            for (var i = 0; i < 4; ++i) {
                var childKey = parentKey + i.toString();
                if (isLeaf) {
                    // No subtree so set all children to null
                    tileInfo[childKey] = null;
                } else if (level < 4) {
                    // We are still in the middle of the subtree, so add child
                    //  only if their bits are set, otherwise set child to null.
                    if (!parent.hasChild(i)) {
                        tileInfo[childKey] = null;
                    } else {
                        if (index === numInstances) {
                            console.log('Incorrect number of instances');
                            return;
                        }

                        var instance = instances[index++];
                        tileInfo[childKey] = instance;
                        populateTiles(childKey, instance, level + 1);
                    }
                }
            }
        }

        var level = 0;
        var root = instances[index++];
        if (quadKey === '') {
            // Root tile has data at its root and one less level
            ++level;
        } else {
            tileInfo[quadKey] = root; // This will only contain the child bitmask
        }

        populateTiles(quadKey, root, level);

        return tileInfo;
    }

    function processTerrain(buffer, totalSize, transferableObjects) {
        var dv = new DataView(buffer);

        var offset = 0;
        var terrainTiles = [];
        while (offset < totalSize) {
            // Each tile is split into 4 parts
            var tileStart = offset;
            for (var quad = 0; quad < 4; ++quad) {
                var size = dv.getUint32(offset, true);
                offset += sizeOfUint32;
                offset += size;
            }
            var tile = buffer.slice(tileStart, offset);
            transferableObjects.push(tile);
            terrainTiles.push(tile);
        }

        return terrainTiles;
    }

    var compressedMagic$1 = 0x7468dead;
    var compressedMagicSwap$1 = 0xadde6874;

    function uncompressPacket(data) {
        // The layout of this decoded data is
        // Magic Uint32
        // Size Uint32
        // [GZipped chunk of Size bytes]

        // Pullout magic and verify we have the correct data
        var dv = new DataView(data);
        var offset = 0;
        var magic = dv.getUint32(offset, true);
        offset += sizeOfUint32;
        if (magic !== compressedMagic$1 && magic !== compressedMagicSwap$1) {
            throw new RuntimeError.RuntimeError('Invalid magic');
        }

        // Get the size of the compressed buffer - the endianness depends on which magic was used
        var size = dv.getUint32(offset, (magic === compressedMagic$1));
        offset += sizeOfUint32;

        var compressedPacket = new Uint8Array(data, offset);
        var uncompressedPacket = pako_inflate.pako.inflate(compressedPacket);

        if (uncompressedPacket.length !== size) {
            throw new RuntimeError.RuntimeError('Size of packet doesn\'t match header');
        }

        return uncompressedPacket;
    }
    var decodeGoogleEarthEnterprisePacket$1 = createTaskProcessorWorker(decodeGoogleEarthEnterprisePacket);

    return decodeGoogleEarthEnterprisePacket$1;

});
