use proc_macro2::*;
use quote::ToTokens;
use std::fmt::Write;
use syn::{
    punctuated::Punctuated, spanned::Spanned, token::Comma, BinOp, Block, Expr, ExprAssign,
    ExprBinary, ExprBlock, ExprBreak, ExprCall, ExprContinue, ExprField, ExprGroup, ExprIf,
    ExprLit, ExprLoop, ExprParen, ExprPath, ExprReturn, ExprWhile, FnArg, ItemConst, ItemFn, Local,
    Pat, PatType, ReturnType, Signature, Stmt, Type, ExprCast,
};

macro_rules! err {
    ($ctx:expr, $reason:ident) => {{
        return Err(ToVulkanError::Unsupported($ctx.span(), Reason::$reason));
    }};
}

pub trait ToVulkan {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError>;
}

impl ToVulkan for ItemFn {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        self.sig.to_vulkan(w)?;
        self.block.to_vulkan(w)?;
        Ok(())
    }
}

impl ToVulkan for Signature {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        self.output.to_vulkan(w)?;
        w.write_char(' ')?;
        self.ident.to_vulkan(w)?;
        w.write_char('(')?;
        self.inputs.to_vulkan(w)?;
        w.write_char(')')?;
        Ok(())
    }
}

impl<T> ToVulkan for Punctuated<T, Comma>
where
    T: ToVulkan + Spanned,
{
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        let mut insert_comma = false;
        for elem in self.iter() {
            if insert_comma {
                w.write_str(", ")?;
            }
            elem.to_vulkan(w)?;
            insert_comma = true;
        }
        Ok(())
    }
}

impl ToVulkan for FnArg {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        match self {
            FnArg::Receiver(_) => err!(self, SelfArg),
            FnArg::Typed(ty) => ty.to_vulkan(w),
        }
    }
}

impl ToVulkan for PatType {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        self.ty.to_vulkan(w)?;
        w.write_char(' ')?;
        match &*self.pat {
            syn::Pat::Ident(ident) => ident.ident.to_vulkan(w),
            _ => err!(self.pat, PatternMatching),
        }
    }
}

impl ToVulkan for Ident {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        write!(w, "{self}")?;
        Ok(())
    }
}

impl ToVulkan for ReturnType {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        match self {
            Self::Default => write!(w, "void")?,
            Self::Type(_, t) => t.to_vulkan(w)?,
        };
        Ok(())
    }
}

/// translates types from rust to vulkan
fn translate_ty(ty: &Ident) -> Result<String, ToVulkanError> {
    let string = ty.to_string();
    let string: &str = &string;
    Ok(match string {
        "bool" => "bool",
        "i32" => "int",
        "u32" => "uint",
        "f32" => "float",
        "f64" => "double",
        ty if ty.contains("vec") => ty,
        _ => err!(ty, Type),
    }
    .into())
}

impl ToVulkan for Type {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        match self {
            Type::Path(ty) => match ty.path.get_ident() {
                Some(ty) => {
                    let ty = translate_ty(&ty)?;
                    w.write_str(&ty)?;
                    Ok(())
                }
                None => err!(self, Path),
            },
            _ => err!(self, Type),
        }
    }
}

impl ToVulkan for Block {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        w.write_char('{')?;
        w.write_char('\n')?;
        for stmt in &self.stmts {
            stmt.to_vulkan(w)?;
            if !stmt.is_block() {
                w.write_str(";")?;
            }
            w.write_str("\n")?;
        }
        w.write_char('}')?;
        Ok(())
    }
}

impl ToVulkan for Stmt {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        match self {
            Stmt::Local(Local {
                pat:
                    Pat::Type(PatType {
                        pat: box Pat::Ident(var_name),
                        ty: box Type::Path(ty),
                        ..
                    }),
                init,
                ..
            }) => {
                let var_ty = match ty.path.get_ident() {
                    Some(ty) => ty,
                    None => err!(ty, Path),
                };
                let ty_name = translate_ty(&var_ty)?;
                w.write_str(&ty_name)?;
                w.write_char(' ')?;
                w.write_str(&format!("{}", var_name.ident))?;
                if let Some(init) = init {
                    w.write_str(" = ")?;
                    init.expr.to_vulkan(w)?;
                }
                Ok(())
            }
            Stmt::Local(var) => {
                err!(var, NoTypeAnnotation)
            }
            Stmt::Expr(e, _) => e.to_vulkan(w),
            Stmt::Macro(_) | Stmt::Item(_) => err!(self, NestedItem),
        }
    }
}

