use crate::client::Response;
use crate::connection::Authenticated;
use crate::query::{WithInfo, WithoutInfo};
use crate::resource::AsResource;
use crate::{Connection, DatabaseStream, Query, Result};
use std::borrow::{Borrow, BorrowMut};
use std::marker::PhantomData;
use std::net::TcpStream;

/// Represents database command code in the [standard mode](https://docs.basex.org/wiki/Standard_Mode).
enum Command {
    Query = 0,
    Create = 8,
    Add = 9,
    Replace = 12,
    Store = 13,
}

/// Encapsulates a command with optional input. To execute it, either call [`with_input`] or [`without_input`].
///
/// [`with_input`]: self::CommandWithOptionalInput::with_input
/// [`without_input`]: self::CommandWithOptionalInput::without_input
pub struct CommandWithOptionalInput<'a, T>
where
    T: DatabaseStream,
{
    connection: &'a mut Connection<T, Authenticated>,
}

impl<'a, T> CommandWithOptionalInput<'a, T>
where
    T: DatabaseStream,
{
    fn new(connection: &'a mut Connection<T, Authenticated>) -> Self {
        Self { connection }
    }

    /// Sends the input to the command and executes it, returning its response as a string.
    pub fn with_input<'b, R: AsResource<'b>>(self, input: R) -> Result<String> {
        self.connection.send_arg(&mut input.into_read())?;
        self.connection.get_response()
    }

    /// Omits the input from command and executes it, returning its response as a string.
    pub fn without_input(self) -> Result<String> {
        self.connection.skip_arg()?;
        self.connection.get_response()
    }
}

/// Represents an interface to communicate with the BaseX server. Its main purpose is to send database
/// [commands](https://docs.basex.org/wiki/Commands) and create [queries](https://docs.basex.org/wiki/XQuery).
///
/// Start by connecting to the database using [`Client::connect`].
///
/// # Example
/// ```
/// # use basex::{Client, ClientError, Connection};
/// # use std::io::Read;
/// # fn main() -> Result<(), ClientError> {
/// let mut client = Client::connect("localhost", 1984, "admin", "admin")?;
///
/// let info = client.create("a45d766")?
///     .with_input("<Root><Text></Text><Lala></Lala><Papa></Papa></Root>")?;
/// assert!(info.starts_with("Database 'a45d766' created"));
///
/// let query = client.query("count(/Root/*)")?.without_info()?;
/// let mut result = String::new();
/// let mut response = query.execute()?;
/// response.read_to_string(&mut result);
/// assert_eq!(result, "3");
///
/// let mut query = response.close()?;
/// query.close()?;
/// # Ok(())
/// # }
/// ```
///
/// [`Client::connect`]: crate::client::Client<TcpStream>::connect
#[derive(Debug)]
pub struct Client<T>
where
    T: DatabaseStream,
{
    connection: Connection<T, Authenticated>,
}

impl Client<TcpStream> {
    /// Connects and authenticates to BaseX server using TCP stream.
    ///
    /// # Example
    ///
    /// ```
    /// # use basex::{Client, ClientError};
    /// # fn main() -> Result<(), ClientError> {
    /// let client = Client::connect("localhost", 1984, "admin", "admin")?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn connect(host: &str, port: u16, user: &str, password: &str) -> Result<Client<TcpStream>> {
        let stream = TcpStream::connect(&format!("{}:{}", host, port))?;
        let connection = Connection::new(stream).authenticate(user, password)?;

        Ok(Client::new(connection))
    }
}

