use crate::collect;
use crate::db::{File, parse};
use crate::goto_definition;
use crate::resolve;
use crate::symbols::Name;
use rowan::{TextRange, TextSize};
use salsa::Database as Db;
use squawk_syntax::ast::{self, AstNode};

/// `VSCode` has some theming options based on these types.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InlayHintKind {
    Type,
    Parameter,
}

#[derive(Clone, PartialEq, Eq)]
pub struct InlayHint {
    pub position: TextSize,
    pub label: String,
    pub kind: InlayHintKind,
    // Need this to be an Option because we can still inlay hints when we don't
    // have the destination.
    // For example: `insert into t(a, b) values (1, 2)`
    pub target: Option<TextRange>,
    // TODO: combine with the target range above
    pub file: Option<File>,
}

#[salsa::tracked]
pub fn inlay_hints(db: &dyn Db, file: File) -> Vec<InlayHint> {
    let mut hints = vec![];
    for node in parse(db, file).tree().syntax().descendants() {
        if let Some(call_expr) = ast::CallExpr::cast(node.clone()) {
            inlay_hint_call_expr(db, &mut hints, file, call_expr);
        } else if let Some(insert) = ast::Insert::cast(node) {
            inlay_hint_insert(db, &mut hints, file, insert);
        }
    }
    hints
}

fn inlay_hint_call_expr(
    db: &dyn Db,
    hints: &mut Vec<InlayHint>,
    file_id: File,
    call_expr: ast::CallExpr,
) -> Option<()> {
    let arg_list = call_expr.arg_list()?;
    let expr = call_expr.expr()?;

    let name_ref = if let Some(name_ref) = ast::NameRef::cast(expr.syntax().clone()) {
        name_ref
    } else {
        ast::FieldExpr::cast(expr.syntax().clone())?.field()?
    };

    let location =
        goto_definition::goto_definition(db, file_id, name_ref.syntax().text_range().start())
            .into_iter()
            .next()?;

    let def_file = parse(db, location.file).tree();

    let function_name_node = def_file.syntax().covering_element(location.range);

    if let Some(create_function) = function_name_node
        .ancestors()
        .find_map(ast::CreateFunction::cast)
        && let Some(param_list) = create_function.param_list()
    {
        for (param, arg) in param_list.params().zip(arg_list.args()) {
            if let Some(param_name) = param.name() {
                let arg_start = arg.syntax().text_range().start();
                let target = Some(param_name.syntax().text_range());
                hints.push(InlayHint {
                    position: arg_start,
                    label: format!("{}: ", param_name.syntax().text()),
                    kind: InlayHintKind::Parameter,
                    target,
                    file: Some(location.file),
                });
            }
        }
    };

    Some(())
}

fn inlay_hint_insert(
    db: &dyn Db,
    hints: &mut Vec<InlayHint>,
    file_id: File,
    insert: ast::Insert,
) -> Option<()> {
    let name_start = insert
        .path()?
        .segment()?
        .name_ref()?
        .syntax()
        .text_range()
        .start();
    // We need to support the table definition not being found since we can
    // still provide inlay hints when a column list is provided
    let location = goto_definition::goto_definition(db, file_id, name_start)
        .into_iter()
        .next();

    let def_file = location.as_ref().map(|loc| loc.file).unwrap_or(file_id);
    let def_tree = parse(db, def_file).tree();

    let create_table = location.as_ref().and_then(|loc| {
        def_tree
            .syntax()
            .covering_element(loc.range)
            .ancestors()
            .find_map(ast::CreateTableLike::cast)
    });

    let columns = if let Some(column_list) = insert.column_list() {
        // `insert into t(a, b, c) values (1, 2, 3)`
        column_list
            .columns()
            .filter_map(|col| {
                let col_name = col.name_ref().map(|x| Name::from_node(&x))?;
                let target = create_table
                    .as_ref()
                    .and_then(|x| resolve::find_column_in_create_table(db, def_file, x, &col_name))
                    .and_then(|x| x.into_iter().next())
                    .map(|x| x.range);
                Some((col_name, target, location.as_ref().map(|x| x.file)))
            })
            .collect()
    } else {
        // `insert into t values (1, 2, 3)`
        collect::columns_from_create_table(db, def_file, &create_table?)
            .into_iter()
            .map(|(col_name, ptr)| {
                let target = ptr.map(|p| p.text_range());
                (col_name, target, location.as_ref().map(|x| x.file))
            })
            .collect()
    };

    inlay_hint_insert_select(hints, columns, insert.select_variant()?)
}