trait IsBlock {
    fn is_block(&self) -> bool;
}

impl IsBlock for Stmt {
    fn is_block(&self) -> bool {
        match self {
            Stmt::Local(_) => false,
            Stmt::Item(_) => true,
            Stmt::Expr(e, _) => e.is_block(),
            Stmt::Macro(_) => true,
        }
    }
}

impl IsBlock for Expr {
    fn is_block(&self) -> bool {
        match self {
            Expr::If(_)
            | Expr::Loop(_)
            | Expr::While(_)
            | Expr::Async(_)
            | Expr::Block(_)
            | Expr::Const(_)
            | Expr::Macro(_)
            | Expr::Match(_)
            | Expr::Unsafe(_)
            | Expr::ForLoop(_)
            | Expr::TryBlock(_) => true,
            _ => false,
        }
    }
}

impl ToVulkan for Expr {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        match self {
            Expr::Binary(b) => b.to_vulkan(w),
            Expr::Lit(l) => l.to_vulkan(w),
            Expr::Group(g) => g.to_vulkan(w),
            Expr::Paren(g) => g.to_vulkan(w),
            Expr::Path(p) => p.to_vulkan(w),
            Expr::Return(r) => r.to_vulkan(w),
            Expr::Call(c) => c.to_vulkan(w),
            Expr::While(e) => e.to_vulkan(w),
            Expr::Assign(a) => a.to_vulkan(w),
            Expr::Loop(l) => l.to_vulkan(w),
            Expr::Break(b) => b.to_vulkan(w),
            Expr::Continue(c) => c.to_vulkan(w),
            Expr::Field(f) => f.to_vulkan(w),
            Expr::If(i) => i.to_vulkan(w),
            Expr::Block(b) => b.to_vulkan(w),
            Expr::Tuple(t) => err!(t, TupleExpr),
            Expr::Cast(c) => c.to_vulkan(w),
            _ => err!(self, NotImplemented),
        }
    }
}

impl ToVulkan for ExprCast {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        w.write_str("(")?;
        self.ty.to_vulkan(w)?;
        w.write_str(") ")?;
        self.expr.to_vulkan(w)
    }
}

impl ToVulkan for ExprIf {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        w.write_str("if(")?;
        self.cond.to_vulkan(w)?;
        w.write_str(")")?;
        self.then_branch.to_vulkan(w)?;
        if let Some((_, else_branch)) = &self.else_branch {
            w.write_str("else ")?;
            else_branch.to_vulkan(w)?;
        }
        Ok(())
    }
}
impl ToVulkan for ExprBlock {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        self.block.to_vulkan(w)
    }
}

impl ToVulkan for ExprField {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        self.base.to_vulkan(w)?;
        w.write_char('.')?;
        match &self.member {
            syn::Member::Named(n) => n.to_vulkan(w)?,
            syn::Member::Unnamed(u) => err!(u, UnnamedField),
        };
        Ok(())
    }
}

impl ToVulkan for ExprContinue {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        if let Some(_) = &self.label {
            err!(self, LabelLoop);
        } else {
            write!(w, "continue")?;
        }
        Ok(())
    }
}

impl ToVulkan for ExprBreak {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        if let Some(_) = &self.label {
            err!(self, LabelLoop);
        } else {
            write!(w, "break")?;
        }
        Ok(())
    }
}

impl ToVulkan for ExprLoop {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        if let Some(label) = &self.label {
            err!(label, LabelLoop);
        }
        w.write_str("while(true)\n")?;
        self.body.to_vulkan(w)?;
        w.write_str("\n")?;
        Ok(())
    }
}

impl ToVulkan for ExprAssign {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        self.left.to_vulkan(w)?;
        w.write_str(" = ")?;
        self.right.to_vulkan(w)?;
        Ok(())
    }
}

