//! Port of C [vtablog](https://sqlite.org/src/file/ext/misc/vtablog.c)
use std::borrow::Cow;
use std::ffi::{c_int, CStr, CString};
use std::marker::PhantomData;
use std::str::FromStr as _;
use std::sync::atomic::{AtomicUsize, Ordering};

use fallible_iterator::FallibleIterator as _;

use crate::types::Type;
use crate::vtab::{
    Context, CreateVTab, Filters, IndexInfo, Inserts, Module, TransactionVTab, UpdateVTab, Updates,
    VTab, VTabConnection, VTabCursor, VTabKind,
};
use crate::{ffi, ValueRef};
use crate::{Connection, Error, Result};

const MODULE_NAME: &CStr = c"vtablog";

/// Register the "vtablog" module.
pub fn load_module(conn: &Connection) -> Result<()> {
    const MODULE: Module<VTabLog> = Module::update_module_with_tx();
    let aux: Option<()> = None;
    conn.create_module(MODULE_NAME, &MODULE, aux)
}

/// An instance of the vtablog virtual table
#[repr(C)]
struct VTabLog {
    /// Base class. Must be first
    base: ffi::sqlite3_vtab,
    /// Associated connection
    db: *mut ffi::sqlite3,
    /// Number of rows in the table
    n_row: i64,
    /// Instance number for this vtablog table
    i_inst: usize,
    /// Number of cursors created
    n_cursor: usize,
}

impl VTabLog {
    fn connect_create(
        db: &mut VTabConnection,
        aux: Option<&()>,
        module_name: &[u8],
        database_name: &[u8],
        table_name: &[u8],
        args: &[&[u8]],
        is_create: bool,
    ) -> Result<(Cow<'static, CStr>, Self)> {
        debug_assert_eq!(aux, None);
        debug_assert_eq!(module_name, MODULE_NAME.to_bytes());
        static N_INST: AtomicUsize = AtomicUsize::new(1);
        let i_inst = N_INST.fetch_add(1, Ordering::SeqCst);
        println!(
            "VTabLog::{}(tab={}, database_name={}, table_name={}, args={:?}):",
            if is_create { "create" } else { "connect" },
            i_inst,
            str::from_utf8(database_name)?,
            str::from_utf8(table_name)?,
            args.iter().map(|b| str::from_utf8(b)).collect::<Vec<_>>(),
        );
        let mut schema = None;
        let mut n_row = None;

        for c_slice in args {
            let (param, value) = super::parameter(c_slice)?;
            let value = value.as_ref();
            match param {
                "schema" => {
                    if schema.is_some() {
                        return Err(Error::ModuleError(format!(
                            "more than one '{param}' parameter"
                        )));
                    }
                    schema = Some(CString::new(value)?);
                }
                "rows" => {
                    if n_row.is_some() {
                        return Err(Error::ModuleError(format!(
                            "more than one '{param}' parameter"
                        )));
                    }
                    if let Ok(n) = i64::from_str(value) {
                        n_row = Some(n);
                    }
                }
                _ => {
                    return Err(Error::ModuleError(format!(
                        "unrecognized parameter '{param}'"
                    )));
                }
            }
        }
        if schema.is_none() {
            return Err(Error::ModuleError("no schema defined".to_owned()));
        }
        let vtab = Self {
            base: ffi::sqlite3_vtab::default(),
            db: unsafe { db.handle() },
            n_row: n_row.unwrap_or(10),
            i_inst,
            n_cursor: 0,
        };
        Ok((Cow::Owned(schema.unwrap()), vtab))
    }
}

impl Drop for VTabLog {
    fn drop(&mut self) {
        println!("VTabLog::drop({})", self.i_inst);
    }
}

unsafe impl<'vtab> VTab<'vtab> for VTabLog {
    type Aux = ();
    type Cursor = VTabLogCursor<'vtab>;

    fn connect(
        db: &mut VTabConnection,
        aux: Option<&Self::Aux>,
        module_name: &[u8],
        database_name: &[u8],
        table_name: &[u8],
        args: &[&[u8]],
    ) -> Result<(Cow<'static, CStr>, Self)> {
        Self::connect_create(db, aux, module_name, database_name, table_name, args, false)
    }

