use opengl_rust::{
	prelude::*,
	shader,
};

struct GameState {
	logic_frames: u64,
}

struct GraphicsContext {
	window: sdl2::video::Window,
	gl_ctx: sdl2::video::GLContext,
	
	vertex_buffer: gpu_buffers::VertexBuffer,
	index_buffer: gpu_buffers::IndexBuffer,
	
	shader_program: shader::ShaderProgram,
	shader_locations: ShaderLocations,
}

struct ShaderLocations {
	attr_pos: u32,
	attr_normal: u32,
	attr_color: u32,
	uni_mvp: i32,
}

impl ShaderLocations {
	pub fn new (shader_program: &shader::ShaderProgram) -> anyhow::Result <Self> 
	{
		let attr = |name: &str| shader_program.get_attribute_location (&CString::new (name.as_bytes ())?).try_into ().context ("Attribute location negative");
		let uni = |name: &str| shader_program.get_uniform_location (&CString::new (name.as_bytes ())?).try_into ().context ("Uniform location bad");
		
		Ok (Self {
			attr_pos:    attr ("attr_pos")?,
			attr_normal: attr ("attr_normal")?,
			attr_color:  attr ("attr_color")?,
			uni_mvp:     uni ("uni_mvp")?,
		})
	}
}

#[instrument (level = "trace", skip (ctx, state))]
fn draw_graphics (
	ctx: &GraphicsContext, 
	state: &GameState,
	bumps: &[(f32, f32)],
)
-> anyhow::Result <()>
{
	let shader_locs = &ctx.shader_locations;
	let attr_pos = shader_locs.attr_pos;
	let attr_normal = shader_locs.attr_normal;
	let attr_color = shader_locs.attr_color;
	let uni_mvp = shader_locs.uni_mvp;
	
	ctx.window.gl_make_current (&ctx.gl_ctx).unwrap ();
	
	glezz::enable (gl::DEPTH_TEST);
	
	glezz::clear_color (0.392f32, 0.710f32, 0.965f32, 1.0f32);
	glezz::clear (gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT | gl::STENCIL_BUFFER_BIT);
	
	let screen_size = (960.0, 540.0);
	
	let proj_mat = Mat4::perspective_rh_gl (30.0f32.to_radians (), screen_size.0 / screen_size.1, 0.125, 200.0);
	
	let spin_period = 360 * 8;
	
	let view_mat = proj_mat *
	Mat4::from_translation (Vec3::from ((0.0, 0.0, -80.0))) *
	Mat4::from_rotation_x (-60.0 * 3.14159 / 180.0) *
	// Mat4::from_rotation_z ((state.logic_frames % spin_period) as f32 * 3.1415926535 * 2.0 / spin_period as f32) *
	Mat4::from_translation (Vec3::from ((-32.0, -32.0, -5.0)));
	
	let mvp = view_mat;
	
	glezz::uniform_matrix_4fv (uni_mvp, &mvp);
	
	ctx.shader_program.use_program ();
	glezz::enable_vertex_attrib_array (Some (attr_pos));
	glezz::enable_vertex_attrib_array (Some (attr_color));
	glezz::enable_vertex_attrib_array (Some (attr_normal));
	
	ctx.vertex_buffer.bind ();
	ctx.index_buffer.bind ();
	
	let bump_period = 360 * 7;
	let bump_theta = (state.logic_frames % bump_period) as f32 * 3.1415926535 * 2.0 / bump_period as f32;
	
	let ClientArrays {
		vertexes,
		indexes,
	} = make_heightmap_arrays (bumps);
	
	upload_vertexes (&vertexes)?;
	
	unsafe {
		let num_quads = 64 * 64;
		let stride = 4 * 9;
		gl::VertexAttribPointer (attr_pos, 3, gl::FLOAT, 0, stride, 0 as *const u8 as *const c_void);
		gl::VertexAttribPointer (attr_normal, 3, gl::FLOAT, 0, stride, (4 * 3) as *const u8 as *const c_void);
		gl::VertexAttribPointer (attr_color, 3, gl::FLOAT, 0, stride, (4 * 6) as *const u8 as *const c_void);
		
		gl::DrawRangeElements (gl::TRIANGLES, 0, num_quads * 4, num_quads as i32 * 6, gl::UNSIGNED_INT, 0 as *const u8 as *const c_void);
	}
	
	ctx.window.gl_swap_window ();
	
	Ok (())
}

