import { Vec } from "../math/Vec";
import { Mat3 } from "../math/Mat3";
import { Mat4 } from "../math/Mat4";
import { v3, Vec3 } from "../math/Vec3";
import { Box } from "../struct/3d/Box";
import { Sphere } from "../struct/3d/Sphere";
import { BufferAttribute, Float32BufferAttribute, Uint16BufferAttribute, Uint32BufferAttribute } from "./buffer-attribute";
import { isBufferArray, TypedArray } from "./types";
import { Vec2 } from "../math/Vec2";
import { Vec4 } from "../math/Vec4";
import { verctorToNumbers } from "../alg/common";


export interface IGeometry {
    position: number[];
    normal?: number[];
    index?: number[];
    uv?: number[];
    uv2?: number[];
    tangent?: number[];
}

export interface IBufferGeometry {
    position: Float32Array | undefined;
    index?: Uint32Array | Uint16Array
    normal?: Float32Array;
    uv?: Float32Array;
    uv2?: Float32Array;
    tangent?: Float32Array;
}


var _bufferGeometryId = 1; // BufferGeometry uses odd numbers as Id

var _m1 = new Mat4();
var _offset = new Vec3();
var _box = new Box();
var _boxMorphTargets = new Box();
var _vector = new Vec3();

/**
 * BufferType 几何体，用于独立计算几何体
 */
export class BufferGeometry {
    name: string;
    index: BufferAttribute | undefined;
    morphAttributes: any;
    morphTargetsRelative: boolean;
    groups: { start: number; count: number; materialIndex?: number }[];
    boundingBox: Box | undefined;
    boundingSphere: Sphere | undefined;
    drawRange: { start: number; count: number; };
    attributes: { [key: string]: BufferAttribute };
    parameters: any;
    readonly isBufferGeometry: true = true;
    uuid: string = "";
    type: string = "BufferGeometry";
    constructor() {

        Object.defineProperty(this, 'id', { value: _bufferGeometryId += 2 });

        this.name = '';

        this.attributes = {};

        this.morphAttributes = {};
        this.morphTargetsRelative = false;

        this.groups = [];

        this.drawRange = { start: 0, count: Infinity };
    }

    /**
     * 转化成BufferArray来计算
     * @param geo 
     */
    setFromGeometry(geo: IGeometry) {
        this.setAttribute('position', new Float32BufferAttribute(geo.position, 3))
        if (geo.uv)
            this.setAttribute('uv', new Float32BufferAttribute(geo.uv, 2));

        if (geo.index)
            this.setIndex(geo.index)

        return this;
    }

    getIndex() {

        return this.index;

    }

    setIndex(index: BufferAttribute | TypedArray | number[]) {
        if (Array.isArray(index)) {
            this.index = new (Vec.max(index as any) > 65535 ? Uint32BufferAttribute : Uint16BufferAttribute)(index, 1);
        } else if (index instanceof BufferAttribute) {
            this.index = index;
        } else {
            this.index = new (Vec.max(index as any) > 65535 ? Uint32BufferAttribute : Uint16BufferAttribute)(index, 1)
        }

    }

    getAttribute(name: string) {

        return this.attributes[name];

    }

    setAttribute(name: string, attribute: BufferAttribute) {

        this.attributes[name] = attribute;
        return this;

    }

    addAttribute(name: string, attribute: BufferAttribute | TypedArray | Array<number | Vec2 | Vec3 | Vec4>, itemSize: number = 1) {
        if (Array.isArray(attribute)) {
            if (attribute[0] instanceof Vec2) {
                var nums = verctorToNumbers(attribute);
                this.setAttribute(name, new Float32BufferAttribute(nums, 2))
            } else if (attribute[0] instanceof Vec3) {
                var nums = verctorToNumbers(attribute);
                this.setAttribute(name, new Float32BufferAttribute(nums, 3))
            } else if (attribute[0] instanceof Vec4) {
                var nums = verctorToNumbers(attribute);
                this.setAttribute(name, new Float32BufferAttribute(nums, 4))
            } else if (!isNaN(attribute[0])) {
                this.setAttribute(name, new Float32BufferAttribute(attribute, itemSize));
            } else {
                console.error("类型不存在");
            }
        }
        else if (attribute instanceof BufferAttribute) {
            this.attributes[name] = attribute;
        } else if (isBufferArray(attribute)) {
            this.setAttribute(name, new BufferAttribute(attribute as any, itemSize))
        }

        return this;
    }