impl ToVulkan for ExprWhile {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        if let Some(label) = &self.label {
            err!(label, LabelLoop);
        }
        w.write_str("while(")?;
        self.cond.to_vulkan(w)?;
        w.write_str(")\n")?;
        self.body.to_vulkan(w)?;
        w.write_str("\n")?;
        Ok(())
    }
}

impl ToVulkan for ExprReturn {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        w.write_str("return ")?;
        if let Some(expr) = &self.expr {
            expr.to_vulkan(w)?;
        }
        Ok(())
    }
}

impl ToVulkan for ExprPath {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        match self.path.get_ident() {
            Some(ident) => ident.to_vulkan(w),
            None => err!(self, Path),
        }
    }
}

impl ToVulkan for ExprParen {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        w.write_char('(')?;
        self.expr.to_vulkan(w)?;
        w.write_char(')')?;
        Ok(())
    }
}

impl ToVulkan for ExprGroup {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        w.write_char('(')?;
        self.expr.to_vulkan(w)?;
        w.write_char(')')?;
        Ok(())
    }
}

impl ToVulkan for ExprLit {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        let string = self.to_token_stream().to_string();
        w.write_str(&string)?;
        Ok(())
    }
}

impl ToVulkan for ExprBinary {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        self.left.to_vulkan(w)?;
        w.write_char(' ')?;
        self.op.to_vulkan(w)?;
        w.write_char(' ')?;
        self.right.to_vulkan(w)?;
        Ok(())
    }
}

impl ToVulkan for ExprCall {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        match &*self.func {
            Expr::Path(p) => match p.path.get_ident() {
                Some(fun) => {
                    fun.to_vulkan(w)?;
                    w.write_char('(')?;
                    self.args.to_vulkan(w)?;
                    w.write_char(')')?;
                    Ok(())
                }
                None => err!(p, Path),
            },
            _ => err!(self.func, FunctionSource),
        }
    }
}

impl ToVulkan for ItemConst {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        self.ty.to_vulkan(w)?;
        w.write_char(' ')?;
        self.ident.to_vulkan(w)?;
        w.write_str(" = ")?;
        self.expr.to_vulkan(w)?;
        w.write_char(';')?;
        Ok(())
    }
}

impl ToVulkan for BinOp {
    fn to_vulkan(&self, w: &mut impl Write) -> Result<(), ToVulkanError> {
        let string = self.to_token_stream().to_string();
        w.write_str(&string)?;
        Ok(())
    }
}

impl From<std::fmt::Error> for ToVulkanError {
    fn from(value: std::fmt::Error) -> Self {
        Self::FmtErr(value)
    }
}

pub enum Reason {
    SelfArg,
    NoTypeAnnotation,
    TupleExpr,
    UnnamedField,
    Path,
    PatternMatching,
    FunctionSource,
    NestedItem,
    NotImplemented,
    Type,
    LabelLoop,
    #[allow(unused)]
    Unknown,
}

impl Reason {
    pub fn as_str(&self) -> &'static str {
        use Reason::*;
        match self {
            SelfArg => "self parameter",
            NoTypeAnnotation => "missing type annotation",
            UnnamedField => "Cannot encode unnamed fields",
            TupleExpr => "GLSL does not have tuples",
            Type => "unsupported type",
            Path => "functions and constants can only be directly referenced (no path using ::). Consider bringing them in scope with `use`",
            PatternMatching => "pattern matching is only supported in very limited ways",
            FunctionSource => "can only call functions that are defined on a global scope",
            NestedItem => "Cannot nest items such as functions",
            NotImplemented => "Feature not yet implemented",
            LabelLoop => "Cannot label loops for use in break and continue",
            Unknown => "unknown",
        }
    }
}
pub enum ToVulkanError {
    FmtErr(std::fmt::Error),
    Unsupported(Span, Reason),
}

pub fn struct_name(id: &Ident) -> Ident {
    Ident::new(&format!("__glsl_item__{}__", id), id.span())
}

pub fn item_name(id: &Ident) -> Ident {
    Ident::new(&format!("GLSL_{}", id.to_string().to_uppercase()), id.span())
}