struct ClientArrays {
	vertexes: Vec <f32>,
	indexes: Vec <u32>,
}

fn make_heightmap_arrays (bumps: &[(f32, f32)]) -> ClientArrays {
	let mut vertexes = vec![];
	let mut indexes = vec![];
	let mut start_index = 0;
	
	let bump_at = |bump_x, bump_y, x, y| {
		let x = x - bump_x;
		let y = y - bump_y;
		
		let d2: f32 = x * x + y * y;
		if d2 > 8.0 * 8.0 {
			return 0.0;
		}
		let d = d2.sqrt ();
		
		let t = f32::min (1.0, f32::max (0.0, (8.0 - d) / 6.0));
		let z = -2.0 * t * t + 3.0 * t * t;
		
		f32::max (0.0, 4.0 * z)
	};
	
	let height_fn = |(x, y)| {
		bumps.iter ()
		.map (|(bump_x, bump_y)| bump_at (bump_x, bump_y, x, y))
		.sum ()
	};
	
	for y in 0..64 {
		for x in 0..64 {
			let (r, g, b) = if (x + y) % 2 == 0 {
				(0.4, 0.4, 0.4)
			}
			else {
				(0.5, 0.5, 0.5)
			};
			
			let x = x as f32;
			let y = y as f32;
			let i = start_index;
			
			let xys = [
				(x + 0.0, y + 0.0),
				(x + 1.0, y + 0.0),
				(x + 1.0, y + 1.0),
				(x + 0.0, y + 1.0),
			];
			
			let zs = [
				height_fn (xys [0]),
				height_fn (xys [1]),
				height_fn (xys [2]),
				height_fn (xys [3]),
			];
			
			let slope_x = (zs [1] + zs [2]) - (zs [0] + zs [3]);
			let slope_y = (zs [2] + zs [3]) - (zs [0] + zs [1]);
			
			let tangent_x = Vec3::from ((1.0, 0.0, slope_x));
			let tangent_y = Vec3::from ((0.0, 1.0, slope_y));
			let normal = Vec3::cross (tangent_x, tangent_y)
			.normalize ();
			
			let (nx, ny, nz) = normal.into ();
			
			for ((x, y), z) in xys.iter ().zip (&zs) {
				vertexes.extend (&[
					*x, *y, *z,
					nx, ny, nz,
					r, g, b,
				])
			}
			
			indexes.extend (&[
				i + 0, i + 1, i + 2,
				i + 0, i + 2, i + 3,
			]);
			start_index += 4;
		}
	}
	
	ClientArrays {
		vertexes,
		indexes,
	}
}

fn upload_vertexes (vertexes: &[f32]) -> anyhow::Result <()> {
	unsafe {
		gl::BufferSubData (
			gl::ARRAY_BUFFER,
			0,
			(vertexes.len () * 4).try_into ()?,
			&vertexes [0] as *const f32 as *const c_void
		);
	}
	
	Ok (())
}