    deleteAttribute(name: string) {

        delete this.attributes[name];
        return this;

    }

    addGroup(start: number, count: number, materialIndex?: number) {

        this.groups.push({

            start: start,
            count: count,
            materialIndex: materialIndex !== undefined ? materialIndex : 0

        });

    }

    clearGroups() {

        this.groups = [];

    }

    setDrawRange(start: number, count: number) {

        this.drawRange.start = start;
        this.drawRange.count = count;

    }

    applyMat4(matrix: Mat4) {

        var position = this.attributes.position;

        if (position !== undefined) {

            position.applyMat4(matrix);

            position.needsUpdate = true;

        }

        var normal = this.attributes.normal;

        if (normal !== undefined) {

            var normalMatrix = new Mat3().getNormalMat(matrix);

            normal.applyNormalMat(normalMatrix);

            normal.needsUpdate = true;

        }

        var tangent = this.attributes.tangent;

        if (tangent !== undefined) {

            tangent.transformDirection(matrix);

            tangent.needsUpdate = true;

        }

        if (!this.boundingBox) {

            this.computeBoundingBox();

        }

        if (!this.boundingSphere) {

            this.computeBoundingSphere();

        }

        return this;

    }

    rotateX(angle: number) {

        // rotate geometry around world x-axis

        _m1.makeRotationX(angle);

        this.applyMat4(_m1);

        return this;

    }

    rotateY(angle: number) {

        // rotate geometry around world y-axis

        _m1.makeRotationY(angle);

        this.applyMat4(_m1);

        return this;

    }

    rotateZ(angle: number) {

        // rotate geometry around world z-axis

        _m1.makeRotationZ(angle);

        this.applyMat4(_m1);

        return this;

    }

    translate(x: number, y: number, z: number) {

        // translate geometry

        _m1.makeTranslation(x, y, z);

        this.applyMat4(_m1);

        return this;

    }

    scale(x: number, y: number, z: number) {

        // scale geometry

        _m1.makeScale(x, y, z);

        this.applyMat4(_m1);

        return this;

    }

    lookAt(vector: Vec3) {

        _m1.lookAt(v3(), vector, Vec3.UnitY);

        this.applyMat4(_m1);

        return this;

    }

    center() {

        this.computeBoundingBox();

        this.boundingBox!.getCenter(_offset).negate();

        this.translate(_offset.x, _offset.y, _offset.z);

        return this;

    }

    setFromObject(object: any) {

        // console.log( 'THREE.BufferGeometry.setFromObject(). Converting', object, this );

        var geometry = object.geometry;

        if (object.isPoints || object.isLine) {

            var positions = new Float32BufferAttribute(geometry.vertices.length * 3, 3);
            var colors = new Float32BufferAttribute(geometry.colors.length * 3, 3);

            this.setAttribute('position', positions.copyVec3sArray(geometry.vertices));
            this.setAttribute('color', colors.copyColorsArray(geometry.colors));

            if (geometry.lineDistances && geometry.lineDistances.length === geometry.vertices.length) {

                var lineDistances = new Float32BufferAttribute(geometry.lineDistances.length, 1);

                this.setAttribute('lineDistance', lineDistances.copyArray(geometry.lineDistances));

            }

            if (geometry.boundingSphere !== null) {

                this.boundingSphere = geometry.boundingSphere.clone();

            }

            if (geometry.boundingBox !== null) {

                this.boundingBox = geometry.boundingBox.clone();

            }

        } else if (object.isMesh) {

            // if (geometry && geometry.isGeometry) {

            //     this.fromGeometry(geometry);

            // }

        }

        return this;

    }

    setFromPoints(points: Vec3[]) {

        var position = [];

        for (var i = 0, l = points.length; i < l; i++) {

            var point = points[i];
            position.push(point.x, point.y, point.z || 0);

        }

        this.setAttribute('position', new Float32BufferAttribute(position, 3));

        return this;

    }

