Source: package/block/block.js

import { pick, UUIDRegex, URLRegex, unique, getAssigningComponent } from '../../lib.js';
import { Vector3, Quaternion, Group, ArrowHelper, AxesHelper, BufferGeometry, BoxHelper, Matrix4, BufferAttribute,  Math, Box3, Box3Helper, BoxGeometry, MeshBasicMaterial, Mesh } from '../../../node_modules/three/build/three.module.js';
import { checkPropTypes } from '../../lib.js';
import { Reporter } from '../../reporter/reporter.js';
import { Component } from '../../component/component.js';
import { Connector } from '../connector/connector.js';
import { WrappedImage } from '../image/wrapped_image.js';
import { BuildableComponent } from '../component/buildable_component.js';
import { PositionedMesh } from '../mesh/positioned_mesh.js';
import { PositionedMeshGroup } from '../mesh/positioned_mesh_group.js';
import { Project } from '../../project.js';
import { BlockCategory } from './block_category.js';
import { Theme } from '../theme/theme.js';
import { PositionedComponent } from '../component/positioned_component.js';
import { WrappedMesh } from '../mesh/wrapped_mesh.js';
import { MaterialSet } from '../material/material_set.js';



/**
 * @typedef {Object} SubAssignable 
 * @property {PositionedMesh} [positionedMesh]
 * @property {PositionedMeshGroup} [positionedMeshGroup]
 * @property {Array<MaterialSet>} [materialVariantGroups]
 * @property {BuildableComponent} [assigningComponent]
 */


/** 
 * Unit of geometric configuration and pricing
 * Blocks can be connected to other blocks through connectors.
 */
class Block extends BuildableComponent {

    /**
     * @param {Reporter} reporter
     * @param {Object} settings
     * @param {UUID} [settings.id]
     * @param {string} [settings.name]
     * @param {Object} [settings.userData]
     * @param {Array<BlockCategory>} [settings.categories]
     * @param {Theme} [settings.theme]
     * @param {Object} [settings.dimensions]
     * @param {Object} [settings.dimensionsCenter]
     * @param {WrappedImage} [settings.dimensionsImage]
     * @param {WrappedImage} [settings.thumbnail]
     * @param {Array<Connector>} [settings.connectors]
     * @param {Array<PositionedMesh>} [settings.positionedMeshes]
     * @param {Array<PositionedMeshGroup>} [settings.positionedMeshGroups]
     * @param {Boolean} [settings.assignable = true]
     */