impl<T> Client<T>
where
    T: DatabaseStream,
{
    /// Returns new client instance with the given connection bound to it. Works only with authenticated connections.
    ///
    /// Typically, you only need to use this method when using a custom connection. It is used heavily in tests, for
    /// example. For regular usage, refer to the [`Client::connect`] method.
    ///
    /// # Example
    ///
    /// ```
    /// # use basex::{Client, ClientError, Connection};
    /// # use std::net::TcpStream;
    /// # fn main() -> Result<(), ClientError> {
    /// let stream = TcpStream::connect("localhost:1984")?;
    /// let connection = Connection::new(stream).authenticate("admin", "admin")?;
    ///
    /// let client = Client::new(connection);
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// [`Client::connect`]: crate::client::Client<TcpStream>::connect
    pub fn new(connection: Connection<T, Authenticated>) -> Self {
        Self { connection }
    }

    /// Executes a server [`command`](https://docs.basex.org/wiki/Commands) including arguments.
    ///
    /// Returns response which can be read using the [`Read`] trait.
    ///
    /// # Example
    ///
    /// ```
    /// # use basex::{Client, Result};
    /// # use std::io::Read;
    /// # fn main() -> Result<()> {
    /// let mut client = Client::connect("localhost", 1984, "admin", "admin")?;
    /// let mut list = String::new();
    /// client.execute("LIST")?.read_to_string(&mut list)?;
    /// println!("{}", list);
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// [`Read`]: std::io::Read
    pub fn execute(mut self, command: &str) -> Result<Response<T>> {
        self.connection.send_arg(&mut command.as_bytes())?;
        Ok(Response::new(self))
    }

    /// Creates a new database with the specified `name` and, optionally, an initial `input` and opens it.
    ///
    /// * Overwrites existing database with the same `name`.
    /// * The `name` must be [valid database name](http://docs.basex.org/wiki/Commands#Valid_Names).
    /// * The `input` is a stream with valid XML.
    /// * More options can be controlled by setting [Create Options](http://docs.basex.org/wiki/Options#Create_Options)
    ///
    /// # Example
    ///
    /// ```
    /// # use basex::{Client, Result};
    /// # fn main() -> Result<()> {
    /// let mut client = Client::connect("localhost", 1984, "admin", "admin")?;
    /// client.create("boy_sminem")?.with_input("<wojak pink_index=\"69\"></wojak>")?;
    /// client.create("bogdanoff")?.without_input()?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn create(&mut self, name: &str) -> Result<CommandWithOptionalInput<T>> {
        self.connection.send_cmd(Command::Create as u8)?;
        self.connection.send_arg(&mut name.as_bytes())?;
        Ok(CommandWithOptionalInput::new(&mut self.connection))
    }

    /// Replaces resources in the currently opened database, addressed by `path`, with the XML document read from
    /// `input`, or adds new documents if no resource exists at the specified path.
    ///
    /// # Example
    ///
    /// ```
    /// # use basex::{Client, Result};
    /// # fn main() -> Result<()> {
    /// let mut client = Client::connect("localhost", 1984, "admin", "admin")?;
    /// client.create("bell")?.without_input()?;
    /// client.replace("bogdanoff", "<wojak pink_index=\"69\"></wojak>")?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn replace<'a>(&mut self, path: &str, input: impl AsResource<'a>) -> Result<String> {
        self.connection.send_cmd(Command::Replace as u8)?;
        self.connection.send_arg(&mut path.as_bytes())?;
        self.connection.send_arg(&mut input.into_read())?;
        self.connection.get_response()
    }

    /// Stores a binary file from `input` in the currently opened database under `path`. Overwrites existing resource.
    ///
    /// # Example
    ///
    /// ```
    /// # use basex::{Client, Result};
    /// # fn main() -> Result<()> {
    /// let mut client = Client::connect("localhost", 1984, "admin", "admin")?;
    /// let mut blob = [0 as u8, 1, 2, 3];
    /// client.create("asylum")?.without_input()?;
    /// client.store("bogdanoff", &mut &blob[..])?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn store<'a>(&mut self, path: &str, input: impl AsResource<'a>) -> Result<String> {
        self.connection.send_cmd(Command::Store as u8)?;
        self.connection.send_arg(&mut path.as_bytes())?;
        self.connection.send_arg(&mut input.into_read())?;
        self.connection.get_response()
    }

    /// Adds an XML resource to the currently opened database under the specified `path`.
    ///
    /// * Keeps multiple documents with the same `path`. If this is unwanted, use `Client::replace`.
    /// * On the server-side if the stream is too large to be added in one go, its data structures will be cached to
    /// disk first. Caching can be enforced by turning the `ADDCACHE` option on.
    /// * The `input` is a stream with valid XML.
    ///
    /// # Example
    ///
    /// ```
    /// # use basex::{Client, Result};
    /// # fn main() -> Result<()> {
    /// let mut client = Client::connect("localhost", 1984, "admin", "admin")?;
    /// client.create("taurus")?.without_input()?;
    /// client.add("bogdanoff", &mut "<wojak pink_index=\"69\"></wojak>".as_bytes())?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn add<'a>(&mut self, path: &str, input: impl AsResource<'a>) -> Result<String> {
        self.connection.send_cmd(Command::Add as u8)?;
        self.connection.send_arg(&mut path.as_bytes())?;
        self.connection.send_arg(&mut input.into_read())?;
        self.connection.get_response()
    }

    /// Creates a new `query` from given XQuery code.
    ///
    /// You then need to make a statement about collecting compiler info using either [`with_info`] or [`without_info`].
    ///
    /// # Example
    ///
    /// ```
    /// # use basex::{Client, Result};
    /// # use std::io::Read;
    /// # fn main() -> Result<()> {
    /// let mut client = Client::connect("localhost", 1984, "admin", "admin")?;
    ///
    /// let info = client.create("triangle")?
    ///     .with_input("<polygon><line></line><line></line><line></line></polygon>")?;
    /// assert!(info.starts_with("Database 'triangle' created"));
    ///
    /// let query = client.query("count(/polygon/*)")?.without_info()?;
    /// let mut result = String::new();
    /// let mut response = query.execute()?;
    /// response.read_to_string(&mut result)?;
    /// assert_eq!(result, "3");
    ///
    /// let mut query = response.close()?;
    /// query.close()?;
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// [`with_info`]: self::QueryWithOptionalInfo::with_info
    /// [`without_info`]: self::QueryWithOptionalInfo::without_info
    pub fn query<'a, R: AsResource<'a>>(self, query: R) -> Result<QueryWithOptionalInfo<'a, T, R>> {
        Ok(QueryWithOptionalInfo::new(self, query))
    }
}