    updateFromObject(object: any) {

        var geometry = object.geometry;

        if (object.isMesh) {

            var direct = geometry.__directGeometry;

            if (geometry.elementsNeedUpdate === true) {

                direct = undefined;
                geometry.elementsNeedUpdate = false;

            }

            // if (direct === undefined) {

            //     return this.fromGeometry(geometry);

            // }

            direct.verticesNeedUpdate = geometry.verticesNeedUpdate;
            direct.normalsNeedUpdate = geometry.normalsNeedUpdate;
            direct.colorsNeedUpdate = geometry.colorsNeedUpdate;
            direct.uvsNeedUpdate = geometry.uvsNeedUpdate;
            direct.groupsNeedUpdate = geometry.groupsNeedUpdate;

            geometry.verticesNeedUpdate = false;
            geometry.normalsNeedUpdate = false;
            geometry.colorsNeedUpdate = false;
            geometry.uvsNeedUpdate = false;
            geometry.groupsNeedUpdate = false;

            geometry = direct;

        }

        var attribute;

        if (geometry.verticesNeedUpdate === true) {

            attribute = this.attributes.position;

            if (attribute !== undefined) {

                attribute.copyVec3sArray(geometry.vertices);
                attribute.needsUpdate = true;

            }

            geometry.verticesNeedUpdate = false;

        }

        if (geometry.normalsNeedUpdate === true) {

            attribute = this.attributes.normal;

            if (attribute !== undefined) {

                attribute.copyVec3sArray(geometry.normals);
                attribute.needsUpdate = true;

            }

            geometry.normalsNeedUpdate = false;

        }

        if (geometry.colorsNeedUpdate === true) {

            attribute = this.attributes.color;

            if (attribute !== undefined) {

                attribute.copyColorsArray(geometry.colors);
                attribute.needsUpdate = true;

            }

            geometry.colorsNeedUpdate = false;

        }

        if (geometry.uvsNeedUpdate) {

            attribute = this.attributes.uv;

            if (attribute !== undefined) {

                attribute.copyVec2sArray(geometry.uvs);
                attribute.needsUpdate = true;

            }

            geometry.uvsNeedUpdate = false;

        }

        if (geometry.lineDistancesNeedUpdate) {

            attribute = this.attributes.lineDistance;

            if (attribute !== undefined) {

                attribute.copyArray(geometry.lineDistances);
                attribute.needsUpdate = true;

            }

            geometry.lineDistancesNeedUpdate = false;

        }

        if (geometry.groupsNeedUpdate) {

            geometry.computeGroups(object.geometry);
            this.groups = geometry.groups;

            geometry.groupsNeedUpdate = false;

        }

        return this;

    }

    // fromGeometry(geometry: any) {

    //     geometry.__directGeometry = new DirectGeometry().fromGeometry(geometry);

    //     return this.fromDirectGeometry(geometry.__directGeometry);

    // }

    // fromDirectGeometry(geometry) {

    //     var positions = new Float32Array(geometry.vertices.length * 3);
    //     this.setAttribute('position', new BufferAttribute(positions, 3).copyVec3sArray(geometry.vertices));

    //     if (geometry.normals.length > 0) {

    //         var normals = new Float32Array(geometry.normals.length * 3);
    //         this.setAttribute('normal', new BufferAttribute(normals, 3).copyVec3sArray(geometry.normals));

    //     }

    //     if (geometry.colors.length > 0) {

    //         var colors = new Float32Array(geometry.colors.length * 3);
    //         this.setAttribute('color', new BufferAttribute(colors, 3).copyColorsArray(geometry.colors));

    //     }

    //     if (geometry.uvs.length > 0) {

    //         var uvs = new Float32Array(geometry.uvs.length * 2);
    //         this.setAttribute('uv', new BufferAttribute(uvs, 2).copyVec2sArray(geometry.uvs));

    //     }

    //     if (geometry.uvs2.length > 0) {

    //         var uvs2 = new Float32Array(geometry.uvs2.length * 2);
    //         this.setAttribute('uv2', new BufferAttribute(uvs2, 2).copyVec2sArray(geometry.uvs2));

