// License: see LICENSE file at root directory of `master` branch

//! # Remote procedure call (RPC)
//!
//! ## Notes
//!
//! Message are encoded in [Binn][project:binn] format. Rust projects can use [Binn-IR][crate:binn-ir] crate.
//!
//! ### Server
//!
//! The server works via [TCP], over [TLS] streams. It accepts raw binary data. However it sends [xz] compressed data to clients.
//!
//! It uses a self-signed [PKCS #12 certificate][::CERT_P12] to encrypt data.
//!
//! Limits:
//!
//! - It's _not_ designed with asynchronous I/O. This might be changed if future Rust would support asynchronous I/O natively (built-in).
//! - It's _not_ designed to defend against [Denial of service attacks][DoS].
//!
//! ### Client
//!
//! - Maximum request size: [`MAX_REQUEST_SIZE`][::MAX_REQUEST_SIZE].
//!
//! ## Specification
//!
//! Specification will be made when the project reaches stable release.
//!
//! Currently, you can:
//!
//! - Use [`make_request()`][::make_request()] to make requests (as plain bytes) and send them to server. Then use
//!   [`read_response()`][::read_response()] to read responses.
//!
//! - A response consists of:
//!
//!     + [`RESPONSE_PARAM_STATUS_CODE`][::RESPONSE_PARAM_STATUS_CODE]: always available.
//!     + [`RESPONSE_PARAM_MSG`][::RESPONSE_PARAM_MSG]: always available.
//!     + [`RESPONSE_PARAM_DATA`][::RESPONSE_PARAM_DATA]: _optional_.
//!
//! - Details of responses depend on what you request. For example, to parse unread notifications, check documentation of
//!   [`REQUEST_FOR_GETTING_UNREAD_NOTIFICATIONS`][::REQUEST_FOR_GETTING_UNREAD_NOTIFICATIONS].
//!
//! [TCP]: https://en.wikipedia.org/wiki/Transmission_Control_Protocol
//! [TLS]: https://en.wikipedia.org/wiki/Transport_Layer_Security
//! [xz]: https://en.wikipedia.org/wiki/Xz
//! [DoS]: https://en.wikipedia.org/wiki/Denial-of-service_attack
//!
//! [::CERT_P12]: ../constant.CERT_P12.html
//! [::MAX_REQUEST_SIZE]: constant.MAX_REQUEST_SIZE.html
//! [::REQUEST_FOR_GETTING_UNREAD_NOTIFICATIONS]: constant.REQUEST_FOR_GETTING_UNREAD_NOTIFICATIONS.html
//! [::RESPONSE_PARAM_STATUS_CODE]: constant.RESPONSE_PARAM_STATUS_CODE.html
//! [::RESPONSE_PARAM_MSG]: constant.RESPONSE_PARAM_MSG.html
//! [::RESPONSE_PARAM_DATA]: constant.RESPONSE_PARAM_DATA.html
//! [::make_request()]: fn.make_request.html
//! [::read_response()]: fn.read_response.html
//!
//! [project:binn]: https://github.com/liteserver/binn
//! [crate:binn-ir]: https://bitbucket.org/haibison/binn-ir

use {
    std::{
        borrow::Cow,
        io::{Error, ErrorKind, Read},
    },

    binn_ir::Value,

    crate::Result,
};

#[cfg(test)]
use std::collections::HashSet;

/// # Default server port
pub const DEFAULT_SERVER_PORT: u16 = 64064;

/// # Request method
pub type RequestMethod = u8;

/// # Request ID
pub type RequestId = u8;

/// # Request parameter
pub type RequestParam = i32;

/// # Status code
pub type StatusCode = u16;

/// # Response parameter
pub type ResponseParam = RequestParam;

/// # Data size
pub type DataSize = binn_ir::Size;

/// # Content
pub type Content = binn_ir::Map;

/// # What
pub type What = u8;

/// # Maximum request size
pub const MAX_REQUEST_SIZE: DataSize = 1024;

/// # Maximum response size
pub const MAX_RESPONSE_SIZE: DataSize = DataSize::max_value();

/// # Request method: `get`
pub const REQUEST_METHOD_GET: RequestMethod = 0;

/// # Request method: `post`
pub const REQUEST_METHOD_POST: RequestMethod = 1;

/// # Request method: `delete`
pub const REQUEST_METHOD_DELETE: RequestMethod = 2;

