import { useMemo, useRef } from "react";
import { BufferAttribute, BufferGeometry, Color, Mesh } from "three";
import { getTerrainHeight } from "./helpers";
import Grass from "./grass";
import { createNoise2D } from "simplex-noise";

interface TerrainChunkProps {
	x: number;
	y: number;
	size: number;
	resolution: number;
	scale: number;
	hillScale: number;
	hillHeight: number;
	detailScale: number;
	detailHeight: number;
	noise2D: (x: number, y: number) => number;
	grassCount: number;
	grassSize: number;
	grassLOD: number;
	terrainDryColor: string;
	terrainLushColor: string;
	grassDryColor: string;
	grassLushColor: string;
	grassBlades: number;
	grassSegments: number;
	grassLODStart: number;
	grassLODExponent: number;
}
function TerrainChunk({
	x,
	y,
	size,
	resolution,
	scale,
	hillScale,
	hillHeight,
	detailScale,
	detailHeight,
	noise2D,
	grassCount,
	grassSize,
	grassLOD,
	terrainDryColor,
	terrainLushColor,
	grassDryColor,
	grassLushColor,
	grassBlades,
	grassSegments,
	grassLODStart,
	grassLODExponent
}: TerrainChunkProps) {
	const halfSize = size / 2;
	const worldXBase = x * size;
	const worldZBase = y * size;

	const minX = Math.abs(worldXBase) <= halfSize ? 0 : Math.abs(worldXBase) - halfSize;
	const minZ = Math.abs(worldZBase) <= halfSize ? 0 : Math.abs(worldZBase) - halfSize;
	const chunkMinDist = Math.sqrt(minX * minX + minZ * minZ);

	const shouldRenderGrass = chunkMinDist < grassLOD;

	const meshRef = useRef<Mesh>(null);

	const geometry = useMemo(() => {
		const geo = new BufferGeometry();

		const vertices: Array<number> = [];
		const indices: Array<number> = [];

		const step = size / (resolution - 1);
		const halfSize = size / 2;
		const worldXBase = x * size;
		const worldZBase = y * size;

		/*
			vtx gen
		*/
		for (let iz = 0; iz < resolution; iz++) {
			for (let ix = 0; ix < resolution; ix++) {
				const localX = ix * step - halfSize;
				const localZ = iz * step - halfSize;

				const localY = getTerrainHeight(
					localX,
					localZ,
					worldXBase,
					worldZBase,
					scale,
					hillScale,
					hillHeight,
					detailScale,
					detailHeight,
					noise2D
				);

				vertices.push(localX, localY, localZ);
			}
		}

		/*
			idx gen
		*/
		for (let iz = 0; iz < resolution - 1; iz++) {
			for (let ix = 0; ix < resolution - 1; ix++) {
				const topLeft = iz * resolution + ix;
				const topRight = topLeft + 1;
				const bottomLeft = (iz + 1) * resolution + ix;
				const bottomRight = bottomLeft + 1;

				indices.push(topLeft, bottomLeft, topRight);
				indices.push(topRight, bottomLeft, bottomRight);
			}
		}
		geo.setAttribute(
			'position',
			new BufferAttribute(new Float32Array(vertices), 3)
		);

		const colors: Array<number> = [];
		for (let iz = 0; iz < resolution; iz++) {
			for (let ix = 0; ix < resolution; ix++) {
				const localX = ix * step - halfSize;
				const localZ = iz * step - halfSize;
				const globalX = worldXBase + localX;
				const globalZ = worldZBase + localZ;
				const colorNoise = noise2D(globalX * 0.02, globalZ * 0.02);
				const t = (colorNoise + 1) / 2;

				const dryGreen = new Color(terrainDryColor);
				const lushGreen = new Color(terrainLushColor);

				const r = dryGreen.r + (lushGreen.r - dryGreen.r) * t;
				const g = dryGreen.g + (lushGreen.g - dryGreen.g) * t;
				const b = dryGreen.b + (lushGreen.b - dryGreen.b) * t;

				colors.push(r, g, b);
			}
		}
		geo.setAttribute('color', new BufferAttribute(new Float32Array(colors), 3));

		geo.setIndex(indices);
		geo.computeVertexNormals();

		return geo;
	}, [
		x,
		y,
		size,
		resolution,
		scale,
		hillScale,
		hillHeight,
		detailScale,
		detailHeight,
		noise2D,
		terrainDryColor,
		terrainLushColor
	]);

	return (
		<group>
			<mesh
				ref={meshRef}
				geometry={geometry}
				position={[x * size, 0, y * size]}
			>
				<meshStandardMaterial vertexColors roughness={1.0} metalness={0.0} />
			</mesh>
			{shouldRenderGrass && (
				<Grass
					x={x}
					y={y}
					size={size}
					count={grassCount}
					grassSize={grassSize}
					scale={scale}
					hillScale={hillScale}
					hillHeight={hillHeight}
					detailScale={detailScale}
					detailHeight={detailHeight}
					noise2D={noise2D}
					grassLOD={grassLOD}
					dryColor={grassDryColor}
					lushColor={grassLushColor}
					grassBlades={grassBlades}
					grassSegments={grassSegments}
					grassLODStart={grassLODStart}
					grassLODExponent={grassLODExponent}
				/>
			)}
		</group>
	);
}