    //     }

    //     // groups

    //     this.groups = geometry.groups;

    //     // morphs

    //     for (var name in geometry.morphTargets) {

    //         var array = [];
    //         var morphTargets = geometry.morphTargets[name];

    //         for (var i = 0, l = morphTargets.length; i < l; i++) {

    //             var morphTarget = morphTargets[i];

    //             var attribute = new Float32BufferAttribute(morphTarget.data.length * 3, 3);
    //             attribute.name = morphTarget.name;

    //             array.push(attribute.copyVec3sArray(morphTarget.data));

    //         }

    //         this.morphAttributes[name] = array;

    //     }

    //     // skinning

    //     if (geometry.skinIndices.length > 0) {

    //         var skinIndices = new Float32BufferAttribute(geometry.skinIndices.length * 4, 4);
    //         this.setAttribute('skinIndex', skinIndices.copyVec4sArray(geometry.skinIndices));

    //     }

    //     if (geometry.skinWeights.length > 0) {

    //         var skinWeights = new Float32BufferAttribute(geometry.skinWeights.length * 4, 4);
    //         this.setAttribute('skinWeight', skinWeights.copyVec4sArray(geometry.skinWeights));

    //     }

    //     //

    //     if (geometry.boundingSphere !== null) {

    //         this.boundingSphere = geometry.boundingSphere.clone();

    //     }

    //     if (geometry.boundingBox !== null) {

    //         this.boundingBox = geometry.boundingBox.clone();

    //     }

    //     return this;

    // }

    computeBoundingBox() {

        if (!this.boundingBox) {
            this.boundingBox = new Box();
        }

        var position = this.attributes.position;
        var morphAttributesPosition = this.morphAttributes.position;

        if (position) {

            this.boundingBox!.setFromBufferAttribute(position);

            // process morph attributes if present

            if (morphAttributesPosition) {

                for (var i = 0, il = morphAttributesPosition.length; i < il; i++) {

                    var morphAttribute = morphAttributesPosition[i];
                    _box.setFromBufferAttribute(morphAttribute);

                    if (this.morphTargetsRelative) {

                        _vector.addVecs(this.boundingBox.min, _box.min);
                        this.boundingBox.expandByPoint(_vector);

                        _vector.addVecs(this.boundingBox.max, _box.max);
                        this.boundingBox.expandByPoint(_vector);

                    } else {

                        this.boundingBox.expandByPoint(_box.min);
                        this.boundingBox.expandByPoint(_box.max);

                    }

                }

            }

        } else {

            this.boundingBox.makeEmpty();

        }

        if (isNaN(this.boundingBox.min.x) || isNaN(this.boundingBox.min.y) || isNaN(this.boundingBox.min.z)) {

            console.error('THREE.BufferGeometry.computeBoundingBox: Computed min/max have NaN values. The "position" attribute is likely to have NaN values.', this);

        }

    }