/// # Request parameter: method
pub const REQUEST_PARAM_METHOD: RequestParam = 0;

/// # Request parameter: user name
pub const REQUEST_PARAM_USER_NAME: RequestParam = 1;

/// # Request parameter: passphrase
pub const REQUEST_PARAM_PASSPHRASE: RequestParam = 2;

/// # Request parameter: data
pub const REQUEST_PARAM_DATA: RequestParam = 3;

/// # Request parameter: what
pub const REQUEST_PARAM_WHAT: RequestParam = 4;

/// # Request for inserting new notification
///
/// - Method: [`REQUEST_METHOD_POST`][::REQUEST_METHOD_POST].
/// - Data: [`Notification`][::Notification]. ID, date created and user ID are ignored.
/// - Response data: none.
///
/// [::REQUEST_METHOD_POST]: constant.REQUEST_METHOD_POST.html
/// [::Notification]: ../db/notifications/struct.Notification.html
pub const REQUEST_FOR_INSERTING_NEW_NOTIFICATION: What = 0;

/// # Request for getting unread notifications
///
/// - Method: [`REQUEST_METHOD_GET`][::REQUEST_METHOD_GET].
/// - Response data: a list of notifications. You can use [`Notification`][::Notification] to parse data.
///
/// [::REQUEST_METHOD_GET]: constant.REQUEST_METHOD_GET.html
/// [::Notification]: ../db/notifications/struct.Notification.html
pub const REQUEST_FOR_GETTING_UNREAD_NOTIFICATIONS: What = 1;

/// # Request for marking notifications as read
///
/// - Method: [`REQUEST_METHOD_POST`][::REQUEST_METHOD_POST].
/// - Data: list of notification IDs.
/// - Response data: none.
///
/// [::REQUEST_METHOD_POST]: constant.REQUEST_METHOD_POST.html
pub const REQUEST_FOR_MARKING_NOTIFICATIONS_AS_READ: What = 2;

/// # Request for deleting notifications via range of dates
///
/// - Method: [`REQUEST_METHOD_DELETE`][::REQUEST_METHOD_DELETE].
/// - Data: a list containing start date and end date, in Unix seconds.
/// - Response data: none.
///
/// [::REQUEST_METHOD_DELETE]: constant.REQUEST_METHOD_DELETE.html
pub const REQUEST_FOR_DELETING_NOTIFICATIONS_VIA_RANGE_OF_DATES: What = 3;

/// # Status: OK
pub const STATUS_OK: StatusCode = 0;

/// # Status: server error
pub const STATUS_SERVER_ERROR: StatusCode = 1;

/// # Status: invalid input from client
pub const STATUS_INVALID_INPUT: StatusCode = 2;

/// # Response parameter: status code
pub const RESPONSE_PARAM_STATUS_CODE: ResponseParam = 0;

/// # Response parameter: message
pub const RESPONSE_PARAM_MSG: ResponseParam = 1;

/// # Response parameter: data
pub const RESPONSE_PARAM_DATA: ResponseParam = 2;

/// # Makes request
pub fn make_request<'a>(user_name: Cow<'a, str>, passphrase: Cow<'a, str>, method: RequestMethod, what: What, data: Option<Value>)
-> Result<Vec<u8>> {
    let content = {
        let mut map = binn_ir::map();

        map.map_insert(REQUEST_PARAM_USER_NAME, user_name)?;
        map.map_insert(REQUEST_PARAM_PASSPHRASE, tiny_keccak::sha3_512(passphrase.as_bytes()).to_vec())?;
        map.map_insert(REQUEST_PARAM_METHOD, method)?;
        map.map_insert(REQUEST_PARAM_WHAT, what)?;
        if let Some(data) = data {
            map.map_insert(REQUEST_PARAM_DATA, data)?;
        }

        map
    };

    let content_size = content.size()?;
    let header = Value::from(content_size);

    let request_size = {
        let header_size = header.size()?;
        header_size.checked_add(content_size).ok_or_else(||
            Error::new(
                ErrorKind::InvalidData,
                format!("Request is too large: {header_size} + {content_size} (bytes)", header_size=header_size, content_size=content_size),
            )
        )?
    };
    if request_size > MAX_REQUEST_SIZE {
        return Err(Error::new(
            ErrorKind::InvalidData,
            format!(
                "Request is too large: {request_size} bytes (max allowed: {max_request_size})",
                request_size=request_size, max_request_size=MAX_REQUEST_SIZE,
            ),
        ));
    }

    let mut result = Vec::with_capacity(request_size as usize);
    header.encode(&mut result)?;
    content.encode(&mut result)?;

    Ok(result)
}

