use binrw::{binrw,BinReaderExt,BinWrite,BinWriterExt};

use crate::newtypes;
use crate::file::BlockId;
use strafesnet_common::physics::Time;

const VERSION:u32=0;

type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,Time>;

#[derive(Debug)]
pub enum Error{
	InvalidHeader(binrw::Error),
	InvalidSegment(binrw::Error),
	SegmentConvert(newtypes::integer::RatioError),
	InstructionConvert(newtypes::physics::InstructionConvert),
	InstructionWrite(binrw::Error),
	InvalidSegmentId(SegmentId),
	InvalidData(binrw::Error),
	IO(std::io::Error),
	File(crate::file::Error),
}

// Bot files are simply the sequence of instructions that the physics received during the run.
// The instructions are partitioned into timestamped blocks for ease of streaming.
//
// Keyframe information required for efficient seeking
// is part of a different file, and is generated from this file.

/* block types

BLOCK_BOT_HEADER:
// Segments are laid out in chronological order,
// but block_id is not necessarily in ascending order.
//
// This is to place the final segment close to the start of the file,
// which allows the duration of the bot to be conveniently calculated
// from the first and last instruction timestamps.
//
// Use exact physics version for replay playback
// Use highest compatible physics version for verification
u32 physics_version
u32 num_segments
for _ in 0..num_segments{
	i64 time
	u32 instruction_count
	u32 block_id
}

BLOCK_BOT_SEGMENT:
// segments can potentially be losslessly compressed!
for _ in 0..instruction_count{
	// TODO: delta encode as much as possible (time,mousepos)
	i64 time
	physics::Instruction instruction
}

*/

#[binrw]
#[brw(little)]
struct SegmentHeader{
	time:i64,
	instruction_count:u32,
	block_id:BlockId,
}
#[binrw]
#[brw(little)]
struct Header{
	physics_version:u32,
	num_segments:u32,
	#[br(count=num_segments)]
	segments:Vec<SegmentHeader>,
}

#[binrw]
#[brw(little)]
#[derive(Clone,Copy,Debug,id::Id)]
pub struct SegmentId(u32);

pub struct Segment{
	pub instructions:Vec<TimedPhysicsInstruction>
}

#[derive(Clone,Copy,Debug)]
pub struct SegmentInfo{
	/// time of the first instruction in this segment.
	#[expect(dead_code)]
	time:Time,
	instruction_count:u32,
	/// How many total instructions in segments up to and including this segment
	/// Alternatively, the id of the first instruction be in the _next_ segment
	instructions_subtotal:u64,
	block_id:BlockId,
}

pub struct StreamableBot<R:BinReaderExt>{
	file:crate::file::File<R>,
	segment_map:Vec<SegmentInfo>,
}
impl<R:BinReaderExt> StreamableBot<R>{
	pub(crate) fn new(mut file:crate::file::File<R>)->Result<Self,Error>{
		//assume the file seek is in the right place to start reading header
		let header:Header=file.data_mut().read_le().map_err(Error::InvalidHeader)?;
		let mut instructions_subtotal=0;
		let segment_map=header.segments.into_iter().map(|SegmentHeader{time,instruction_count,block_id}|{
			instructions_subtotal+=instruction_count as u64;
			SegmentInfo{
				time:Time::raw(time),
				instruction_count,
				instructions_subtotal,
				block_id,
			}
		}).collect();
		Ok(Self{
			file,
			segment_map,
		})
	}
	#[expect(dead_code)]
	fn get_segment_info(&self,segment_id:SegmentId)->Result<SegmentInfo,Error>{
		Ok(*self.segment_map.get(segment_id.get() as usize).ok_or(Error::InvalidSegmentId(segment_id))?)
	}
	pub fn find_segments_instruction_range(&self,start_instruction:u64,end_instruction:u64)->&[SegmentInfo]{
		let start=self.segment_map.partition_point(|segment_info|segment_info.instructions_subtotal<start_instruction);
		let end=self.segment_map.partition_point(|segment_info|segment_info.instructions_subtotal<end_instruction);
		&self.segment_map[start..=end]
	}
	// pub fn find_segments_time_range(&self,start_time:Time,end_time:Time)->&[SegmentInfo]{
	// 	// TODO: This is off by one, both should be one less
	// 	let start=self.segment_map.partition_point(|segment_info|segment_info.time<start_time);
	// 	let end=self.segment_map.partition_point(|segment_info|segment_info.time<end_time);
	// 	&self.segment_map[start..=end]
	// }
	fn append_to_segment(&mut self,segment_info:SegmentInfo,segment:&mut Segment)->Result<(),Error>{
		let mut block=self.file.block_reader(segment_info.block_id).map_err(Error::File)?;
		for _ in 0..segment_info.instruction_count{
			let instruction:newtypes::physics::TimedInstruction=block.read_le().map_err(Error::InvalidSegment)?;
			segment.instructions.push(instruction.try_into().map_err(Error::SegmentConvert)?);
		}
		Ok(())
	}
	pub fn load_segment(&mut self,segment_info:SegmentInfo)->Result<Segment,Error>{
		let mut segment=Segment{
			instructions:Vec::with_capacity(segment_info.instruction_count as usize),
		};
		self.append_to_segment(segment_info,&mut segment)?;
		Ok(segment)
	}
	pub fn read_all(&mut self)->Result<Segment,Error>{
		let mut segment=Segment{
			instructions:Vec::new(),
		};
		for i in 0..self.segment_map.len(){
			let segment_info=self.segment_map[i];
			self.append_to_segment(segment_info,&mut segment)?;
		}
		Ok(segment)
	}
}