    constructor(reporter, settings) {

        // set defaults
        settings.assignable = typeof (settings.assignable) === 'boolean' ? settings.assignable : true;
        settings.dimensions = settings.dimensions !== undefined ? settings.dimensions : new Vector3(1,1,1);
        settings.dimensionsCenter = settings.dimensionsCenter !== undefined ? settings.dimensionsCenter : new Vector3(0.5,0.5,0.5);


        super(
            reporter,
            settings,
            {
                parse: {
                    positionedMeshes: 'integral',
                    positionedMeshGroups: 'integral',
                    connectors: 'integral',
                    categories: 'integral',
                }
            }
        );

        // mf typescript
        if (!this.connectors) {
            /** @type {Array<Connector>} */
            this.connectors = [];
        }

        checkPropTypes(
            settings,
            {},
            {
                theme: Theme,

                connectors: val =>
                    Array.isArray(val) &&
                    val.every(cn => cn instanceof Connector),

                positionedMeshes: val =>
                    Array.isArray(val) &&
                    val.every(m => m instanceof PositionedMesh),

                positionedMeshGroups: val =>
                    Array.isArray(val) &&
                    val.every(em => em instanceof PositionedMeshGroup),

                categories: val =>
                    Array.isArray(val) &&
                    val.every(em => em instanceof BlockCategory),
            }
        );

        // theme sanity checking
        // block can not have BOTH a theme and themed connectors

        if (settings.theme) {
            const themedConnector = (settings.connectors || []).find(cntr => cntr.themeGroup);
            if (themedConnector) {
                throw new Error(`${this.label} has theme "${settings.theme.name}" AND ${themedConnector.label} with theme group "${themedConnector.themeGroup.name}". This is not allowed.`);
            }
        }

        // set block field on connectors to this object
        for (let connector of settings.connectors || []) {
            connector.block = this;
        }

        // positioned meshes can be be re-used, 
        this.distinctPositionedMeshes = this.allDependencies.filter(dep => dep instanceof PositionedMesh).filter(unique);

        // meshes can be re-used
        this.distinctMeshes = this.distinctPositionedMeshes.map(pm => pm.component).filter(unique);

        this.distinctMVGs = [];
        this.distinctMeshesPerMVG = {};

        for (let mesh of this.distinctMeshes) {
            // only meshes can have a material variant group
            if (mesh.materialVariantGroup) {
                if (!this.distinctMVGs.includes(mesh.materialVariantGroup)) {
                    this.distinctMVGs.push(mesh.materialVariantGroup);
                }

                this.distinctMeshesPerMVG[mesh.materialVariantGroup.id] = [
                    mesh,
                    ...(this.distinctMeshesPerMVG[mesh.materialVariantGroup.id] || [])
                ];
            }
        }

        // no need to filter on unique, because only positioned components can be assignable
        // (beside the block itself) and these are already unique

        this.subAssignables = (settings.positionedMeshes || [])
            .filter(pm => pm.assignable == true)
            .map(apm => ({ positionedMesh: apm }));


        for (let posMeshGroup of settings.positionedMeshGroups || []) {

            // add the positioned mesh group to the list of assignable if appropriate

            if (posMeshGroup.assignable === true) {
                this.subAssignables.push({
                    positionedMeshGroup: posMeshGroup
                });
            }

            // add its assignables dependencies icw the group

            this.subAssignables.push(
                ...posMeshGroup.assignableDependencies.map(assDep => ({
                    positionedMeshGroup: posMeshGroup,
                    positionedMesh: assDep
                }))
            )
        }

        for ( let subAssignable of this.subAssignables ) {
            subAssignable.assigningComponent = getAssigningComponent(subAssignable);
            subAssignable.materialVariantGroups = subAssignable.assigningComponent.applicableMaterialVariantGroups;

            subAssignable.id = `B:${this.slug}`;

            subAssignable.name = this.name || this.slug;

            if ( subAssignable.positionedMeshGroup) {
                subAssignable.name += ' ' + subAssignable.positionedMeshGroup.slug;
                subAssignable.id = `-MG:${subAssignable.positionedMeshGroup.slug}`;
            }

            if ( subAssignable.positionedMesh) {
                subAssignable.name += ' ' + subAssignable.positionedMesh.name || subAssignable.positionedMesh.slug;
                subAssignable.id = `-M:${subAssignable.positionedMeshGroup.slug}`;
            }
        }

        
        //setup min & max
        //const boxMin = new Vector3( 0,0,0)
        
        const boxMin = new Vector3( -this.settings.dimensions.x/2, -this.settings.dimensions.y/2, -this.settings.dimensions.z/2 )
        const boxMax = new Vector3( this.settings.dimensions.x/2, this.settings.dimensions.y/2, this.settings.dimensions.z/2 )

        //Math.round( line.distance() * 100 ) 

        //create boundingbox
        const boundingBox = new Box3( boxMin, boxMax  )

        //create vertices
        const vertices = new Float32Array( [
            boundingBox.min.x, boundingBox.min.y, boundingBox.min.z,
            boundingBox.min.x, boundingBox.min.y, boundingBox.max.z,
            boundingBox.max.x, boundingBox.min.y, boundingBox.max.z,
            boundingBox.max.x, boundingBox.min.y, boundingBox.min.z,
            boundingBox.min.x, boundingBox.max.y, boundingBox.min.z,
            boundingBox.min.x, boundingBox.max.y, boundingBox.max.z,
            boundingBox.max.x, boundingBox.max.y, boundingBox.max.z,
            boundingBox.max.x, boundingBox.max.y, boundingBox.min.z
        ] );

        //create geometry
        const boundingBoxGeometry = new BufferGeometry()
        boundingBoxGeometry.name = "boundingBox";
        boundingBoxGeometry.visible = false;
        boundingBoxGeometry.setAttribute( 'position', new BufferAttribute( vertices, 3 ) );

        //console.log( boundingBoxGeometry )
        

        const matrix = new Matrix4();
        const qat = new Quaternion();
        const scale = new Vector3( 1, 1, 1)
        matrix.compose( this.settings.dimensionsCenter, qat, scale )

        //console.log( matrix )

        boundingBoxGeometry.applyMatrix4( matrix )
        //boundingBoxGeometry.computeBoundingBox()

        //boundingBoxGeometry.layers.set( Project.layerMap.boundingBox );
        this.boundingBox = boundingBoxGeometry
        //console.log( this.boundingBox.boundingBox )
  
    }

    static _exportName = {
        singular: 'block',
        plural: 'blocks'
    };


    /**
     * All positioned meshes that are (directly and indirectly) referenced in this block
     * @type {Array<PositionedMesh>}
     */

    distinctPositionedMeshes;


    /**
     * All (wrapped) meshes that are (indirectly) referenced in this block
     * @type {Array<WrappedMesh>}
     */

    distinctMeshes;


    /**
     * Material variant groups that are referenced by at least one of
     * the meshes in this block
     * @type {Array<MaterialSet>}
     */

    distinctMVGs;


    /** @type {Object<UUID,Array<PositionedMesh>>} */

    distinctMeshesPerMVG;




    /**
     * The assignables this block offers, includes the block itself if it is assignable
     * @type {Array<SubAssignable>} 
     */

    subAssignables;



    _onTreeSet() {
        // if (this._tree.materialSets) {
        //     for (let materialSet of this._tree.materialSets) {

        //         // the meshes in the blockInstance that will go in the Configuration
        //         // are all wrapped in a PositionedMesh class and will have a reference
        //         // to the original positionedMesh object in their userData


        //         this.meshesPerMaterialSet[materialSet] = this.allPositionedMeshes.filter(
        //             posMesh => posMesh.component.materialVariantGroup === materialSet
        //         );
        //     }
        // }
    }