    computeBoundingSphere() {

        if (!this.boundingSphere) {

            this.boundingSphere = new Sphere();

        }

        var position = this.attributes.position;
        var morphAttributesPosition = this.morphAttributes.position;

        if (position) {

            // first, find the center of the bounding sphere

            var center = this.boundingSphere!.center;

            _box.setFromBufferAttribute(position);

            // process morph attributes if present

            if (morphAttributesPosition) {

                for (var i = 0, il: number = morphAttributesPosition.length; i < il; i++) {

                    var morphAttribute = morphAttributesPosition[i];
                    _boxMorphTargets.setFromBufferAttribute(morphAttribute);

                    if (this.morphTargetsRelative) {

                        _vector.addVecs(_box.min, _boxMorphTargets.min);
                        _box.expandByPoint(_vector);

                        _vector.addVecs(_box.max, _boxMorphTargets.max);
                        _box.expandByPoint(_vector);

                    } else {

                        _box.expandByPoint(_boxMorphTargets.min);
                        _box.expandByPoint(_boxMorphTargets.max);

                    }

                }

            }

            _box.getCenter(center);

            // second, try to find a boundingSphere with a radius smaller than the
            // boundingSphere of the boundingBox: sqrt(3) smaller in the best case

            var maxRadiusSq = 0;

            for (var i = 0, il: number = position.count; i < il; i++) {

                _vector.fromBufferAttribute(position, i);

                maxRadiusSq = Math.max(maxRadiusSq, center!.distanceToSquared(_vector));

            }

            // process morph attributes if present

            if (morphAttributesPosition) {

                for (var i = 0, il: number = morphAttributesPosition.length; i < il; i++) {

                    var morphAttribute = morphAttributesPosition[i];
                    var morphTargetsRelative = this.morphTargetsRelative;

                    for (var j = 0, jl = morphAttribute.count; j < jl; j++) {

                        _vector.fromBufferAttribute(morphAttribute, j);

                        if (morphTargetsRelative) {

                            _offset.fromBufferAttribute(position, j);
                            _vector.add(_offset);

                        }

                        maxRadiusSq = Math.max(maxRadiusSq, center!.distanceToSquared(_vector));

                    }

                }

            }

            this.boundingSphere!.radius = Math.sqrt(maxRadiusSq);

            if (isNaN(this.boundingSphere!.radius)) {

                console.error('THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values.', this);

            }

        }

    }

    computeFaceNormals() {

        // backwards compatibility

    }

    computeVertexNormals() {

        var index = this.index;
        var attributes = this.attributes;

        if (attributes.position) {

            var positions = attributes.position.array;

            if (attributes.normal === undefined) {

                this.setAttribute('normal', new BufferAttribute(new Float32Array(positions.length), 3));

            } else {

                // reset existing normals to zero

                var array = attributes.normal.array;

                for (var i = 0, il = array.length; i < il; i++) {

                    array[i] = 0;

                }

            }

            var normals = attributes.normal.array;

            var vA, vB, vC;
            var pA = new Vec3(), pB = new Vec3(), pC = new Vec3();
            var cb = new Vec3(), ab = new Vec3();

            // indexed elements

            if (index) {

                var indices = index.array;

                for (var i = 0, il = index.count; i < il; i += 3) {

                    vA = indices[i + 0] * 3;
                    vB = indices[i + 1] * 3;
                    vC = indices[i + 2] * 3;

                    pA.fromArray(positions, vA);
                    pB.fromArray(positions, vB);
                    pC.fromArray(positions, vC);

                    cb.subVecs(pC, pB);
                    ab.subVecs(pA, pB);
                    cb.cross(ab);

                    normals[vA] += cb.x;
                    normals[vA + 1] += cb.y;
                    normals[vA + 2] += cb.z;

                    normals[vB] += cb.x;
                    normals[vB + 1] += cb.y;
                    normals[vB + 2] += cb.z;

                    normals[vC] += cb.x;
                    normals[vC + 1] += cb.y;
                    normals[vC + 2] += cb.z;

                }

            } else {

                // non-indexed elements (unconnected triangle soup)

                for (var i = 0, il = positions.length; i < il; i += 9) {

                    pA.fromArray(positions, i);
                    pB.fromArray(positions, i + 3);
                    pC.fromArray(positions, i + 6);

                    cb.subVecs(pC, pB);
                    ab.subVecs(pA, pB);
                    cb.cross(ab);

                    normals[i] = cb.x;
                    normals[i + 1] = cb.y;
                    normals[i + 2] = cb.z;

                    normals[i + 3] = cb.x;
                    normals[i + 4] = cb.y;
                    normals[i + 5] = cb.z;

                    normals[i + 6] = cb.x;
                    normals[i + 7] = cb.y;
                    normals[i + 8] = cb.z;

                }

            }

            this.normalizeNormals();

            attributes.normal.needsUpdate = true;

        }

    }