#[tokio::main]
async fn main () -> anyhow::Result <()> {
	use std::sync::Arc;
	
	use futures_util::StreamExt;
	use tokio::sync::Mutex;
	
	tracing_subscriber::fmt::fmt ()
	.with_env_filter (tracing_subscriber::EnvFilter::from_default_env())
	.with_span_events (tracing_subscriber::fmt::format::FmtSpan::CLOSE)
	.init ();
	
	let sdl_context = sdl2::init ().map_err (|e| anyhow! ("Can't init SDL: {}", e))?;
	
	let video_subsystem = sdl_context.video ().map_err (|e| anyhow! ("Can't get SDL video subsystem: {}", e))?;
	let window = video_subsystem.window ("Heightmap terrain demo", 960, 540)
		.position_centered ()
		.opengl ()
		.build ()
	?;
	
	gl::load_with (|s| {
		video_subsystem.gl_get_proc_address (s) as *const _
	});
	
	assert! (gl::ClearColor::is_loaded ());
	
	let gl_ctx = window.gl_create_context ().map_err (|e| anyhow! ("Can't create OpenGL context: {}", e))?;
	
	window.gl_make_current (&gl_ctx).map_err (|e| anyhow! ("Can't make OpenGL context current: {}", e))?;
	
	let mut time_step = TimeStep::new (60, 1000);
	
	let mut graphics_frames = 0;
	
	let controller_subsystem = sdl_context.game_controller ().unwrap ();
	let controller = controller_subsystem.open (0).ok ();
	
	let mut event_pump = sdl_context.event_pump ().unwrap ();
	
	let shader_program = shader::shader_from_files ("shaders/terrain-vert.glsl", "shaders/terrain-frag.glsl");
	let shader_locations = ShaderLocations::new (&shader_program)?;
	
	let ClientArrays {
		vertexes,
		indexes,
	} = make_heightmap_arrays (&[]);
	
	debug! ("Floats in vertex buffer: {}", vertexes.len ());
	
	let vertex_buffer = gpu_buffers::VertexBuffer::streaming (vertexes.len ());
	let index_buffer = gpu_buffers::IndexBuffer::from_slice_u32 (&indexes);
	
	upload_vertexes (&vertexes)?;
	
	unsafe {
		gl::BufferSubData (
			gl::ARRAY_BUFFER,
			0,
			(vertexes.len () * 4).try_into ()?,
			&vertexes [0] as *const f32 as *const c_void
		);
	}
	
	let graphics_ctx = GraphicsContext {
		window,
		gl_ctx,
		
		vertex_buffer,
		index_buffer,
		
		shader_program,
		shader_locations,
	};
	
	let mut game_state = GameState {
		logic_frames: 0,
	};
	
	let server_cert = tokio::fs::read ("quic_server.crt").await?;
	let server_addr = "127.0.0.1:5000".parse().unwrap();
	let endpoint = make_client_endpoint("0.0.0.0:0".parse().unwrap(), &[&server_cert])?;
    // connect to server
    let quinn::NewConnection {
        connection,
        mut datagrams,
        ..
    } = endpoint
        .connect(&server_addr, "localhost")
        .unwrap()
        .await
        .unwrap();
    println!("[client] connected: addr={}", connection.remote_address());
	
	let networked_state = Arc::new (Mutex::new (NetworkedState::default ()));
	let networked_state_2 = Arc::clone (&networked_state);
	
	tokio::spawn (async move {
		while let Some (Ok (datagram)) = datagrams.next ().await {
			let state = rmp_serde::from_slice (&datagram)?;
			let mut guard = networked_state_2.lock ().await;
			*guard = state;
		}
		
		Ok::<_, anyhow::Error> (())
	});
	
	'running: loop {
		let frames_to_do = time_step.step ();
		
		let _mouse = event_pump.mouse_state ();
		
		let mut cmd = NetworkCommand::default ();
		
		for event in event_pump.poll_iter() {
			match event {
				Event::Quit {..} |
				Event::KeyDown { keycode: Some (Keycode::Escape), .. } => {
					break 'running
				},
				Event::KeyDown { keycode: Some (keycode), .. } => {
					match keycode {
						Keycode::Left => cmd.left = true,
						Keycode::Right => cmd.right = true,
						Keycode::Up => cmd.up = true,
						Keycode::Down => cmd.right = true,
						_ => (),
					}
				},
				_ => (),
			}
		}
		
		let keyboard_state = event_pump.keyboard_state ();
		if keyboard_state.is_scancode_pressed (Scancode::Left) {
			cmd.left = true
		}
		if keyboard_state.is_scancode_pressed (Scancode::Right) {
			cmd.right = true
		}
		if keyboard_state.is_scancode_pressed (Scancode::Up) {
			cmd.up = true
		}
		if keyboard_state.is_scancode_pressed (Scancode::Down) {
			cmd.down = true
		}
		
		let bytes = rmp_serde::to_vec (&cmd)?;
		connection.send_datagram (bytes.into ())?;
		
		for _ in 0..frames_to_do {
			game_state.logic_frames += 1;
		}
		
		let state = {
			let guard = networked_state.lock ().await;
			guard.clone ()
		};
		draw_graphics (&graphics_ctx, &game_state, &state.positions)?;
		graphics_frames += 1;
		
		std::thread::sleep (Duration::from_millis (15));
	}
	
	Ok (())
}