    /**
     * Build
     * @param {ComponentPart} part
     * @param {LoadingQuality} quality
     * @param {Object<string,Component>} dependencies
     * @returns {Promise<Block>}
     */

    async _build(part, quality, dependencies) {

        switch (part) {

            case 'UI':

                //console.log( 'build UI')

                if (this._settings.thumbnail) {
                    this._setContent('UI', quality, this._settings.thumbnail.content.main[quality]);
                }
                else {
                    this._setContent('UI', quality, Project.defaultImages.missing.cloneNode(true));
                }

                break;

            case 'main':

                /** @type {Group} */

                const block = new Group();

                block.castShadow = true;
                block.receiveShadow = true;

                //console.log( this.boundingBox.boundingBox )
                //const boxHelper = new Box3Helper( this.boundingBox.boundingBox, 0xffff00 ) //-> helpers kunnen nog nie gecloned worden!
                //block.add(  boxHelper )

                // console.log( this.settings )

                //HELPER - BASE POINT
                const axesHelper = new AxesHelper(5);

                //HELPER - ARROW -> CONNECTOR
                const connectorGroup = new Group()
                connectorGroup.name = "connectors"

                const dir = new Vector3(0, 0, 0.1);
                dir.normalize();                                                                //normalize the direction vector (convert to vector of length 1)
                const origin = new Vector3(0, 0, 0);                                          // origin of the arrowHelper (not able to set oafter creation -> use position instead)
                const length = 0.15;                                                             // length of the arrowHelper
                const hex = 0xff0000;                                                           // color of the arrowHelper
                const headLength = 0.05                                                         // The length of the head of the arrow. Default is 0.2 * length.                                               
                const headWidth = 0.05                                                          // The width of the head of the arrow. Default is 0.2 * headLength.
                const arrowHelper = new ArrowHelper(dir, origin, length, hex, 0.05, 0.05);   // arrowhelper template


                
                
  
                //HELPER - ARROW -> place helpers in block group
                // for (var i = 0; i < this.settings.connectors.length; i++) {

                //     //get the connector data
                //     //console.log( this.settings) 
                //     const conn = this.settings.connectors[i].content.main[quality]  //get the connector   
                //     //console.log( conn )                         
                //     const newDir = new Vector3(0, 0, 2);                            //create new vector, deze moet groter zijn dan 1! zit anders een bug in dat die de vector niet negatief kan roteren!
                //     newDir.applyQuaternion(conn.quaternion)                         //apply connector rotation to vector  
                //     //console.log( newDir )
                //     const newArrowHelper = arrowHelper.clone()                      //create copy of the arrow helper template                        
                //     newArrowHelper.position.copy(conn.position);                    //copy the position of the connector to the arrowhelper
                //     newArrowHelper.setDirection(newDir)
                //     //newArrowHelper.layers.set( layerMap.connectorHelpers )

                //     connectorGroup.add(newArrowHelper)
                //     //connectorGroup.layers.set( layerMap.connectorHelpers )

                //     //block.add(connectorGroup)

                // }


                // clone and add the MeshGroups to the block
                // and position correcty

                if (this._settings.positionedMeshGroups) {

                    /** @type {Array<PositionedMeshGroup>} */

                    const positionedMeshGroups = Object.values(this._settings.positionedMeshGroups || {});

                    if (this._debug) {
                        console.log('PosMeshGroups groups', positionedMeshGroups);
                    }

                    for (let positionedMeshGroup of positionedMeshGroups) {

                        /** @type {Group} */

                        const positionedMeshGroupClone = positionedMeshGroup.content.main[quality].clone();

                        block.add(positionedMeshGroupClone);
                    }
                }



                // clone and add the (Wrapped)Meshes to the group
                // and position correcty

                if (this._settings.positionedMeshes) {

                    /** @type {Array<PositionedMesh>} */

                    const positionedMeshes = Object.values(this._settings.positionedMeshes);

                    // console.log( this._settings.positionedMeshes )

                    if (this._debug) {
                        console.log('Positioned meshed', positionedMeshes);
                    }

                    for (let positionedMesh of positionedMeshes) {

                        /** @type {THREE.Mesh} */

                        const positionedMeshClone = positionedMesh.content.main[quality].clone();

                        positionedMeshClone.castShadow = true;
                        positionedMeshClone.receiveShadow = true;

                        block.add(positionedMeshClone);
                    }
                }


                // export the block and its connectors

                const content = {
                    block,
                    connectors: this._settings.connectors || []
                }

                // console.log( content)

                this._setContent('main', quality, content);

                break;
            
            case 'specs':

                //console.log( 'build specs')

                if (this._settings.dimensionsImage) {
                    this._setContent('specs', quality, this._settings.dimensionsImage.content.main[quality]);
                }
                else {
                    this._setContent('specs', quality, Project.defaultImages.missing.cloneNode(true));
                }

                break;

            default:

                this._setContent(part, quality, null);
                
                break;
        }
        return this;
    }

}

export { Block };