    fn best_index(&self, info: &mut IndexInfo) -> Result<bool> {
        println!(
            "VTabLog::best_index({}, num_of_order_by: {}, col_used: {}, distinct: {:?})",
            self.i_inst,
            info.num_of_order_by(),
            info.col_used(),
            info.distinct()
        );
        let mut in_constraint = None;
        for (i, constraint) in info.constraints().enumerate() {
            println!(
                "  constraint[{}]: col={}, usable={}, op={:?}, rhs={:?}, in={:?}",
                i,
                constraint.column(),
                constraint.is_usable(),
                constraint.operator(),
                info.rhs_value(i),
                info.is_in_constraint(i),
            );
            if info.is_in_constraint(i)? {
                in_constraint = Some(i);
            }
        }
        info.set_estimated_cost(500.);
        info.set_estimated_rows(500);
        info.set_idx_str("idx");
        info.set_idx_cstr(c"idx");
        if let Some(idx) = in_constraint {
            info.set_in_constraint(idx, true)?;
            info.constraint_usage(idx).set_argv_index(1);
        }
        Ok(true)
    }

    fn open(&'vtab mut self) -> Result<Self::Cursor> {
        self.n_cursor += 1;
        println!(
            "VTabLog::open(tab={}, cursor={})",
            self.i_inst, self.n_cursor
        );
        Ok(VTabLogCursor {
            base: ffi::sqlite3_vtab_cursor::default(),
            i_cursor: self.n_cursor,
            row_id: 0,
            phantom: PhantomData,
        })
    }
}

impl CreateVTab<'_> for VTabLog {
    const KIND: VTabKind = VTabKind::Default;

    fn create(
        db: &mut VTabConnection,
        aux: Option<&Self::Aux>,
        module_name: &[u8],
        database_name: &[u8],
        table_name: &[u8],
        args: &[&[u8]],
    ) -> Result<(Cow<'static, CStr>, Self)> {
        Self::connect_create(db, aux, module_name, database_name, table_name, args, true)
    }

    fn destroy(&self) -> Result<()> {
        println!("VTabLog::destroy({})", self.i_inst);
        Ok(())
    }
}

impl UpdateVTab<'_> for VTabLog {
    fn delete(&mut self, arg: ValueRef<'_>) -> Result<()> {
        println!("VTabLog::delete({}, {arg:?})", self.i_inst);
        Ok(())
    }

    fn insert(&mut self, args: &Inserts<'_>) -> Result<i64> {
        println!(
            "VTabLog::insert({}, on_conflict:{:?}, {:?})",
            self.i_inst,
            unsafe { args.on_conflict(self.db) },
            args.iter().collect::<Vec<ValueRef<'_>>>()
        );
        Ok(self.n_row)
    }

    fn update(&mut self, args: &Updates<'_>) -> Result<()> {
        println!(
            "VTabLog::update({}, on_conflict:{:?}, {:?})",
            self.i_inst,
            unsafe { args.on_conflict(self.db) },
            args.iter()
                .enumerate()
                .map(|(i, v)| (v, args.no_change(i)))
                .collect::<Vec<(ValueRef<'_>, bool)>>()
        );
        Ok(())
    }
}

impl TransactionVTab<'_> for VTabLog {
    fn begin(&mut self) -> Result<()> {
        println!("VTabLog::begin({})", self.i_inst);
        Ok(())
    }

    fn sync(&mut self) -> Result<()> {
        println!("VTabLog::sync({})", self.i_inst);
        Ok(())
    }

    fn commit(&mut self) -> Result<()> {
        println!("VTabLog::commit({})", self.i_inst);
        Ok(())
    }

    fn rollback(&mut self) -> Result<()> {
        println!("VTabLog::rollback({})", self.i_inst);
        Ok(())
    }
}

/// A cursor for the Series virtual table
#[repr(C)]
struct VTabLogCursor<'vtab> {
    /// Base class. Must be first
    base: ffi::sqlite3_vtab_cursor,
    /// Cursor number
    i_cursor: usize,
    /// The rowid
    row_id: i64,
    phantom: PhantomData<&'vtab VTabLog>,
}

