mod content;
mod eval;
mod scope;

use std::path::PathBuf;

use patricia_tree::StringPatriciaSet;
use quote::quote;
use syn::punctuated::Punctuated;
use syn::{
    token, AngleBracketedGenericArguments, ExprLit, ExprTuple, GenericArgument, GenericParam,
    Generics, Ident, Item, ItemFn, ItemMod, ItemUse, Lit, LitStr, PathArguments, PathSegment,
    ReturnType, Signature, Stmt, TraitBound, TraitBoundModifier, TypeImplTrait, TypeParam,
    TypeParamBound, TypePath, UseGroup, UseName, UsePath, UseTree, Visibility,
};

const DOCS_DIR: &str = "docs";
const MAX_TUPLE_LEN: usize = 10;

pub fn files_to_rust(files: Vec<String>) -> String {
    let file = syn::File {
        shebang: None,
        attrs: Vec::new(),
        items: vec![files_to_modules(files)],
    };

    let output = quote!(#file).to_string();
    output
}

fn get_module_at(global_module: &mut ItemMod, limit: usize) -> &mut ItemMod {
    dbg!(&global_module, limit);
    let mut prev_deepest = global_module;
    let mut counter = 0;

    while counter < limit {
        prev_deepest = match prev_deepest.content.as_mut().unwrap().1.last_mut().unwrap() {
            Item::Mod(next) => next,
            _ => unreachable!(),
        };

        counter += 1;
    }

    prev_deepest
}

fn new_module(name: &str) -> ItemMod {
    ItemMod {
        attrs: Vec::new(),
        vis: Visibility::Public(token::Pub::default()),
        unsafety: None,
        mod_token: token::Mod::default(),
        ident: Ident::new(name, proc_macro2::Span::call_site()),
        content: Some((
            token::Brace::default(),
            vec![Item::Use(ItemUse {
                attrs: Vec::new(),
                vis: Visibility::Inherited,
                use_token: token::Use::default(),
                leading_colon: None,
                tree: UseTree::Path(UsePath {
                    ident: Ident::new("xilem_html", proc_macro2::Span::call_site()),
                    colon2_token: token::PathSep::default(),
                    tree: Box::new(UseTree::Group(UseGroup {
                        brace_token: token::Brace::default(),
                        items: Punctuated::<UseTree, token::Comma>::from_iter(
                            vec![
                                UseTree::Name(UseName {
                                    ident: Ident::new("elements", proc_macro2::Span::call_site()),
                                }),
                                UseTree::Name(UseName {
                                    ident: Ident::new(
                                        "ViewSequence",
                                        proc_macro2::Span::call_site(),
                                    ),
                                }),
                            ]
                            .into_iter(),
                        ),
                    })),
                }),
                semi_token: token::Semi::default(),
            })],
        )),
        semi: None,
    }
}

fn fs_path_to_module_ident(path: &str) -> String {
    let file_path = PathBuf::from(path);
    file_path
        .file_name()
        .unwrap()
        .to_string_lossy()
        .split('.')
        .next()
        .unwrap()
        .to_string()
}

fn files_to_modules(mut files: Vec<String>) -> Item {
    let paths = StringPatriciaSet::from_iter(files.iter());
    // Sorting the list of filenames allows for easier module creation later
    files.sort();

    let mut global = new_module(DOCS_DIR);

    let mut parent = &mut global;
    let mut depth = 0;

    for file in &files {
        // Get the longest shared prefix by getting the parent directory
        let file_path = PathBuf::from(&file);
        let file_parent = file_path.parent().unwrap().to_string_lossy();
        dbg!(&file_parent);
        let prefix = paths
            .get_longest_common_prefix(&file_parent)
            .unwrap_or(&file_parent);
        dbg!(prefix);
        let mut common_prefixed = paths.iter_prefix(prefix);
        let first_common = &common_prefixed.next().unwrap();
        let last_common = &common_prefixed.last().unwrap_or(file.clone());

        // Create Rust AST from file contents
        let typst_ast = eval::eval_file(&file);
        // let xilem_expressions = typst_to_xilem(typst_ast.content());
        // let function = xilem_to_function(&fs_path_to_module_ident(&file), xilem_expressions);
        let function = scope::typst_scope_to_rust(typst_ast.scope());

        // Increase depth if first item in prefix
        // Make sure not to immediately jump 1 level too deep if there's only 1 file
        if first_common == file && prefix != DOCS_DIR {
            depth += 1;
            parent
                .content
                .as_mut()
                .unwrap()
                .1
                .push(Item::Mod(new_module(
                    &file_path
                        .parent()
                        .unwrap()
                        .file_name()
                        .unwrap()
                        .to_string_lossy(),
                )));
            parent = get_module_at(&mut global, depth);
        }

        // Get a mutable reference to the module children
        let content = &mut parent.content.as_mut();
        // Append the function corresponding to our Typst file
        content
            .get_or_insert(&mut ((token::Brace::default()), Vec::new()))
            .1
            .push(function);

        // Decrease depth if last item in prefix
        dbg!(&first_common, &last_common);
        if last_common == file && depth > 0 {
            depth -= 1;
            parent = get_module_at(&mut global, depth);
        }
    }

    Item::Mod(global)
}

fn xilem_to_function(name: &str, blocks: Vec<syn::Expr>) -> Item {
    Item::Fn(ItemFn {
        attrs: Vec::new(),
        vis: Visibility::Public(token::Pub::default()),
        sig: Signature {
            constness: None,
            asyncness: None,
            unsafety: None,
            abi: None,
            fn_token: token::Fn::default(),
            ident: Ident::new(name, proc_macro2::Span::call_site()),
            generics: Generics { lt_token: None, params: Punctuated::from_iter(vec![GenericParam::Type(TypeParam { attrs: Vec::new(), ident: Ident::new("T", proc_macro2::Span::call_site()), colon_token: None, bounds: Punctuated::new(), eq_token: None, default: None })].into_iter()), gt_token: None, where_clause: None },
            paren_token: token::Paren::default(),
            inputs: Punctuated::new(),
            variadic: None,
            output: ReturnType::Type(
                token::RArrow::default(),
                Box::new(syn::Type::ImplTrait(TypeImplTrait {
                    impl_token: token::Impl::default(),
                    bounds: Punctuated::from_iter(
                        vec![TypeParamBound::Trait(TraitBound {
                            paren_token: None,
                            modifier: TraitBoundModifier::None,
                            lifetimes: None,
                            path: syn::Path {
                                leading_colon: None,
                                segments: Punctuated::from_iter(
                                    vec![PathSegment {
                                        ident: Ident::new(
                                            "ViewSequence",
                                            proc_macro2::Span::call_site(),
                                        ),
                                        arguments: PathArguments::AngleBracketed(
                                            AngleBracketedGenericArguments {
                                                colon2_token: None,
                                                lt_token: token::Lt::default(),
                                                args: Punctuated::from_iter(
                                                    vec![GenericArgument::Type(
                                                        syn::Type::Path(TypePath {
                                                            qself: None,
                                                            path: syn::Path {
                                                                leading_colon: None,
                                                                segments: Punctuated::from_iter(vec![PathSegment { ident: Ident::new("T", proc_macro2::Span::call_site()), arguments: PathArguments::None }].into_iter()),
                                                            },
                                                        }),
                                                    )]
                                                    .into_iter(),
                                                ),
                                                gt_token: token::Gt::default(),
                                            },
                                        ),
                                    }]
                                    .into_iter(),
                                ),
                            },
                        })]
                        .into_iter(),
                    ),
                })),
            ),
        },
        block: Box::new(syn::Block {
            brace_token: token::Brace::default(),
            stmts: vec![Stmt::Expr(
                syn::Expr::Tuple(ExprTuple {
                    attrs: Vec::new(),
                    paren_token: token::Paren::default(),
                    elems: Punctuated::from_iter(blocks.into_iter()),
                }),
                None,
            )],
        }),
    })
}

fn extract_literal_string(expression: &ExprLit) -> String {
    if let Lit::Str(lit_str) = &expression.lit {
        lit_str.value()
    } else {
        unreachable!()
    }
}

fn typst_to_xilem(root: typst::model::Content) -> Vec<syn::Expr> {
    let typed_content = content::SupportedContent::downcast(&root);
    let xilem_expressions = typed_content
        .to_xilem()
        .into_iter()
        // Merge any adjacent string literal
        .fold(Vec::new(), |mut state, inline| {
            if let syn::Expr::Lit(current_literal) = &inline {
                if let Some(syn::Expr::Lit(ref mut prev_literal)) = state.last_mut() {
                    let prev_string = extract_literal_string(&prev_literal);
                    let current_string = extract_literal_string(&current_literal);
                    let merged_string = prev_string + &current_string;
                    *prev_literal = literal_string(merged_string.as_str());
                    return state;
                }
            }

            state.push(inline);
            state
        });
    let tuples = split_tuple(xilem_expressions);

    tuples
}

fn literal_string(value: &str) -> ExprLit {
    let lit_str = LitStr::new(value, proc_macro2::Span::call_site());
    ExprLit {
        attrs: Vec::new(),
        lit: Lit::Str(lit_str),
    }
}

// Xilem only supports tuples of max length 10, so split up inlines into nested tuples until it fits
fn split_tuple(expressions: Vec<syn::Expr>) -> Vec<syn::Expr> {
    if expressions.len() <= MAX_TUPLE_LEN {
        return expressions;
    }

    let mut elements: Vec<syn::Expr> = Vec::new();

    for expression_chunk in expressions.chunks(MAX_TUPLE_LEN) {
        let expression_group: Vec<syn::Expr> = expression_chunk.to_vec();
        elements.push(syn::Expr::Tuple(ExprTuple {
            attrs: Vec::new(),
            paren_token: token::Paren::default(),
            elems: Punctuated::from_iter(expression_group.into_iter()),
        }))
    }

    // May need to recursively nest inside tuples
    if elements.len() > MAX_TUPLE_LEN {
        elements = split_tuple(elements);
    }
    elements
}