interface TerrainProps {
	chunks?: number;
	chunkSize?: number;
	resolution?: number;
	scale?: number;
	hillScale: number;
	hillHeight: number;
	detailScale: number;
	detailHeight: number;
	grassCount?: number;
	grassSize?: number;
	grassLOD?: number;
	terrainDryColor?: string;
	terrainLushColor?: string;
	grassDryColor?: string;
	grassLushColor?: string;
	grassBlades?: number;
	grassSegments?: number;
	grassLODStart?: number;
	grassLODExponent?: number;
}
export default function Terrain({
	chunks = 5,
	chunkSize = 10,
	resolution = 8,
	scale = 1,
	hillScale = 0.2,
	hillHeight = 3,
	detailScale = 3,
	detailHeight = 0.1,
	grassCount = 6000,
	grassSize = 0.6,
	grassLOD = 40,
	terrainDryColor = '#010100',
	terrainLushColor = '#000800',
	grassDryColor = '#556b19',
	grassLushColor = '#348a34',
	grassBlades = 2,
	grassSegments = 2,
	grassLODStart = 0.5,
	grassLODExponent = 1.0
}: TerrainProps) {
	const noise2D = useMemo(() => createNoise2D(), []);
	const offset = -Math.floor(chunks / 2);

	const chunkPositions = useMemo(() => {
		const positions: [number, number][] = [];
		for (let x = 0; x < chunks; x++)
			for (let y = 0; y < chunks; y++) positions.push([x + offset, y + offset]);

		return positions;
	}, [chunks, offset]);

	return (
		<group>
			{chunkPositions.map(([x, y], index) => (
				<TerrainChunk
					key={`${x}-${y}-${index}`}
					x={x}
					y={y}
					size={chunkSize}
					resolution={resolution}
					scale={scale}
					hillScale={hillScale}
					hillHeight={hillHeight}
					detailScale={detailScale}
					detailHeight={detailHeight}
					noise2D={noise2D}
					grassCount={grassCount}
					grassSize={grassSize}
					grassLOD={grassLOD}
					terrainDryColor={terrainDryColor}
					terrainLushColor={terrainLushColor}
					grassDryColor={grassDryColor}
					grassLushColor={grassLushColor}
					grassBlades={grassBlades}
					grassSegments={grassSegments}
					grassLODStart={grassLODStart}
					grassLODExponent={grassLODExponent}
				/>
			))}
		</group>
	);
}