impl VTabLogCursor<'_> {
    fn vtab(&self) -> &VTabLog {
        unsafe { &*(self.base.pVtab as *const VTabLog) }
    }
}

impl Drop for VTabLogCursor<'_> {
    fn drop(&mut self) {
        println!(
            "VTabLogCursor::drop(tab={}, cursor={})",
            self.vtab().i_inst,
            self.i_cursor
        );
    }
}

unsafe impl VTabCursor for VTabLogCursor<'_> {
    fn filter(&mut self, idx_num: c_int, idx_str: Option<&str>, args: &Filters<'_>) -> Result<()> {
        println!(
            "VTabLogCursor::filter(tab={}, cursor={}, idx_num={idx_num}, idx_str={idx_str:?}, args={})",
            self.vtab().i_inst,
            self.i_cursor,
            args.len()
        );
        for (i, arg) in args.iter().enumerate() {
            if arg.data_type() == Type::Null {
                println!(
                    " in_values[{}]: {:?}",
                    i,
                    args.in_values(i)?.collect::<Vec<ValueRef>>()
                );
            }
        }
        self.row_id = 0;
        Ok(())
    }

    fn next(&mut self) -> Result<()> {
        println!(
            "VTabLogCursor::next(tab={}, cursor={}): rowid {} -> {}",
            self.vtab().i_inst,
            self.i_cursor,
            self.row_id,
            self.row_id + 1
        );
        self.row_id += 1;
        Ok(())
    }

    fn eof(&self) -> bool {
        let eof = self.row_id >= self.vtab().n_row;
        println!(
            "VTabLogCursor::eof(tab={}, cursor={}): {}",
            self.vtab().i_inst,
            self.i_cursor,
            eof,
        );
        eof
    }

    fn column(&self, ctx: &mut Context, i: c_int) -> Result<()> {
        if ctx.no_change() {
            println!(
                "VTabLogCursor::column(tab={}, cursor={}, i={}): no change",
                self.vtab().i_inst,
                self.i_cursor,
                i,
            );
            return Ok(());
        }
        let value = if i < 26 {
            format!(
                "{}{}",
                "abcdefghijklmnopqrstuvwyz".chars().nth(i as usize).unwrap(),
                self.row_id
            )
        } else {
            format!("{i}{}", self.row_id)
        };
        println!(
            "VTabLogCursor::column(tab={}, cursor={}, i={}): {}",
            self.vtab().i_inst,
            self.i_cursor,
            i,
            value,
        );
        if i == 0 {
            println!("  db busy: {:?}", unsafe {
                ctx.get_connection().map(|c| c.is_busy())
            });
        }
        ctx.set_result(&value)
    }

    fn rowid(&self) -> Result<i64> {
        println!(
            "VTabLogCursor::rowid(tab={}, cursor={}): {}",
            self.vtab().i_inst,
            self.i_cursor,
            self.row_id,
        );
        Ok(self.row_id)
    }
}

#[cfg(test)]
mod test {
    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
    use wasm_bindgen_test::wasm_bindgen_test as test;

    use crate::{Connection, Result};
    #[test]
    fn test_module() -> Result<()> {
        let db = Connection::open_in_memory()?;
        super::load_module(&db)?;

        db.execute_batch(
            "CREATE VIRTUAL TABLE temp.log USING vtablog(
                    schema='CREATE TABLE x(a,b,c)',
                    rows=3
                );",
        )?;
        let mut stmt = db.prepare("SELECT * FROM log;")?;
        let mut rows = stmt.query([])?;
        while rows.next()?.is_some() {}
        db.execute("DELETE FROM log WHERE a = ?1", ["a1"])?;
        db.execute(
            "INSERT INTO log (a, b, c) VALUES (?1, ?2, ?3)",
            ["a", "b", "c"],
        )?;
        db.execute(
            "UPDATE log SET b = ?1, c = ?2 WHERE a = ?3",
            ["bn", "cn", "a1"],
        )?;
        db.query_one("SELECT b, c FROM log WHERE a = 'a1'", [], |_| Ok(0))?;
        db.execute("UPDATE log SET b = '' WHERE a IN (?1, ?2)", ["a1", "a2"])?;
        Ok(())
    }
}
