// Copyright 2019, The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

//! # Broadcast strategy
//!
//! Describes a strategy for selecting peers and active connections when sending messages.

use std::{fmt, fmt::Formatter};

use tari_comms::{peer_manager::node_id::NodeId, types::CommsPublicKey};

use crate::envelope::NodeDestination;

/// Describes a strategy for selecting peers and active connections when sending messages.
#[derive(Debug, Clone)]
pub enum BroadcastStrategy {
    /// Send to a particular peer matching the given node ID
    DirectNodeId(Box<NodeId>),
    /// Send to a particular peer matching the given Public Key
    DirectPublicKey(Box<CommsPublicKey>),
    /// Send to all connected peers. If no peers are connected, no messages are sent.
    Flood(Vec<NodeId>),
    /// Send to a random set of peers of size n that are Communication Nodes, excluding the given node IDs
    Random(usize, Vec<NodeId>),
    Broadcast(Vec<NodeId>),
    SelectedPeers(Vec<NodeId>),
    /// Propagate to a set of neighbours and random peers
    Propagate(NodeDestination, Vec<NodeId>),
}

impl fmt::Display for BroadcastStrategy {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
        #[allow(clippy::enum_glob_use)]
        use BroadcastStrategy::*;
        match self {
            DirectPublicKey(pk) => write!(f, "DirectPublicKey({pk})"),
            DirectNodeId(node_id) => write!(f, "DirectNodeId({node_id})"),
            Flood(excluded) => write!(f, "Flood({} excluded)", excluded.len()),
            Random(n, excluded) => write!(f, "Random({}, {} excluded)", n, excluded.len()),
            Broadcast(excluded) => write!(f, "Broadcast({} excluded)", excluded.len()),
            Propagate(destination, excluded) => write!(f, "Propagate({}, {} excluded)", destination, excluded.len(),),
            SelectedPeers(peers) => write!(f, "SelectedPeers({} peer(s))", peers.len()),
        }
    }
}

impl BroadcastStrategy {
    /// Returns true if this strategy will send multiple indirect messages, otherwise false
    pub fn is_multi_message(&self, _chosen_peers: &[NodeId]) -> bool {
        use BroadcastStrategy::{Broadcast, Flood, Propagate, Random};

        matches!(self, Broadcast(_) | Propagate(_, _) | Flood(_) | Random(_, _))
    }

    /// Returns true if the strategy is to send directly to the peer, otherwise false
    pub fn is_direct(&self) -> bool {
        use BroadcastStrategy::{DirectNodeId, DirectPublicKey};
        matches!(self, DirectNodeId(_) | DirectPublicKey(_))
    }

    /// Returns a reference to the NodeId used in the `DirectNodeId` strategy, otherwise None if the strategy is not
    /// `DirectNodeId`.
    pub fn direct_node_id(&self) -> Option<&NodeId> {
        use BroadcastStrategy::DirectNodeId;
        match self {
            DirectNodeId(node_id) => Some(node_id),
            _ => None,
        }
    }

    /// Returns a reference to the `CommsPublicKey` used in the `DirectPublicKey` strategy, otherwise None if the
    /// strategy is not `DirectPublicKey`.
    pub fn direct_public_key(&self) -> Option<&CommsPublicKey> {
        use BroadcastStrategy::DirectPublicKey;
        match self {
            DirectPublicKey(pk) => Some(pk),
            _ => None,
        }
    }

    /// Returns the `CommsPublicKey` used in the `DirectPublicKey` strategy, otherwise None if the strategy is not
    /// `DirectPublicKey`.
    pub fn into_direct_public_key(self) -> Option<Box<CommsPublicKey>> {
        use BroadcastStrategy::DirectPublicKey;
        match self {
            DirectPublicKey(pk) => Some(pk),
            _ => None,
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn is_direct() {
        assert!(BroadcastStrategy::DirectPublicKey(Box::default()).is_direct());
        assert!(BroadcastStrategy::DirectNodeId(Box::default()).is_direct());
        assert!(!BroadcastStrategy::Broadcast(Default::default()).is_direct());
        assert!(!BroadcastStrategy::Propagate(Default::default(), Default::default()).is_direct(),);
        assert!(!BroadcastStrategy::Flood(Default::default()).is_direct());
        assert!(!BroadcastStrategy::Random(0, vec![]).is_direct());
    }

    #[test]
    fn direct_public_key() {
        assert!(
            BroadcastStrategy::DirectPublicKey(Box::default())
                .direct_public_key()
                .is_some()
        );
        assert!(
            BroadcastStrategy::DirectNodeId(Box::default())
                .direct_public_key()
                .is_none()
        );
        assert!(
            BroadcastStrategy::Broadcast(Default::default(),)
                .direct_public_key()
                .is_none()
        );
        assert!(
            BroadcastStrategy::Flood(Default::default())
                .direct_public_key()
                .is_none()
        );
        assert!(BroadcastStrategy::Random(0, vec![]).direct_public_key().is_none());
    }

    #[test]
    fn direct_node_id() {
        assert!(
            BroadcastStrategy::DirectPublicKey(Box::default())
                .direct_node_id()
                .is_none()
        );
        assert!(
            BroadcastStrategy::DirectNodeId(Box::default())
                .direct_node_id()
                .is_some()
        );
        assert!(
            BroadcastStrategy::Broadcast(Default::default(),)
                .direct_node_id()
                .is_none()
        );
        assert!(BroadcastStrategy::Flood(Default::default()).direct_node_id().is_none());
        assert!(BroadcastStrategy::Random(0, vec![]).direct_node_id().is_none());
    }
}