/// # Makes response
pub fn make_response<'a>(status: StatusCode, msg: Cow<'a, str>, data: Option<Value>) -> Result<Vec<u8>> {
    let content = {
        let mut map = binn_ir::map();

        map.map_insert(RESPONSE_PARAM_STATUS_CODE, status)?;
        map.map_insert(RESPONSE_PARAM_MSG, msg)?;
        if let Some(data) = data {
            map.map_insert(RESPONSE_PARAM_DATA, data)?;
        }

        map
    };

    let content_size = content.size()?;
    let header = Value::from(content_size);

    let response_size = {
        let header_size = header.size()?;
        header_size.checked_add(content_size).ok_or_else(||
            Error::new(
                ErrorKind::InvalidData,
                format!("Response is too large: {header_size} + {content_size} (bytes)", header_size=header_size, content_size=content_size),
            )
        )?
    };
    if response_size > MAX_RESPONSE_SIZE {
        return Err(Error::new(
            ErrorKind::InvalidData,
            format!(
                "Response is too large: {response_size} bytes (max allowed: {max_response_size})",
                response_size=response_size, max_response_size=MAX_RESPONSE_SIZE,
            ),
        ));
    }

    let mut result = Vec::with_capacity(response_size as usize);
    header.encode(&mut result)?;
    content.encode(&mut result)?;

    Ok(result)
}

/// # Reads content
fn read_content<R>(reader: &mut R, max_size: DataSize) -> Result<Content> where R: Read {
    let content_size = binn_ir::decode_u32(reader)?.ok_or_else(|| Error::new(ErrorKind::InvalidData, "Failed to read content size"))?;
    match content_size <= max_size {
        true => binn_ir::decode_map(&mut reader.take(u64::from(content_size)))?.ok_or_else(||
            Error::new(ErrorKind::InvalidData, "Failed to decode content")
        ),
        false => Err(Error::new(
            ErrorKind::InvalidData,
            format!("Content is too large: {content_size} bytes (max allowed: {max_size})", content_size=content_size, max_size=max_size),
        )),
    }
}

/// # Reads request
pub fn read_request<R>(reader: &mut R) -> Result<Content> where R: Read {
    read_content(reader, MAX_REQUEST_SIZE)
}

/// # Reads response
pub fn read_response<R>(reader: &mut R) -> Result<Content> where R: Read {
    read_content(reader, MAX_RESPONSE_SIZE)
}

#[test]
fn tests() {
    let request_methods = &[REQUEST_METHOD_GET, REQUEST_METHOD_POST, REQUEST_METHOD_DELETE];
    assert_eq!(request_methods.len(), request_methods.iter().collect::<HashSet<_>>().len());

    let request_params = &[REQUEST_PARAM_METHOD, REQUEST_PARAM_USER_NAME, REQUEST_PARAM_PASSPHRASE, REQUEST_PARAM_DATA, REQUEST_PARAM_WHAT];
    assert_eq!(request_params.len(), request_params.iter().collect::<HashSet<_>>().len());

    let status_codes = &[STATUS_OK, STATUS_INVALID_INPUT, STATUS_SERVER_ERROR];
    assert_eq!(status_codes.len(), status_codes.iter().collect::<HashSet<_>>().len());

    let response_params = &[RESPONSE_PARAM_STATUS_CODE, RESPONSE_PARAM_MSG, RESPONSE_PARAM_DATA];
    assert_eq!(response_params.len(), response_params.iter().collect::<HashSet<_>>().len());

    let whats = &[
        REQUEST_FOR_INSERTING_NEW_NOTIFICATION, REQUEST_FOR_GETTING_UNREAD_NOTIFICATIONS, REQUEST_FOR_MARKING_NOTIFICATIONS_AS_READ,
        REQUEST_FOR_DELETING_NOTIFICATIONS_VIA_RANGE_OF_DATES,
    ];
    assert_eq!(whats.len(), whats.iter().collect::<HashSet<_>>().len());
}