const MAX_BLOCK_SIZE:usize=64*1024;//64 kB
pub fn write_bot<W:BinWriterExt>(mut writer:W,physics_version:u32,instructions:impl IntoIterator<Item=TimedPhysicsInstruction>)->Result<(),Error>{
	// decide which instructions to put in which segment
	// write segment 1 to block 1
	// write segment N to block 2
	// write rest of segments
	// 1 2 3 4 5
	// becomes
	// [1 5] 2 3 4
	struct SegmentHeaderInfo{
		time:Time,
		instruction_count:u32,
		range:core::ops::Range<usize>
	}

	let mut segment_header_infos=Vec::new();
	let mut raw_segments=std::io::Cursor::new(Vec::new());

	// block info
	let mut start_time=Time::ZERO;
	let mut start_position=raw_segments.position() as usize;
	let mut instruction_count=0;

	let mut last_position=start_position;

	let mut iter=instructions.into_iter();

	macro_rules! collect_instruction{
		($instruction:expr)=>{
			let time=$instruction.time;
			let instruction_writable:newtypes::physics::TimedInstruction=$instruction.try_into().map_err(Error::InstructionConvert)?;
			instruction_writable.write_le(&mut raw_segments).map_err(Error::InstructionWrite)?;
			instruction_count+=1;
			let position=raw_segments.position() as usize;
			// exceeds max block size
			if MAX_BLOCK_SIZE<position-last_position{
				segment_header_infos.push(SegmentHeaderInfo{
					time:start_time,
					instruction_count,
					range:start_position..last_position,
				});
				start_position=last_position;
				instruction_count=0;
				start_time=time;
			}
			last_position=position;
		}
	}

	// unroll one loop iteration to grab the starting time
	if let Some(instruction)=iter.next(){
		start_time=instruction.time;
		collect_instruction!(instruction);
	}

	for instruction in iter{
		collect_instruction!(instruction);
	}
	//last block, whatever size it happens to be
	{
		let final_position=raw_segments.position() as usize;
		segment_header_infos.push(SegmentHeaderInfo{
			time:start_time,
			instruction_count,
			range:start_position..final_position,
		});
	}
	// drop cursor
	let raw_segments=raw_segments.into_inner();

	let num_segments=segment_header_infos.len();

	// segments list is in chronological order
	let make_segment_header=|block_id,&SegmentHeaderInfo{time,instruction_count,range:ref _range}|SegmentHeader{
		time:time.get(),
		instruction_count,
		block_id,
	};
	let segments=if 2<num_segments{
		let mut segments=Vec::with_capacity(num_segments);
		// segment 1 is second block
		if let Some(seg)=segment_header_infos.first(){
			segments.push(make_segment_header(BlockId::new(1),seg));
		}
		// rest of segments start at fourth block
		for (i,seg) in segment_header_infos[1..num_segments-1].iter().enumerate(){
			make_segment_header(BlockId::new(3+i as u32),seg);
		}
		// last segment is third block
		if let Some(seg)=segment_header_infos.last(){
			segments.push(make_segment_header(BlockId::new(2),seg));
		}
		segments
	}else{
		// all segments in order
		segment_header_infos.iter().enumerate().map(|(i,seg)|
			make_segment_header(BlockId::new(1+i as u32),seg)
		).collect()
	};

	let header=Header{
		physics_version,
		num_segments:num_segments as u32,
		segments,
	};

	// map header is +1
	let block_count=1+num_segments as u32;

	let mut offset=crate::file::Header::calculate_size(block_count) as u64;
	// block_location is one longer than block_count
	let mut block_location=Vec::with_capacity(1+block_count as usize);

	//probe header length
	let mut bot_header_data=Vec::new();
	header.write_le(&mut std::io::Cursor::new(&mut bot_header_data)).map_err(Error::InvalidData)?;

	// the first block location is the map header
	block_location.push(offset);
	offset+=bot_header_data.len() as u64;
	block_location.push(offset);

	// priming includes file header + first 3 blocks [bot header, first segment, last segment]
	let priming=if 2<num_segments{
		// segment 1 is block 2
		if let Some(seg)=segment_header_infos.first(){
			offset+=seg.range.len() as u64;
			block_location.push(offset);
		}
		// last segment is block 3
		if let Some(seg)=segment_header_infos.last(){
			offset+=seg.range.len() as u64;
			block_location.push(offset);
		}

		let priming=offset;

		// rest of segments
		for seg in &segment_header_infos[1..num_segments-1]{
			offset+=seg.range.len() as u64;
			block_location.push(offset);
		}
		priming
	}else{
		// all segments in order
		for seg in &segment_header_infos{
			offset+=seg.range.len() as u64;
			block_location.push(offset);
		}
		offset
	};

	let file_header=crate::file::Header{
		fourcc:crate::file::FourCC::Bot,
		version:VERSION,
		priming,
		resource:0,
		block_count,
		block_location,
	};

	// write file header
	writer.write_le(&file_header).map_err(Error::InvalidData)?;
	// write bot header
	writer.write(&bot_header_data).map_err(Error::IO)?;

	// write blocks
	if 2<num_segments{
		// segment 1 is block 2
		if let Some(seg)=segment_header_infos.first(){
			writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
		}
		// last segment is block 3
		if let Some(seg)=segment_header_infos.last(){
			writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
		}
		// rest of segments
		for seg in &segment_header_infos[1..num_segments-1]{
			writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
		}
	}else{
		// all segments in order
		for seg in segment_header_infos{
			writer.write(&raw_segments[seg.range]).map_err(Error::IO)?;
		}
	}
	Ok(())
}
