import { BufferGeometry, MeshBasicMaterial, MeshMatcapMaterial, MeshStandardMaterial } from "three";
import { assert } from "../../../../../../core/assert.js";
import { m3_rm_compose_transform } from "../../../../../../core/geom/mat3/m3_rm_compose_transform.js";
import { GameAssetType } from "../../../../../asset/GameAssetType.js";
import Entity from "../../../../../ecs/Entity.js";
import { Transform } from "../../../../../ecs/transform/Transform.js";
import { InterpolationType } from "../../../../../navigation/ecs/components/InterpolationType.js";
import { ShadedGeometry } from "../../../mesh-v2/ShadedGeometry.js";
import { ShadedGeometryFlags } from "../../../mesh-v2/ShadedGeometryFlags.js";
import { TubeMaterialType } from "../TubeMaterialType.js";
import { build_geometry_catmullrom } from "./build_geometry_catmullrom.js";
import { build_geometry_linear } from "./build_geometry_linear.js";
import { compute_smooth_profile_normals } from "./compute_smooth_profile_normals.js";
import { fix_shape_normal_order } from "./fix_shape_normal_order.js";

/**
 *
 * @param {number} segment_start
 * @param {number} segment_end
 * @returns {BufferGeometry}
 * @param {Path} path_component
 * @param {TubePathStyle} style
 * @param {number[]|Float32Array} shape_normal
 * @param {number[]|Float32Array} shape
 * @param {number[]|Float32Array} shape_transform
 */
function make_geometry_segment(
    path_component, style,
    shape, shape_normal, shape_transform,
    segment_start, segment_end
) {
    assert.notNaN(segment_start, 'segment_start');
    assert.notNaN(segment_end, 'segment_end');
    assert.greaterThanOrEqual(segment_end, segment_start, 'segment_start must be <= segment_end');

    const interpolation = path_component.interpolation;

    if (interpolation === InterpolationType.Linear) {
        return build_geometry_linear(path_component, style, shape, shape_normal, shape_transform, segment_start, segment_end);
    } else if (interpolation === InterpolationType.CatmullRom) {
        return build_geometry_catmullrom(path_component, style, shape, shape_normal, shape_transform, segment_start, segment_end);
    } else {
        throw new Error(`Unsupported interpolation type '${interpolation}'`);
    }

}

/**
 *
 * @param {TubePathStyle} style
 * @param {AssetManager} am
 * @returns {Material|MeshStandardMaterial|MeshMatcapMaterial|MeshBasicMaterial}
 */
function make_material(style, am) {
    let material_def;

    const material_type = style.material_type;

    if (material_type === TubeMaterialType.Basic) {
        material_def = new MeshBasicMaterial();
    } else if (material_type === TubeMaterialType.Standard) {
        material_def = new MeshStandardMaterial({
            metalness: style.material.metalness,
            roughness: style.material.roughness,
        });
    } else if (material_type === TubeMaterialType.Matcap) {

        material_def = new MeshMatcapMaterial();

        am.get({
            path: style.material.texture, type: GameAssetType.Texture, callback: asset => {
                material_def.matcap = asset.create();

                material_def.needsUpdate = true;
            }, failure: console.error
        });

    } else {
        throw new Error(`Unsupported material type '${material_type}'`);
    }

    return material_def;
}

export class TubePathBuilder {
    constructor() {
        /**
         *
         * @type {TubePathStyle|null}
         */
        this.style = null;

        /**
         *
         * @type {Path|null}
         */
        this.path = null;

        /**
         *
         * @type {MaterialManager|null}
         */
        this.materials = null;

        /**
         *
         * @type {AssetManager|null}
         */
        this.assetManager = null;
    }

    setPath(path) {
        this.path = path;
    }

    setStyle(style) {
        this.style = style;
    }

    setMaterials(m) {
        this.materials = m;
    }

    setAssetManager(assetManager) {
        this.assetManager = assetManager;
    }

    /**
     *
     * @param {Entity[]} destination
     */
    build(destination) {
        const style = this.style;

        // generate three.js curve from path
        const path_component = this.path;

        const pointCount = path_component.getPointCount();

        if (pointCount < 2) {
            // no curve possible without at least 2 points
            return;
        }

        const material_def = make_material(style, this.assetManager);

        material_def.color.set(style.color.toUint());

        if (style.opacity < 1) {
            material_def.transparent = true;
            material_def.opacity = style.opacity;
        }

        const segment_count = style.path_mask.length / 2;

        const shape = fix_shape_normal_order(style.shape);

        let shape_normals = style.shape_normals;
        if (shape_normals === undefined || shape_normals === null) {
            shape_normals = new Float32Array(shape.length);
            compute_smooth_profile_normals(shape, shape_normals);
        }

        const shape_transform = new Float32Array(9);
        m3_rm_compose_transform(shape_transform, 0, 0, style.width, style.width, 0, 0, 0);

        for (let i = 0; i < segment_count; i++) {
            const i2 = i * 2;

            let segment_start = style.path_mask[i2];
            let segment_end = style.path_mask[i2 + 1];

            // check for wrong ordering
            if (segment_end < segment_start) {
                // violated interval constraint, re-order
                const t = segment_end;

                segment_end = segment_start;
                segment_start = t;
            }

            const geometry = make_geometry_segment(
                path_component, style,
                shape, shape_normals, shape_transform,
                segment_start, segment_end
            );

            const entityBuilder = new Entity();

            entityBuilder.add(new Transform());

            const shaded_geometry = ShadedGeometry.from(geometry, material_def);

            shaded_geometry.writeFlag(ShadedGeometryFlags.CastShadow, style.cast_shadow);
            shaded_geometry.writeFlag(ShadedGeometryFlags.ReceiveShadow, style.receive_shadow);

            entityBuilder.add(shaded_geometry);

            destination.push(entityBuilder);

        }

    }
}

