use std::borrow::Cow;

use anyhow::{anyhow, bail, Context};
use fancy_regex::Regex;
use serde::Deserialize;

use crate::typespec;

#[derive(Deserialize, Debug)]
pub struct ModuleInfoHeader {
	pub name: String,
	pub description: String,
	pub headers: Vec<String>,
}

#[derive(Debug)]
pub struct ModuleInfo<'a> {
	pub header: ModuleInfoHeader,
	pub details: &'a str,
}

pub fn parse_module(text: &str) -> anyhow::Result<ModuleInfo> {
	let Some((mut header, mut details)) = text.split_once("-----") else {
		bail!("could not split module header");
	};

	header = header.trim();
	details = details.trim();

	let header =
		toml::from_str::<ModuleInfoHeader>(header).context("parsing module info header")?;

	Ok(ModuleInfo { header, details })
}

#[derive(Debug)]
pub struct ClassInfo<'a> {
	pub type_: ClassType,
	pub name: &'a str,
	pub qml_name: Option<&'a str>,
	pub superclass: Option<&'a str>,
	pub singleton: bool,
	pub uncreatable: bool,
	pub comment: Option<&'a str>,
	pub properties: Vec<Property<'a>>,
	pub invokables: Vec<Invokable<'a>>,
	pub signals: Vec<Signal<'a>>,
	pub enums: Vec<EnumInfo<'a>>,
}

#[derive(Debug)]
pub enum ClassType {
	Object,
	Gadget,
}

#[derive(Debug, Clone, Copy)]
pub struct Property<'a> {
	pub type_: &'a str,
	pub name: &'a str,
	pub comment: Option<&'a str>,
	pub readable: bool,
	pub writable: bool,
	pub default: bool,
}

#[derive(Debug, Clone)]
pub struct Invokable<'a> {
	pub name: &'a str,
	pub ret: &'a str,
	pub comment: Option<&'a str>,
	pub params: Vec<InvokableParam<'a>>,
}

#[derive(Debug, Clone)]
pub struct Signal<'a> {
	pub name: &'a str,
	pub comment: Option<&'a str>,
	pub params: Vec<InvokableParam<'a>>,
}

#[derive(Debug, Clone, Copy)]
pub struct InvokableParam<'a> {
	pub name: &'a str,
	pub type_: &'a str,
}

#[derive(Debug)]
pub struct EnumInfo<'a> {
	pub namespace: &'a str,
	pub enum_name: &'a str,
	pub qml_name: &'a str,
	pub comment: Option<&'a str>,
	pub variants: Vec<Variant<'a>>,
}

#[derive(Debug, Clone, Copy)]
pub struct Variant<'a> {
	pub name: &'a str,
	pub comment: Option<&'a str>,
}

pub struct Parser {
	pub class_regex: Regex,
	pub macro_regex: Regex,
	pub property_regex: Regex,
	pub signals_regex: Regex,
	pub fn_regex: Regex,
	pub signal_regex: Regex,
	pub fn_param_regex: Regex,
	pub defaultprop_classinfo_regex: Regex,
	pub enum_ns_regex: Regex,
	pub enum_class_regex: Regex,
	pub enum_variant_regex: Regex,
}

#[derive(Debug)]
pub struct ParseContext<'a> {
	pub classes: Vec<ClassInfo<'a>>,
	pub enums: Vec<EnumInfo<'a>>,
}

impl Default for ParseContext<'_> {
	fn default() -> Self {
		Self {
			classes: Vec::new(),
			enums: Vec::new(),
		}
	}
}

