use std::{collections::VecDeque, io, time::{Duration, Instant}};

use either::Either;
use yazi_fs::{cha::Cha, engine::{DirReader, FileHolder}};
use yazi_shared::url::{AsUrl, UrlBuf};

use super::ReadDir;

pub enum SizeCalculator {
	File(Option<u64>, Cha),
	Dir(VecDeque<Either<UrlBuf, ReadDir>>, Cha),
}

impl SizeCalculator {
	pub async fn new<U>(url: U) -> io::Result<Self>
	where
		U: AsUrl,
	{
		let url = url.as_url();
		let cha = super::symlink_metadata(url).await?;
		Ok(if cha.is_dir() && !cha.is_indirect() {
			Self::Dir(VecDeque::from([Either::Left(url.to_owned())]), cha)
		} else {
			Self::File(Some(cha.len), cha)
		})
	}

	pub fn cha(&self) -> Cha {
		match *self {
			Self::File(_, cha) | Self::Dir(_, cha) => cha,
		}
	}

	pub async fn total<U>(url: U) -> io::Result<u64>
	where
		U: AsUrl,
	{
		let mut it = Self::new(url).await?;
		let mut total = 0;
		while let Some(n) = it.next().await? {
			total += n;
		}
		Ok(total)
	}

	pub async fn next(&mut self) -> io::Result<Option<u64>> {
		Ok(match self {
			Self::File(size, _) => size.take(),
			Self::Dir(buf, _) => Self::next_chunk(buf).await,
		})
	}

	async fn next_chunk(buf: &mut VecDeque<Either<UrlBuf, ReadDir>>) -> Option<u64> {
		let (mut i, mut size, now) = (0, 0, Instant::now());
		macro_rules! pop_and_continue {
			() => {{
				buf.pop_front();
				if buf.is_empty() {
					return Some(size);
				}
				continue;
			}};
		}

		while i < 2000 && now.elapsed() < Duration::from_millis(100) {
			i += 1;
			let front = buf.front_mut()?;

			if let Either::Left(p) = front {
				*front = match super::read_dir(p).await {
					Ok(it) => Either::Right(it),
					Err(_) => pop_and_continue!(),
				};
			}

			let Ok(Some(dent)) = front.as_mut().right()?.next().await else {
				pop_and_continue!();
			};

			let Ok(cha) = dent.metadata().await else { continue };
			if cha.is_dir() && !cha.is_indirect() {
				buf.push_back(Either::Left(dent.url()));
			} else {
				size += cha.len;
			}
		}
		Some(size)
	}
}
