import * as THREE from 'three';
import { SimplexNoise } from './utils/SimplexNoise.js';

export class TerrainGenerator {
    constructor(options = {}) {
        this.size = options.size || 1000;
        this.segments = options.segments || 256;
        this.maxHeight = options.maxHeight || 50;
        this.waterLevel = options.waterLevel || 0;
        this.beachWidth = options.beachWidth || 20;
        this.shoreWidth = options.shoreWidth || 3.5;
        this.shoreDepth = options.shoreDepth || 1.5;
        this.underwaterDepthBias = options.underwaterDepthBias || 3.5;
        this.underwaterBiasFadeWidth = options.underwaterBiasFadeWidth || 6;
        this.landBias = options.landBias ?? 0.18;
        this.falloffStartRatio = options.falloffStartRatio || 0.24;
        this.maxLandRatio = options.maxLandRatio || 0.48;
        this.edgeDepth = options.edgeDepth || 10;
        this.coreRadiusRatio = options.coreRadiusRatio || 0.2;
        this.continentLift = options.continentLift || 0.35;
        this.coastVariance = options.coastVariance || 0.08;
        this.outerShelfDepth = options.outerShelfDepth || 2.5;
        this.coastlineBlendWidth = options.coastlineBlendWidth || 32;
        
        this.noise = new SimplexNoise(options.seed || 42);
        this.terrain = null;
    }
    
    generate() {
        const geometry = new THREE.PlaneGeometry(
            this.size,
            this.size,
            this.segments,
            this.segments
        );
        
        const positions = geometry.attributes.position.array;
        const colors = new Float32Array(positions.length);
        
        for (let i = 0; i < positions.length; i += 3) {
            const x = positions[i];
            const y = positions[i + 1];
            
            let height = this.getHeight(x, y);
            positions[i + 2] = height;
            
            const color = this.getTerrainColor(x, y, height);
            colors[i] = color.r;
            colors[i + 1] = color.g;
            colors[i + 2] = color.b;
        }
        
        geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
        geometry.computeVertexNormals();
        geometry.rotateX(-Math.PI / 2);
        
        const material = new THREE.MeshStandardMaterial({
            vertexColors: true,
            roughness: 0.85,
            metalness: 0.0,
            flatShading: false
        });
        
        this.terrain = new THREE.Mesh(geometry, material);
        this.terrain.receiveShadow = true;
        this.terrain.castShadow = true;
        
        return this.terrain;
    }
    