impl Parser {
	pub fn new() -> Self {
		Self {
			class_regex: Regex::new(r#"(?<comment>(\s*\/\/\/.*\n)+)?\s*class\s+(?<name>\w+)(?:\s*:\s*public\s+(?<super>\w+)(\s*,(\s*\w+)*)*)?\s*\{(?<body>[\s\S]*?)(?!};\s*Q_ENUM)};"#).unwrap(),
			macro_regex: Regex::new(r#"(?<comment>(\s*\/\/\/.*\n)+)?\s*(?<hide>QSDOC_HIDE\s)?(?<type>(Q|QML|QSDOC)_\w+)\s*(\(\s*(?<args>.*)\s*\))?;"#).unwrap(),
			property_regex: Regex::new(r#"^\s*(?<type>(\w|::|, |<|>|\*)+)\*?\s+(?<name>\w+)(\s+(MEMBER\s+(?<member>\w+)|READ\s+(?<read>\w+)|WRITE\s+(?<write>\w+)|NOTIFY\s+(?<notify>\w+)|(?<const>CONSTANT)))+\s*$"#).unwrap(),
			fn_regex: Regex::new(r#"(?<comment>(\s*\/\/\/.*\n)+)?\s*Q_INVOKABLE\s+(\[\[.*\]\]\s+)?(static\s+)?(?<type>(\w|::|<|>)+\*?)\s+(?<name>\w+)\((?<params>[\s\S]*?)\)(\s*const)?;"#).unwrap(),
			signal_regex: Regex::new(r#"(?<comment>(\s*\/\/\/.*\n)+)?\s*void\s+(?<name>\w+)\((?<params>[\s\S]*?)\);"#).unwrap(),
			fn_param_regex: Regex::new(r#"(const\s+)?(?<type>(\w|::|<|>)+\*?)&?\s+(?<name>\w+)(,|$)"#).unwrap(),
			signals_regex: Regex::new(r#"signals:(?<signals>(\s*(\s*///.*\s*)*void .*;)*)"#).unwrap(),
			defaultprop_classinfo_regex: Regex::new(r#"^\s*"DefaultProperty", "(?<prop>.+)"\s*$"#).unwrap(),
			enum_ns_regex: Regex::new(r#"(?<comment>(\s*\/\/\/.*\n)+)?\s*namespace (?<namespace>\w+)\s*\{[\s\S]*?(QML_ELEMENT|QML_NAMED_ELEMENT\((?<qml_name>\w+)\));[\s\S]*?enum\s*(?<enum_name>\w+)\s*\{(?<body>[\s\S]*?)\};[\s\S]*?\}"#).unwrap(),
			enum_class_regex: Regex::new(r#"(?<comment>(\s*\/\/\/.*\n)+)?\s*enum\s*(?<enum_name>\w+)\s*\{(?<body>[\s\S]*?)\};\s+Q_ENUM\(.+\);"#).unwrap(),
			enum_variant_regex: Regex::new(r#"(?<comment>(\s*\/\/\/.*\n)+)?\s*(?<name>\w+)\s*=\s*.+,"#).unwrap(),
		}
	}

	pub fn parse_classes<'a>(
		&self,
		text: &'a str,
		ctx: &mut ParseContext<'a>,
	) -> anyhow::Result<()> {
		for class in self.class_regex.captures_iter(text) {
			let class = class?;

			let comment = class.name("comment").map(|m| m.as_str());
			let mut name = class.name("name").unwrap().as_str();
			let mut superclass = class.name("super").map(|m| m.as_str());
			let body = class.name("body").unwrap().as_str();

			let mut classtype = None;
			let mut qml_name = None;
			let mut singleton = false;
			let mut uncreatable = false;
			let mut properties = Vec::new();
			let mut default_property = None;
			let mut invokables = Vec::new();
			let mut notify_signals = Vec::new();
			let mut signals = Vec::new();
			let mut enums = Vec::new();

			(|| {
				for macro_ in self.macro_regex.captures_iter(body) {
					let macro_ = macro_?;

					if macro_.name("hide").is_some() {
						continue
					}

					let comment = macro_.name("comment").map(|m| m.as_str());
					let type_ = macro_.name("type").unwrap().as_str();
					let args = macro_.name("args").map(|m| m.as_str());

					(|| {
						match type_ {
							"QSDOC_BASECLASS" => {
								superclass = Some(args.expect(
									"QSDOC_BASECLASS must have the base class as an argument",
								))
							},
							"QSDOC_CNAME" => {
								name = args
									.expect("QSDOC_CNAME must specify the cname as an argument");
							},
							"Q_OBJECT" => classtype = Some(ClassType::Object),
							"Q_GADGET" => classtype = Some(ClassType::Gadget),
							"QML_ELEMENT" | "QSDOC_ELEMENT" => qml_name = Some(name),
							"QML_NAMED_ELEMENT" | "QSDOC_NAMED_ELEMENT" => {
								qml_name = Some(args.ok_or_else(|| {
									anyhow!("expected name for QML_NAMED_ELEMENT")
								})?)
							},
							"QML_SINGLETON" => singleton = true,
							"QML_UNCREATABLE" => uncreatable = true,
							"Q_PROPERTY" | "QSDOC_PROPERTY_OVERRIDE" => {
								let prop =
									self.property_regex
										.captures(args.ok_or_else(|| {
											anyhow!("expected args for Q_PROPERTY")
										})?)?
										.ok_or_else(|| anyhow!("unable to parse Q_PROPERTY"))?;

								let member = prop.name("member").is_some();
								let read = prop.name("read").is_some();
								let write = prop.name("write").is_some();
								let constant = prop.name("const").is_some();

								if let Some(notify) = prop.name("notify").map(|v| v.as_str()) {
									notify_signals.push(notify);
								}

								properties.push(Property {
									type_: prop.name("type").unwrap().as_str(),
									name: prop.name("name").unwrap().as_str(),
									comment,
									readable: read || member,
									writable: !constant && (write || member),
									default: false,
								});
							},
							"Q_CLASSINFO" => {
								let classinfo = self.defaultprop_classinfo_regex.captures(
									args.ok_or_else(|| anyhow!("expected args for Q_CLASSINFO"))?,
								)?;

								if let Some(classinfo) = classinfo {
									let prop = classinfo.name("prop").unwrap().as_str();
									default_property = Some(prop);
								}
							},
							_ => {},
						}
						Ok::<_, anyhow::Error>(())
					})()
					.with_context(|| {
						format!("while parsing macro `{}`", macro_.get(0).unwrap().as_str())
					})?;
				}

				if let Some(prop) = default_property {
					let prop = properties
						.iter_mut()
						.find(|p| p.name == prop)
						.ok_or_else(|| anyhow!("could not find default property `{prop}`"))?;

					prop.default = true;
				}

				for invokable in self.fn_regex.captures_iter(body) {
					let invokable = invokable?;

					let comment = invokable.name("comment").map(|m| m.as_str());
					let type_ = invokable.name("type").unwrap().as_str();
					let name = invokable.name("name").unwrap().as_str();
					let params_raw = invokable.name("params").unwrap().as_str();

					let mut params = Vec::new();

					for param in self.fn_param_regex.captures_iter(params_raw) {
						let param = param?;

						let type_ = param.name("type").unwrap().as_str();
						let name = param.name("name").unwrap().as_str();

						params.push(InvokableParam { type_, name });
					}

					invokables.push(Invokable {
						name,
						ret: type_,
						comment,
						params,
					});
				}

				for signal_set in self.signals_regex.captures_iter(body) {
					let signal_set = signal_set?;

					let signals_body = signal_set.name("signals").unwrap().as_str();

					for signal in self.signal_regex.captures_iter(signals_body) {
						let signal = signal?;

						if signal.name("invokable").is_some() {
							continue;
						}

						let comment = signal.name("comment").map(|m| m.as_str());
						let name = signal.name("name").unwrap().as_str();
						let params_raw = signal.name("params").unwrap().as_str();

						if notify_signals.contains(&name) {
							continue;
						}

						let mut params = Vec::new();

						for param in self.fn_param_regex.captures_iter(params_raw) {
							let param = param?;

							let type_ = param.name("type").unwrap().as_str();
							let name = param.name("name").unwrap().as_str();

							params.push(InvokableParam { type_, name });
						}

						signals.push(Signal {
							name,
							comment,
							params,
						});
					}
				}

				for enum_ in self.enum_class_regex.captures_iter(body) {
					let enum_ = enum_?;

					let comment = enum_.name("comment").map(|m| m.as_str());
					let enum_name = enum_.name("enum_name").unwrap().as_str();
					let body = enum_.name("body").unwrap().as_str();
					let variants = self.parse_enum_variants(body)?;

					enums.push(EnumInfo {
						namespace: name,
						enum_name,
						qml_name: enum_name,
						comment,
						variants,
					});
				}

				Ok::<_, anyhow::Error>(())
			})()
			.with_context(|| format!("while parsing class `{name}`"))?;

			let Some(type_) = classtype else { continue };

			ctx.classes.push(ClassInfo {
				type_,
				name,
				qml_name,
				superclass,
				singleton,
				uncreatable,
				comment,
				properties,
				invokables,
				signals,
				enums,
			});
		}

		Ok(())
	}

	pub fn parse_enums<'a>(&self, text: &'a str, ctx: &mut ParseContext<'a>) -> anyhow::Result<()> {
		for enum_ in self.enum_ns_regex.captures_iter(text) {
			let enum_ = enum_?;

			let comment = enum_.name("comment").map(|m| m.as_str());
			let namespace = enum_.name("namespace").unwrap().as_str();
			let enum_name = enum_.name("enum_name").unwrap().as_str();
			let qml_name = enum_
				.name("qml_name")
				.map(|m| m.as_str())
				.unwrap_or(namespace);
			let body = enum_.name("body").unwrap().as_str();
			let variants = self.parse_enum_variants(body)?;

			ctx.enums.push(EnumInfo {
				namespace,
				enum_name,
				qml_name,
				comment,
				variants,
			});
		}

		Ok(())
	}

	pub fn parse_enum_variants<'a>(&self, body: &'a str) -> anyhow::Result<Vec<Variant<'a>>> {
		let mut variants = Vec::new();

		for variant in self.enum_variant_regex.captures_iter(body) {
			let variant = variant?;

			let comment = variant.name("comment").map(|m| m.as_str());
			let name = variant.name("name").unwrap().as_str();

			variants.push(Variant { name, comment });
		}

		Ok(variants)
	}

	pub fn parse<'a>(&self, text: &'a str, ctx: &mut ParseContext<'a>) -> anyhow::Result<()> {
		self.parse_classes(text, ctx)?;
		self.parse_enums(text, ctx)?;

		Ok(())
	}
}

impl ParseContext<'_> {
	pub fn gen_typespec(&self, module: &str) -> typespec::TypeSpec {
		typespec::TypeSpec {
			typemap: self
				.classes
				.iter()
				.flat_map(|class| {
					let Some(qmlname) = class.qml_name else { return Vec::new() };

					let mut classes = Vec::new();
					classes.push(typespec::QmlTypeMapping {
						// filters gadgets
						name: qmlname.to_string(),
						cname: class.name.to_string(),
						module: Some(module.to_string()),
					});

					// dirty hack to fix unknowns in resolution
					if let Some(e) = class.enums.iter().find(|e| e.enum_name == "Enum") {
						classes.push(typespec::QmlTypeMapping {
							// filters gadgets
							name: qmlname.to_string(),
							cname: format!("{}::{}", e.namespace, e.enum_name),
							module: Some(module.to_string()),
						});
					}

					classes
				})
				.collect(),
			classes: self
				.classes
				.iter()
				.filter_map(|class| {
					let (description, details) = class
						.comment
						.map(parse_details_desc)
						.unwrap_or((None, None));

					Some(typespec::Class {
						name: class.name.to_string(),
						module: module.to_string(),
						description,
						details,
						// filters gadgets
						superclass: class.superclass?.to_string(),
						singleton: class.singleton,
						uncreatable: class.uncreatable,
						properties: class.properties.iter().map(|p| (*p).into()).collect(),
						functions: class.invokables.iter().map(|f| f.as_typespec()).collect(),
						signals: class.signals.iter().map(|s| s.as_typespec()).collect(),
						enums: class
							.enums
							.iter()
							.map(|enum_| {
								let (description, details) = enum_
									.comment
									.map(parse_details_desc)
									.unwrap_or((None, None));

								typespec::Enum {
									name: enum_.qml_name.to_string(),
									module: Some(module.to_string()),
									cname: Some(format!(
										"{}::{}",
										enum_.namespace, enum_.enum_name
									)),
									description,
									details,
									varaints: enum_.variants.iter().map(|v| (*v).into()).collect(),
								}
							})
							.collect(),
					})
				})
				.collect(),
			gadgets: self
				.classes
				.iter()
				.filter_map(|class| match class.type_ {
					ClassType::Gadget => Some(typespec::Gadget {
						cname: class.name.to_string(),
						properties: class.properties.iter().map(|p| (*p).into()).collect(),
					}),
					_ => None,
				})
				.collect(),
			enums: self
				.enums
				.iter()
				.map(|enum_| {
					let (description, details) = enum_
						.comment
						.map(parse_details_desc)
						.unwrap_or((None, None));

					typespec::Enum {
						name: enum_.qml_name.to_string(),
						module: Some(module.to_string()),
						cname: Some(format!("{}::{}", enum_.namespace, enum_.enum_name)),
						description,
						details,
						varaints: enum_.variants.iter().map(|v| (*v).into()).collect(),
					}
				})
				.collect(),
		}
	}
}

impl From<Property<'_>> for typespec::Property {
	fn from(value: Property) -> Self {
		Self {
			type_: value.type_.to_string(),
			name: value.name.to_string(),
			details: value.comment.map(parse_details),
			readable: value.readable,
			writable: value.writable,
			default: value.default,
		}
	}
}

impl From<Variant<'_>> for typespec::Variant {
	fn from(value: Variant<'_>) -> Self {
		Self {
			name: value.name.to_string(),
			details: value.comment.map(parse_details),
		}
	}
}

impl Invokable<'_> {
	fn as_typespec(&self) -> typespec::Function {
		typespec::Function {
			ret: self.ret.to_string(),
			name: self.name.to_string(),
			details: self.comment.map(parse_details),
			params: self.params.iter().map(|p| (*p).into()).collect(),
		}
	}
}

impl Signal<'_> {
	fn as_typespec(&self) -> typespec::Signal {
		typespec::Signal {
			name: self.name.to_string(),
			details: self.comment.map(parse_details),
			params: self.params.iter().map(|p| (*p).into()).collect(),
		}
	}
}

impl From<InvokableParam<'_>> for typespec::FnParam {
	fn from(value: InvokableParam<'_>) -> Self {
		Self {
			type_: value.type_.to_string(),
			name: value.name.to_string(),
		}
	}
}

