use database::Database;
use endpoint::execute::{ExecuteError, execute_command};
use endpoint::message::{ReadCommandError, ResponsePartStatus, read_command,
                        write_response_part};

use crossbeam;

use smallvec::SmallVec;

use std::io;
use std::net::{TcpListener, ToSocketAddrs};

aggregate_errors!(
    ServeError {
        IoError(io::Error),
        ReadCommandError(ReadCommandError),
    }
);

/// Listen, serving each client on a new thread. In case of failure, wait until
/// all clients have finished.
pub fn listen<A>(addr: A, database: &Database) -> io::Result<()>
    where A: ToSocketAddrs {
    let listener = TcpListener::bind(addr)?;
    crossbeam::scope(|scope| -> io::Result<()> {
        loop {
            let (client, _) = listener.accept()?;
            scope.spawn(move || {
                let mut client_move = client;
                let _ = serve_loop(&mut client_move, database);
            });
        }
    })?;
    Ok(())
}

/// Serve a client continuously.
pub fn serve_loop<C>(client: &mut C, database: &Database)
                     -> Result<(), ServeError>
    where C: io::Read + io::Write {
    loop {
        serve(client, database)?;
    }
}

/// Serve a single command.
pub fn serve<C>(client: &mut C, database: &Database) -> Result<(), ServeError>
    where C: io::Read + io::Write {
    let mut command_buffer = SmallVec::<[u8; 256]>::new();
    let command = read_command(client, &mut command_buffer)?;

    let result = execute_command(command, database, |entry| {
        write_response_part(client, ResponsePartStatus::Success, entry)?;
        Ok(())
    });

    match result {
        Ok(()) => (),
        Err(ref error) => write_error(client, error)?,
    }
    write_response_part(client, ResponsePartStatus::Finish, &[])?;

    Ok(())
}

/// Write an execute error as a response part.
pub fn write_error<C>(client: &mut C, error: &ExecuteError) -> io::Result<()>
    where C: io::Write {
    let buffer = format!("{}", error);
    write_response_part(client, ResponsePartStatus::Error, buffer.as_bytes())?;
    Ok(())
}