impl<T: DatabaseStream> Clone for Client<T> {
    fn clone(&self) -> Self {
        Self {
            connection: self.connection.try_clone().unwrap(),
        }
    }
}

impl<T: DatabaseStream> Borrow<Connection<T, Authenticated>> for Client<T> {
    fn borrow(&self) -> &Connection<T, Authenticated> {
        &self.connection
    }
}

impl<T: DatabaseStream> BorrowMut<Connection<T, Authenticated>> for Client<T> {
    fn borrow_mut(&mut self) -> &mut Connection<T, Authenticated> {
        &mut self.connection
    }
}

pub struct QueryWithOptionalInfo<'a, T, R>
where
    T: DatabaseStream,
    R: AsResource<'a>,
{
    phantom: PhantomData<&'a ()>,
    client: Client<T>,
    query: R,
}

impl<'a, T, R> QueryWithOptionalInfo<'a, T, R>
where
    T: DatabaseStream,
    R: AsResource<'a>,
{
    fn new(client: Client<T>, query: R) -> Self {
        Self {
            phantom: Default::default(),
            client,
            query,
        }
    }

    pub fn with_info(self) -> Result<Query<T, WithInfo>> {
        let (mut client, _) = self.client.execute("SET QUERYINFO true")?.close()?;
        let id = Self::query(&mut client, self.query)?;
        Ok(Query::with_info(id, client))
    }

    pub fn without_info(self) -> Result<Query<T, WithoutInfo>> {
        let (mut client, _) = self.client.execute("SET QUERYINFO false")?.close()?;
        let id = Self::query(&mut client, self.query)?;
        Ok(Query::without_info(id, client))
    }

    fn query(client: &mut Client<T>, query: R) -> Result<String> {
        client.connection.send_cmd(Command::Query as u8)?;
        client.connection.send_arg(&mut query.into_read())?;
        client.connection.get_response()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tests::MockStream;
    use crate::ClientError;

    impl<T> Client<T>
    where
        T: DatabaseStream,
    {
        pub(crate) fn into_inner(self) -> Connection<T, Authenticated> {
            self.connection
        }
    }

    #[test]
    fn test_formats_as_debug() {
        format!("{:?}", Client::new(Connection::failing()));
    }

    #[test]
    fn test_clones() {
        let _ = Client::new(Connection::from_str("")).clone();
    }

    #[test]
    fn test_borrows_as_connection() {
        let _: &Connection<MockStream, Authenticated> = Client::new(Connection::from_str("test")).borrow();
    }

    #[test]
    fn test_database_is_created_with_input() {
        let mut client = Client::new(Connection::from_str("test\0"));

        let info = client
            .create("boy_sminem")
            .unwrap()
            .with_input("<wojak><pink_index>69</pink_index></wojak>")
            .unwrap();

        assert_eq!(
            client.into_inner().into_inner().to_string(),
            "\u{8}boy_sminem\u{0}<wojak><pink_index>69</pink_index></wojak>\u{0}".to_owned()
        );
        assert_eq!("test", info);
    }

    #[test]
    fn test_database_is_created_without_input() {
        let mut client = Client::new(Connection::from_str("test\0"));

        let info = client.create("boy_sminem").unwrap().without_input().unwrap();

        assert_eq!(
            client.into_inner().into_inner().to_string(),
            "\u{8}boy_sminem\u{0}\u{0}".to_owned()
        );
        assert_eq!("test", info);
    }

    #[test]
    fn test_database_fails_to_create_with_failing_stream() {
        let mut client = Client::new(Connection::failing());

        let actual_error = client.create("boy_sminem").err().expect("Operation must fail");

        assert!(matches!(actual_error, ClientError::Io(_)));
    }

    #[test]
    fn test_resource_is_replaced() {
        let mut client = Client::new(Connection::from_str("test\0"));

        let info = client
            .replace("boy_sminem", "<wojak><pink_index>69</pink_index></wojak>")
            .unwrap();

        assert_eq!(
            client.into_inner().into_inner().to_string(),
            "\u{c}boy_sminem\u{0}<wojak><pink_index>69</pink_index></wojak>\u{0}".to_owned()
        );
        assert_eq!("test", info);
    }

    #[test]
    fn test_resource_fails_to_replace_with_failing_stream() {
        let mut client = Client::new(Connection::failing());

        let actual_error = client
            .replace("boy_sminem", "<wojak><pink_index>69</pink_index></wojak>")
            .expect_err("Operation must fail");

        assert!(matches!(actual_error, ClientError::Io(_)));
    }

    #[test]
    fn test_resource_is_stored() {
        let mut client = Client::new(Connection::from_str("test\0"));

        let info = client
            .store("boy_sminem", "<wojak><pink_index>69</pink_index></wojak>")
            .unwrap();

        assert_eq!(
            client.into_inner().into_inner().to_string(),
            "\u{d}boy_sminem\u{0}<wojak><pink_index>69</pink_index></wojak>\u{0}".to_owned()
        );
        assert_eq!("test", info);
    }

    #[test]
    fn test_resource_fails_to_store_with_failing_stream() {
        let mut client = Client::new(Connection::failing());

        let actual_error = client
            .store("boy_sminem", "<wojak><pink_index>69</pink_index></wojak>")
            .expect_err("Operation must fail");

        assert!(matches!(actual_error, ClientError::Io(_)));
    }

    #[test]
    fn test_resource_is_added() {
        let mut client = Client::new(Connection::from_str("test\0"));

        let info = client
            .add("boy_sminem", "<wojak><pink_index>69</pink_index></wojak>")
            .unwrap();

        assert_eq!(
            client.into_inner().into_inner().to_string(),
            "\u{9}boy_sminem\u{0}<wojak><pink_index>69</pink_index></wojak>\u{0}".to_owned()
        );
        assert_eq!("test", info);
    }

    #[test]
    fn test_resource_fails_to_add_with_failing_stream() {
        let mut client = Client::new(Connection::failing());

        let actual_error = client
            .add("boy_sminem", "<wojak><pink_index>69</pink_index></wojak>")
            .expect_err("Operation must fail");

        assert!(matches!(actual_error, ClientError::Io(_)));
    }
}
