use proc_macro2::{Span, TokenStream};
use quote::{TokenStreamExt, quote};
use std::collections::HashMap;
use syn::Ident;

use crate::ast::{Enum, Field, Fields, Struct, Variant, Visit};

use super::common::{CalcDiscriminant, emit_serialize_fixed_le, emit_serialize_specialised};
use super::context::EncodingContext;
use super::optimised;

pub struct SerializeVisitor<'a> {
	pub revision: usize,
	pub ctx: EncodingContext,
	pub stream: &'a mut TokenStream,
}

impl<'a> SerializeVisitor<'a> {
	pub fn new(revision: usize, ctx: EncodingContext, stream: &'a mut TokenStream) -> Self {
		Self {
			revision,
			ctx,
			stream,
		}
	}
}

impl<'ast> Visit<'ast> for SerializeVisitor<'_> {
	fn visit_struct(&mut self, i: &'ast Struct) -> syn::Result<()> {
		if self.ctx.is_optimised() {
			let body = optimised::emit_struct_serialize(i, self.ctx);
			self.stream.append_all(body);
			return Ok(());
		}
		let mut ser_fields = TokenStream::new();
		SerializeFields {
			revision: self.revision,
			stream: &mut ser_fields,
		}
		.visit_struct(i)?;

		match i.fields {
			Fields::Named {
				ref fields,
				..
			} => {
				for f in fields.iter().filter(|x| x.attrs.options.exists_at(self.revision)) {
					let name = &f.name;
					self.stream.append_all(quote! { let #name = &self.#name; });
				}
				self.stream.append_all(ser_fields);
			}
			Fields::Unnamed {
				ref fields,
				..
			} => {
				for (idx, f) in
					fields.iter().filter(|x| x.attrs.options.exists_at(self.revision)).enumerate()
				{
					let binding = f.name.to_binding();
					let idx = syn::Index {
						index: idx as u32,
						span: Span::call_site(),
					};
					self.stream.append_all(quote! { let #binding = &self.#idx; });
				}
				self.stream.append_all(ser_fields);
			}
			Fields::Unit => {}
		}
		self.stream.append_all(quote! { Ok(()) });
		Ok(())
	}

	fn visit_enum(&mut self, i: &'ast Enum) -> syn::Result<()> {
		if self.ctx.is_optimised() {
			let body = optimised::emit_enum_serialize(i, self.ctx)?;
			self.stream.append_all(body);
			return Ok(());
		}
		let mut discriminants = HashMap::new();
		CalcDiscriminant::new(self.revision, &mut discriminants).visit_enum(i)?;

		let mut ser_variants = TokenStream::new();
		SerializeVariant {
			revision: self.revision,
			discriminants,
			stream: &mut ser_variants,
		}
		.visit_enum(i)?;

		self.stream.append_all(quote! {
			match *self{
				#ser_variants
			}
		});

		Ok(())
	}

	fn visit_field(&mut self, i: &'ast Field) -> syn::Result<()> {
		let name = &i.name;
		let writer = quote! { writer };
		let value = quote! { #name };
		let body = if i.attrs.options.fixed {
			emit_serialize_fixed_le(&i.ty, &value, &writer)?
		} else if i.attrs.options.specialised {
			emit_serialize_specialised(&i.ty, &value, &writer)
		} else {
			quote! {
				::revision::SerializeRevisioned::serialize_revisioned(#name, writer)?;
			}
		};

		self.stream.append_all(body);

		Ok(())
	}
}

pub struct SerializeFields<'a> {
	pub revision: usize,
	pub stream: &'a mut TokenStream,
}

impl<'ast> Visit<'ast> for SerializeFields<'_> {
	fn visit_field(&mut self, i: &'ast Field) -> syn::Result<()> {
		if !i.attrs.options.exists_at(self.revision) {
			return Ok(());
		}

		let name = i.name.to_binding();
		let writer = quote! { writer };
		let value = quote! { #name };
		let body = if i.attrs.options.fixed {
			emit_serialize_fixed_le(&i.ty, &value, &writer)?
		} else if i.attrs.options.specialised {
			emit_serialize_specialised(&i.ty, &value, &writer)
		} else {
			quote! {
				::revision::SerializeRevisioned::serialize_revisioned(#name, writer)?;
			}
		};
		self.stream.append_all(body);

		Ok(())
	}
}

pub struct SerializeVariant<'a> {
	pub revision: usize,
	pub discriminants: HashMap<Ident, u32>,
	pub stream: &'a mut TokenStream,
}

impl<'ast> Visit<'ast> for SerializeVariant<'_> {
	fn visit_variant(&mut self, i: &'ast Variant) -> syn::Result<()> {
		if !i.attrs.options.exists_at(self.revision) {
			return Ok(());
		}

		let name = &i.ident;

		self.stream.append_all(quote! {Self::#name});

		let discr =
			self.discriminants.get(name).expect("missed variant during discriminants calculation");

		match i.fields {
			Fields::Named {
				ref fields,
				..
			} => {
				let bindings = fields
					.iter()
					.filter(|x| x.attrs.options.exists_at(self.revision))
					.map(|x| &x.name);
				self.stream.append_all(quote! {
					{ #(ref #bindings),* }
				});

				let mut fields_ser = TokenStream::new();

				SerializeFields {
					revision: self.revision,
					stream: &mut fields_ser,
				}
				.visit_variant(i)?;

				self.stream.append_all(quote! {
					=> {
						::revision::SerializeRevisioned::serialize_revisioned(&#discr,writer)?;
						#fields_ser
						Ok(())
					},
				})
			}
			Fields::Unnamed {
				ref fields,
				..
			} => {
				let bindings = fields
					.iter()
					.filter(|x| x.attrs.options.exists_at(self.revision))
					.map(|x| x.name.to_binding());
				self.stream.append_all(quote! {
					( #(ref #bindings),* )
				});

				let mut fields_ser = TokenStream::new();

				SerializeFields {
					revision: self.revision,
					stream: &mut fields_ser,
				}
				.visit_variant(i)?;

				self.stream.append_all(quote! {
					=> {
						::revision::SerializeRevisioned::serialize_revisioned(&#discr,writer)?;
						#fields_ser
						Ok(())
					}
				})
			}
			Fields::Unit => {
				self.stream.append_all(quote! { => {
					::revision::SerializeRevisioned::serialize_revisioned(&#discr,writer)?;
					Ok(())
				}});
			}
		}

		Ok(())
	}
}