    merge(geometry: BufferGeometry, offset: number) {

        if (!(geometry && geometry.isBufferGeometry)) {

            console.error('THREE.BufferGeometry.merge(): geometry not an instance of THREE.BufferGeometry.', geometry);
            return;

        }

        if (offset === undefined) {

            offset = 0;

            console.warn(
                'THREE.BufferGeometry.merge(): Overwriting original geometry, starting at offset=0. '
                + 'Use BufferGeometryUtils.mergeBufferGeometries() for lossless merge.'
            );

        }

        var attributes = this.attributes;

        for (var key in attributes) {

            if (geometry.attributes[key] === undefined) continue;

            var attribute1 = attributes[key];
            var attributeArray1 = attribute1.array;

            var attribute2 = geometry.attributes[key];
            var attributeArray2 = attribute2.array;

            var attributeOffset = attribute2.itemSize * offset;
            var length = Math.min(attributeArray2.length, attributeArray1.length - attributeOffset);

            for (var i = 0, j = attributeOffset; i < length; i++, j++) {

                attributeArray1[j] = attributeArray2[i];

            }

        }

        return this;

    }

    normalizeNormals() {

        var normals = this.attributes.normal;

        for (var i = 0, il = normals.count; i < il; i++) {

            _vector.x = normals.getX(i);
            _vector.y = normals.getY(i);
            _vector.z = normals.getZ(i);

            _vector.normalize();

            normals.setXYZ(i, _vector.x, _vector.y, _vector.z);

        }

    }

    toFlat() {
        var indices = this.index!.array;
        var attributes = this.attributes;
        var geometry2 = new BufferGeometry();
        function convertBufferAttribute(attribute: BufferAttribute, indices: ArrayLike<number>) {

            var array = attribute.array;
            var itemSize = attribute.itemSize;

            var array2 = new (array as any).constructor(indices.length * itemSize);

            var index = 0, index2 = 0;

            for (var i = 0, l = indices.length; i < l; i++) {

                index = indices[i] * itemSize;

                for (var j = 0; j < itemSize; j++) {

                    array2[index2++] = array[index++];

                }

            }

            return new BufferAttribute(array2, itemSize);

        }

        for (var name in attributes) {

            var attribute = attributes[name];

            var newAttribute = convertBufferAttribute(attribute, indices);

            geometry2.setAttribute(name, newAttribute);

        }

        const indices2 = indices.map((v: number, i: number) => i);

        geometry2.setIndex(indices2);

        return geometry2;
    }

    toNonIndexed() {

        function convertBufferAttribute(attribute: BufferAttribute, indices: ArrayLike<number>) {

            var array = attribute.array;
            var itemSize = attribute.itemSize;

            var array2 = new (array as any).constructor(indices.length * itemSize);

            var index = 0, index2 = 0;

            for (var i = 0, l = indices.length; i < l; i++) {

                index = indices[i] * itemSize;

                for (var j = 0; j < itemSize; j++) {

                    array2[index2++] = array[index++];

                }

            }

            return new BufferAttribute(array2, itemSize);

        }

        //

        if (this.index === undefined) {

            console.warn('THREE.BufferGeometry.toNonIndexed(): Geometry is already non-indexed.');
            return this;

        }

        var geometry2 = new BufferGeometry();

        var indices = this.index!.array;
        var attributes = this.attributes;

        // attributes

        for (var name in attributes) {

            var attribute = attributes[name];

            var newAttribute = convertBufferAttribute(attribute, indices);

            geometry2.setAttribute(name, newAttribute);

        }

        // morph attributes

        var morphAttributes = this.morphAttributes;

        for (name in morphAttributes) {

            var morphArray = [];
            var morphAttribute = morphAttributes[name]; // morphAttribute: array of Float32BufferAttributes

            for (var i = 0, il = morphAttribute.length; i < il; i++) {

                var attribute: BufferAttribute = morphAttribute[i];

                var newAttribute = convertBufferAttribute(attribute, indices);

                morphArray.push(newAttribute);

            }

            geometry2.morphAttributes[name] = morphArray;

        }

        geometry2.morphTargetsRelative = this.morphTargetsRelative;

        // groups

        var groups = this.groups;

        for (var i = 0, l = groups.length; i < l; i++) {

            var group = groups[i];
            geometry2.addGroup(group.start, group.count, group.materialIndex);

        }

        return geometry2;

    }