fn parse_details(text: &str) -> String {
	let mut seen_content = false;
	let mut callout = false;

	let mut str = text
		.lines()
		.map(|line| {
			line.trim()
				.strip_prefix("///")
				.map(|line| line.strip_prefix(' ').unwrap_or(line))
				.unwrap_or(line)
		})
		.filter(|line| {
			let any = !line.is_empty();
			let filter = any || seen_content;
			seen_content |= any;
			filter
		})
		.map(|line| match callout {
			true => {
				if line.starts_with('>') {
					Cow::Borrowed(line[1..].strip_prefix(' ').unwrap_or(&line[1..]))
				} else {
					callout = false;
					Cow::Owned(format!("{{{{< /callout >}}}}\n{line}"))
				}
			},
			false => {
				if line.starts_with("> [!") {
					let code = line[4..].split_once(']');

					if let Some((code, line)) = code {
						let code = code.to_lowercase();
						callout = true;
						return Cow::Owned(format!("{{{{< callout type=\"{code}\" >}}}}\n{line}"))
					}
				}

				return Cow::Borrowed(line);
			},
		})
		.fold(String::new(), |accum, line| accum + line.as_ref() + "\n");

	if callout {
		str += "\n{{< /callout >}}";
	}

	str
}

fn parse_details_desc(text: &str) -> (Option<String>, Option<String>) {
	let details = parse_details(text);
	if details.starts_with('!') {
		match details[1..].split_once('\n') {
			Some((desc, details)) => (
				Some(desc.strip_prefix(' ').unwrap_or(desc).to_string()),
				Some(details.to_string()),
			),
			None => (
				Some(
					details[1..]
						.strip_prefix(' ')
						.unwrap_or(&details[1..])
						.to_string(),
				),
				None,
			),
		}
	} else {
		(None, Some(details))
	}
}