fn inlay_hint_insert_select(
    hints: &mut Vec<InlayHint>,
    columns: Vec<(Name, Option<TextRange>, Option<File>)>,
    select_variant: ast::SelectVariant,
) -> Option<()> {
    if let ast::SelectVariant::Values(values) = &select_variant {
        // `insert into t values (1, 2);`
        for row in values.row_list()?.rows() {
            for ((column_name, target, file_id), expr) in columns.iter().zip(row.exprs()) {
                let expr_start = expr.syntax().text_range().start();
                hints.push(InlayHint {
                    position: expr_start,
                    label: format!("{}: ", column_name),
                    kind: InlayHintKind::Parameter,
                    target: *target,
                    file: *file_id,
                });
            }
        }
        return Some(());
    }

    // `insert into t select 1, 2;`
    let target_list = select_variant.target_list()?;
    for ((column_name, target, file_id), target_expr) in columns.iter().zip(target_list.targets()) {
        let expr = target_expr.expr()?;
        let expr_start = expr.syntax().text_range().start();
        hints.push(InlayHint {
            position: expr_start,
            label: format!("{}: ", column_name),
            kind: InlayHintKind::Parameter,
            target: *target,
            file: *file_id,
        });
    }

    Some(())
}

#[cfg(test)]
mod test {
    use crate::db::{Database, File};
    use crate::inlay_hints::inlay_hints;
    use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle};
    use insta::assert_snapshot;

    #[must_use]
    #[track_caller]
    fn check_inlay_hints(sql: &str) -> String {
        let db = Database::default();
        let file = File::new(&db, sql.to_string().into());

        assert_eq!(crate::db::parse(&db, file).errors(), vec![]);

        let hints = inlay_hints(&db, file);

        if hints.is_empty() {
            return String::new();
        }

        let mut modified_sql = sql.to_string();
        let mut insertions: Vec<(usize, String)> = hints
            .iter()
            .map(|hint| {
                let offset: usize = hint.position.into();
                (offset, hint.label.clone())
            })
            .collect();

        insertions.sort_by(|a, b| b.0.cmp(&a.0));

        for (offset, label) in &insertions {
            modified_sql.insert_str(*offset, label);
        }

        let mut annotations = vec![];
        let mut cumulative_offset = 0;

        insertions.reverse();
        for (original_offset, label) in insertions {
            let new_offset = original_offset + cumulative_offset;
            annotations.push((new_offset, label.len()));
            cumulative_offset += label.len();
        }

        let mut snippet = Snippet::source(&modified_sql).fold(true);

        for (offset, len) in annotations {
            snippet = snippet.annotation(AnnotationKind::Context.span(offset..offset + len));
        }

        let group = Level::INFO.primary_title("inlay hints").element(snippet);

        let renderer = Renderer::plain().decor_style(DecorStyle::Unicode);
        renderer
            .render(&[group])
            .to_string()
            .replace("info: inlay hints", "inlay hints:")
    }

    #[test]
    fn single_param() {
        assert_snapshot!(check_inlay_hints("
create function foo(a int) returns int as 'select $$1' language sql;
select foo(1);
"), @r"
        inlay hints:
          ╭▸ 
        3 │ select foo(a: 1);
          ╰╴           ───
        ");
    }

    #[test]
    fn multiple_params() {
        assert_snapshot!(check_inlay_hints("
create function add(a int, b int) returns int as 'select $$1 + $$2' language sql;
select add(1, 2);
"), @r"
        inlay hints:
          ╭▸ 
        3 │ select add(a: 1, b: 2);
          ╰╴           ───   ───
        ");
    }

    #[test]
    fn no_params() {
        assert_snapshot!(check_inlay_hints("
create function foo() returns int as 'select 1' language sql;
select foo();
"), @"");
    }

    #[test]
    fn with_schema() {
        assert_snapshot!(check_inlay_hints("
create function public.foo(x int) returns int as 'select $$1' language sql;
select public.foo(42);
"), @r"
        inlay hints:
          ╭▸ 
        3 │ select public.foo(x: 42);
          ╰╴                  ───
        ");
    }

    #[test]
    fn with_search_path() {
        assert_snapshot!(check_inlay_hints(r#"
set search_path to myschema;
create function foo(val int) returns int as 'select $$1' language sql;
select foo(100);
"#), @r"
        inlay hints:
          ╭▸ 
        4 │ select foo(val: 100);
          ╰╴           ─────
        ");
    }

    #[test]
    fn multiple_calls() {
        assert_snapshot!(check_inlay_hints("
create function inc(n int) returns int as 'select $$1 + 1' language sql;
select inc(1), inc(2);
"), @r"
        inlay hints:
          ╭▸ 
        3 │ select inc(n: 1), inc(n: 2);
          ╰╴           ───        ───
        ");
    }

    #[test]
    fn more_args_than_params() {
        assert_snapshot!(check_inlay_hints("
create function foo(a int) returns int as 'select $$1' language sql;
select foo(1, 2);
"), @r"
        inlay hints:
          ╭▸ 
        3 │ select foo(a: 1, 2);
          ╰╴           ───
        ");
    }

    #[test]
    fn builtin_function() {
        assert_snapshot!(check_inlay_hints("
select json_strip_nulls('[1, null]', true);
"), @r"
        inlay hints:
          ╭▸ 
        2 │ select json_strip_nulls(target: '[1, null]', strip_in_arrays: true);
          ╰╴                        ────────             ─────────────────
        ");
    }

    #[test]
    fn insert_with_column_list() {
        assert_snapshot!(check_inlay_hints("
create table t (column_a int, column_b int, column_c text);
insert into t (column_a, column_c) values (1, 'foo');
"), @r"
        inlay hints:
          ╭▸ 
        3 │ insert into t (column_a, column_c) values (column_a: 1, column_c: 'foo');
          ╰╴                                           ──────────   ──────────
        ");
    }

    #[test]
    fn insert_without_column_list() {
        assert_snapshot!(check_inlay_hints("
create table t (column_a int, column_b int, column_c text);
insert into t values (1, 2, 'foo');
"), @r"
        inlay hints:
          ╭▸ 
        3 │ insert into t values (column_a: 1, column_b: 2, column_c: 'foo');
          ╰╴                      ──────────   ──────────   ──────────
        ");
    }

    #[test]
    fn insert_multiple_rows() {
        assert_snapshot!(check_inlay_hints("
create table t (x int, y int);
insert into t values (1, 2), (3, 4);
"), @r"
        inlay hints:
          ╭▸ 
        3 │ insert into t values (x: 1, y: 2), (x: 3, y: 4);
          ╰╴                      ───   ───     ───   ───
        ");
    }

    #[test]
    fn insert_no_create_table() {
        assert_snapshot!(check_inlay_hints("
insert into t (a, b) values (1, 2);
"), @r"
        inlay hints:
          ╭▸ 
        2 │ insert into t (a, b) values (a: 1, b: 2);
          ╰╴                             ───   ───
        ");
    }

    #[test]
    fn insert_more_values_than_columns() {
        assert_snapshot!(check_inlay_hints("
create table t (a int, b int);
insert into t values (1, 2, 3);
"), @r"
        inlay hints:
          ╭▸ 
        3 │ insert into t values (a: 1, b: 2, 3);
          ╰╴                      ───   ───
        ");
    }

    #[test]
    fn insert_table_inherits_select() {
        assert_snapshot!(check_inlay_hints("
create table t (a int, b int);
create table u (c int) inherits (t);
insert into u select 1, 2, 3;
"), @"
        inlay hints:
          ╭▸ 
        4 │ insert into u select a: 1, b: 2, c: 3;
          ╰╴                     ───   ───   ───
        ");
    }

    #[test]
    fn insert_table_inherits_builtin_values() {
        assert_snapshot!(check_inlay_hints("
create table t ()
inherits (information_schema.sql_features);
insert into t values (1, 2, 3, 4, 5, 6, 7);
"), @"
        inlay hints:
          ╭▸ 
        4 │ …values (feature_id: 1, feature_name: 2, sub_feature_id: 3, sub_feature_name: 4, is_supported: 5, is_verified_by: 6, comments: 7);
          ╰╴         ────────────   ──────────────   ────────────────   ──────────────────   ──────────────   ────────────────   ──────────
        ");
    }

    #[test]
    fn insert_table_inherits_create_table_as_values() {
        assert_snapshot!(check_inlay_hints("
create table parent as select 1 a, 'x'::text b;
create table child (c int) inherits (parent);
insert into child values (1, 2, 3);
"), @"
        inlay hints:
          ╭▸ 
        4 │ insert into child values (a: 1, b: 2, c: 3);
          ╰╴                          ───   ───   ───
        ");
    }

    #[test]
    fn insert_table_inherits_create_table_as_select_star() {
        assert_snapshot!(check_inlay_hints("
create table base (a int, b text);
create table parent as select * from base;
create table child (c int) inherits (parent);
insert into child values (1, 2, 3);
"), @"
        inlay hints:
          ╭▸ 
        5 │ insert into child values (a: 1, b: 2, c: 3);
          ╰╴                          ───   ───   ───
        ");
    }

    #[test]
    fn insert_table_like_select() {
        assert_snapshot!(check_inlay_hints("
create table x (a int, b int);
create table y (c int, like x);
insert into y select 1, 2, 3;
"), @r"
        inlay hints:
          ╭▸ 
        4 │ insert into y select c: 1, a: 2, b: 3;
          ╰╴                     ───   ───   ───
        ");
    }

    #[test]
    fn insert_select() {
        assert_snapshot!(check_inlay_hints("
create table t (a int, b int);
insert into t select 1, 2;
"), @r"
        inlay hints:
          ╭▸ 
        3 │ insert into t select a: 1, b: 2;
          ╰╴                     ───   ───
        ");
    }

    #[test]
    fn insert_table_like_builtin_values() {
        assert_snapshot!(check_inlay_hints("
create table t (like information_schema.sql_features);
insert into t values (1, 2, 3, 4, 5, 6, 7);
"), @"
        inlay hints:
          ╭▸ 
        3 │ …values (feature_id: 1, feature_name: 2, sub_feature_id: 3, sub_feature_name: 4, is_supported: 5, is_verified_by: 6, comments: 7);
          ╰╴         ────────────   ──────────────   ────────────────   ──────────────────   ──────────────   ────────────────   ──────────
        ");
    }

    #[test]
    fn insert_table_like_select_into_values() {
        assert_snapshot!(check_inlay_hints("
select 1 a, 'x'::text b into parent;
create table child (like parent);
insert into child values (1, 2);
"), @"
        inlay hints:
          ╭▸ 
        4 │ insert into child values (a: 1, b: 2);
          ╰╴                          ───   ───
        ");
    }
}