    toJSON() {

        var data: any = {
            metadata: {
                version: 4.5,
                type: 'BufferGeometry',
                generator: 'BufferGeometry.toJSON'
            }
        };

        // standard BufferGeometry serialization

        data.uuid = this.uuid;
        data.type = this.type;
        if (this.name !== '') data.name = this.name;
        if (Object.keys(this.userData).length > 0) data.userData = this.userData;

        if (this.parameters !== undefined) {

            var parameters = this.parameters;

            for (var key in parameters) {

                if (parameters[key] !== undefined) data[key] = parameters[key];

            }

            return data;

        }

        data.data = { attributes: {} };

        var index = this.index;

        if (index) {

            data.data.index = {
                type: index.array.constructor.name,
                array: Array.prototype.slice.call(index.array)
            };

        }

        var attributes = this.attributes;

        for (var key in attributes) {

            var attribute = attributes[key];

            var attributeData: any = attribute.toJSON();

            if (attribute.name !== '') attributeData.name = attribute.name;

            data.data.attributes[key] = attributeData;

        }

        var morphAttributes: any = {};
        var hasMorphAttributes = false;

        for (var key in this.morphAttributes) {

            var attributeArray = this.morphAttributes[key];

            var array = [];

            for (var i = 0, il = attributeArray.length; i < il; i++) {

                var attribute: BufferAttribute = attributeArray[i];

                var attributeData: any = attribute.toJSON();

                if (attribute.name !== '') attributeData.name = attribute.name;

                array.push(attributeData);

            }

            if (array.length > 0) {

                morphAttributes[key] = array;

                hasMorphAttributes = true;

            }

        }

        if (hasMorphAttributes) {

            data.data.morphAttributes = morphAttributes;
            data.data.morphTargetsRelative = this.morphTargetsRelative;

        }

        var groups = this.groups;

        if (groups.length > 0) {

            data.data.groups = JSON.parse(JSON.stringify(groups));

        }

        var boundingSphere = this.boundingSphere;

        if (boundingSphere) {

            data.data.boundingSphere = {
                center: boundingSphere.center.toArray(),
                radius: boundingSphere.radius
            };

        }

        return data;

    }
    userData(userData: any) {
        throw new Error("Method not implemented.");
    }

    clone() {

        /*
         // Handle primitives
     
         var parameters = this.parameters;
     
         if ( parameters !== undefined ) {
     
         var values = [];
     
         for ( var key in parameters ) {
     
         values.push( parameters[ key ] );
     
         }
     
         var geometry = Object.create( this.constructor.prototype );
         this.constructor.apply( geometry, values );
         return geometry;
     
         }
     
         return new this.constructor().copy( this );
         */

        return new BufferGeometry().copy(this);

    }

    copy(source: BufferGeometry) {

        var name, i, l;

        // reset

        this.attributes = {};
        this.morphAttributes = {};
        this.groups = [];

        // name

        this.name = source.name;

        // index

        var index = source.index;

        if (index) {

            this.setIndex(index.clone());

        }

        // attributes

        var attributes = source.attributes;

        for (name in attributes) {

            var attribute = attributes[name];
            this.setAttribute(name, attribute.clone());

        }

        // morph attributes

        var morphAttributes = source.morphAttributes;

        for (name in morphAttributes) {

            var array = [];
            var morphAttribute = morphAttributes[name]; // morphAttribute: array of Float32BufferAttributes

            for (i = 0, l = morphAttribute.length; i < l; i++) {

                array.push(morphAttribute[i].clone());

            }

            this.morphAttributes[name] = array;

        }

        this.morphTargetsRelative = source.morphTargetsRelative;

        // groups

        var groups = source.groups;

        for (i = 0, l = groups.length; i < l; i++) {

            var group = groups[i];
            this.addGroup(group.start, group.count, group.materialIndex);

        }

        // bounding box

        var boundingBox = source.boundingBox;

        if (boundingBox) {

            this.boundingBox = boundingBox.clone();

        }

        // bounding sphere

        var boundingSphere = source.boundingSphere;

        if (boundingSphere) {

            this.boundingSphere = boundingSphere.clone();

        }

        // draw range

        this.drawRange.start = source.drawRange.start;
        this.drawRange.count = source.drawRange.count;

        // user data

        this.userData = source.userData;

        return this;

    }

}