    getHeight(x, y) {
        const scale = 0.0008;
        
        let height = this.noise.fbm(x * scale, y * scale, 3, 2.0, 0.5);
        const detail = this.noise.fbm(x * scale * 3, y * scale * 3, 2, 2.0, 0.5) * 0.2;
        height += detail;
        
        const distFromCenter = Math.sqrt(x * x + y * y);
        const angle = Math.atan2(y, x);
        const coastNoise = this.noise.fbm(
            Math.cos(angle) * 1.7 + 11.3,
            Math.sin(angle) * 1.7 - 4.8,
            3,
            2.0,
            0.5
        );
        const coastlineRadius = this.size * this.maxLandRatio * (1 + coastNoise * this.coastVariance);
        const falloffStart = coastlineRadius * (this.falloffStartRatio / this.maxLandRatio);
        const coreRadius = this.size * this.coreRadiusRatio;

        let continentLift = 1.0;
        if (distFromCenter < coreRadius) {
            const t = distFromCenter / coreRadius;
            continentLift += (1 - t * t) * this.continentLift;
        }
        
        let continentMask = 1.0;
        let edgeDepth = 0;
        if (distFromCenter > falloffStart) {
            const t = (distFromCenter - falloffStart) / Math.max(0.0001, coastlineRadius - falloffStart);
            const easedT = THREE.MathUtils.smoothstep(t, 0, 1);
            continentMask = Math.max(0, 1 - Math.pow(easedT, 1.05));
            edgeDepth = -this.edgeDepth * Math.pow(Math.max(0, t), 2);
        }
        
        height = ((height * continentLift) + this.landBias) * continentMask + edgeDepth;
        height = height * this.maxHeight * 0.5;

        if (distFromCenter > coastlineRadius) {
            const t = THREE.MathUtils.clamp(
                (distFromCenter - coastlineRadius) / this.coastlineBlendWidth,
                0,
                1
            );
            const forcedSeaFloor = this.waterLevel - this.outerShelfDepth * THREE.MathUtils.smoothstep(t, 0, 1);
            const blend = THREE.MathUtils.smootherstep(t, 0, 1);
            height = THREE.MathUtils.lerp(height, Math.min(height, forcedSeaFloor), blend);
        }
        
        const shoreMin = this.waterLevel - this.shoreWidth;
        const shoreMax = this.waterLevel + this.shoreWidth;
        if (height > shoreMin && height < shoreMax) {
            if (height < this.waterLevel) {
                const t = (height - shoreMin) / (this.waterLevel - shoreMin);
                height = this.waterLevel - this.shoreDepth * Math.pow(1 - t, 1.35);
            } else {
                const t = (height - this.waterLevel) / (shoreMax - this.waterLevel);
                height = this.waterLevel + Math.pow(t, 0.75) * this.shoreWidth;
            }
        }

        if (height < this.waterLevel) {
            const underwaterDepth = THREE.MathUtils.clamp(this.waterLevel - height, 0, this.underwaterBiasFadeWidth);
            const biasStrength = THREE.MathUtils.smootherstep(
                underwaterDepth / this.underwaterBiasFadeWidth,
                0,
                1
            );
            height -= this.underwaterDepthBias * biasStrength;
        }

        if (Math.abs(height - this.waterLevel) < 0.06) {
            height = this.waterLevel - 0.06;
        }
        
        return height;
    }
    
    getTerrainColor(x, y, height) {
        const normalizedHeight = (height - this.waterLevel) / this.maxHeight;
        
        const deepWater = new THREE.Color(0x1a3d5c);
        const shallowWater = new THREE.Color(0x2d6b8a);
        const beach = new THREE.Color(0xc2b280);
        const grass = new THREE.Color(0x3d6b3d);
        const darkGrass = new THREE.Color(0x2d4a2d);
        const rock = new THREE.Color(0x5a5a5a);
        const snow = new THREE.Color(0xe8e8e8);
        
        let color = new THREE.Color();
        
        if (height < this.waterLevel - 1.5) {
            color.copy(deepWater);
        } else if (height < this.waterLevel) {
            const shallowBlend = (height - (this.waterLevel - 1.5)) / 1.5;
            color.lerpColors(deepWater, shallowWater, THREE.MathUtils.clamp(shallowBlend, 0, 1));
        } else if (normalizedHeight < 0.08) {
            color.copy(beach);
        } else if (normalizedHeight < 0.18) {
            const sandToGrass = (normalizedHeight - 0.08) / 0.1;
            color.lerpColors(beach, grass, sandToGrass);
        } else if (normalizedHeight < 0.4) {
            const grassBlend = (normalizedHeight - 0.18) / 0.22;
            color.lerpColors(grass, darkGrass, grassBlend);
        } else if (normalizedHeight < 0.6) {
            const rockBlend = (normalizedHeight - 0.4) / 0.2;
            color.lerpColors(darkGrass, rock, rockBlend);
        } else if (normalizedHeight < 0.8) {
            color.copy(rock);
        } else {
            const snowBlend = (normalizedHeight - 0.8) / 0.2;
            color.lerpColors(rock, snow, snowBlend);
        }
        
        const noiseVariation = this.noise.noise2D(x * 0.05, y * 0.05) * 0.1;
        color.r = Math.max(0, Math.min(1, color.r + noiseVariation));
        color.g = Math.max(0, Math.min(1, color.g + noiseVariation));
        color.b = Math.max(0, Math.min(1, color.b + noiseVariation));
        
        return color;
    }
    
    getTerrain() {
        return this.terrain;
    }
    
    getHeightAt(x, z) {
        return this.getHeight(x, -z);
    }
    
    isLand(x, z) {
        return this.getHeight(x, z) > this.waterLevel;
    }
}
