use num_bigint::BigUint;
use serde::{Deserialize, Serialize};

use crate::{impl_from_uint_for_enum, AssetAmount, MarketId, OrderId};

/// Order side
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub enum OrderSide {
    /// Ask
    /// Note: Ask means sell base asset
    Ask = 1,

    /// Bid
    /// Note: Bid means buy base asset
    Bid = 2,
}

/// Spot order detail
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct SpotOrderDetail {
    /// Order side
    pub side: OrderSide,

    /// Order price
    pub price: BigUint,

    /// Order volume
    /// Note: volume means base asset quantity
    pub volume: BigUint,
}

/// SwapV2 exact tokens
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub enum SwapV2ExactTokens {
    /// Exact input
    ExactInput {
        /// Input amount
        /// Note: input amount means the input asset quantity
        amount: AssetAmount,
        /// Min out
        /// Note: min out means the output asset quantity must be greater than or equal to the min out
        min_out: AssetAmount,
    },

    /// Exact output
    ExactOutput {
        /// Output amount
        /// Note: output amount means the output asset quantity
        amount: AssetAmount,
        /// Max in
        /// Note: max in means the input asset quantity must be less than or equal to the max in
        max_in: AssetAmount,
    },
}

/// SwapV2 order detail
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct SwapV2OrderDetail {
    /// Order side
    pub side: OrderSide,

    /// Exact tokens
    pub exact_tokens: SwapV2ExactTokens,
}

/// Order detail
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub enum OrderDetail {
    /// Spot order detail
    Spot(SpotOrderDetail),

    /// SwapV2 order detail
    SwapV2(SwapV2OrderDetail),
}

/// Submit spot order
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct SubmitOrder {
    /// Market id
    pub market_id: MarketId,

    /// Order detail
    pub order_detail: OrderDetail,
}

/// Order scope
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub enum OrderScope {
    /// Exact order id
    Exact(OrderId),

    /// Side
    Side(OrderSide),

    /// All
    /// Note: all means all orders
    All,
}

/// Cancel order
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct CancelOrder {
    /// Market id
    pub market_id: MarketId,

    /// Cancel order scope
    pub scope: OrderScope,
}

/// Add liquidity
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct AddLiquidity {
    /// Market id
    pub market_id: MarketId,

    /// Base asset quantity
    pub base_asset_quantity: AssetAmount,

    /// Quote asset quantity
    pub quote_asset_quantity: AssetAmount,
}

/// Remove liquidity
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct RemoveLiquidity {
    /// Market id
    pub market_id: MarketId,

    /// Liquidity token quantity
    pub liquidity_token_quantity: AssetAmount,
}

impl From<BigUint> for OrderSide {
    fn from(side: BigUint) -> Self {
        match side {
            side if side == BigUint::from(1u8) => OrderSide::Ask,
            side if side == BigUint::from(2u8) => OrderSide::Bid,
            _ => panic!("invalid order side"),
        }
    }
}

impl Into<BigUint> for OrderSide {
    fn into(self) -> BigUint {
        match self {
            OrderSide::Ask => BigUint::from(1u8),
            OrderSide::Bid => BigUint::from(2u8),
        }
    }
}

impl_from_uint_for_enum!(OrderSide, u8, u16, u32, u64, u128);

#[cfg(test)]
mod tests {
    use ex3_serde::{bincode, cbor};

    use super::*;

    #[test]
    fn test_submit_order_serde() {
        let submit_order = SubmitOrder {
            market_id: MarketId::from(1u8),
            order_detail: OrderDetail::Spot(SpotOrderDetail {
                side: OrderSide::Ask,
                price: BigUint::from(100u32),
                volume: BigUint::from(100u32),
            }),
        };

        // bincode
        let encoded = bincode::serialize(&submit_order).unwrap();
        let decoded: SubmitOrder = bincode::deserialize(&encoded).unwrap();
        assert_eq!(submit_order, decoded);

        // cbor
        let encoded = cbor::serialize(&submit_order).unwrap();
        let decoded: SubmitOrder = cbor::deserialize(&encoded).unwrap();
        assert_eq!(submit_order, decoded);
    }

    #[test]
    fn test_cancel_order_serde() {
        let cancel_order = CancelOrder {
            market_id: MarketId::from(1u8),
            scope: OrderScope::Exact(OrderId::from(1u8)),
        };

        // bincode
        let encoded = bincode::serialize(&cancel_order).unwrap();
        let decoded: CancelOrder = bincode::deserialize(&encoded).unwrap();
        assert_eq!(cancel_order, decoded);

        // cbor
        let encoded = cbor::serialize(&cancel_order).unwrap();
        let decoded: CancelOrder = cbor::deserialize(&encoded).unwrap();
        assert_eq!(cancel_order, decoded);
    }

    #[test]
    fn test_add_liquidity_serde() {
        let add_liquidity = AddLiquidity {
            market_id: MarketId::from(1u8),
            base_asset_quantity: AssetAmount::from(1u8),
            quote_asset_quantity: AssetAmount::from(1u8),
        };

        // bincode
        let encoded = bincode::serialize(&add_liquidity).unwrap();
        let decoded: AddLiquidity = bincode::deserialize(&encoded).unwrap();
        assert_eq!(add_liquidity, decoded);

        // cbor
        let encoded = cbor::serialize(&add_liquidity).unwrap();
        let decoded: AddLiquidity = cbor::deserialize(&encoded).unwrap();
        assert_eq!(add_liquidity, decoded);
    }

    #[test]
    fn test_remove_liquidity_serde() {
        let remove_liquidity = RemoveLiquidity {
            market_id: MarketId::from(1u8),
            liquidity_token_quantity: AssetAmount::from(1u8),
        };

        // bincode
        let encoded = bincode::serialize(&remove_liquidity).unwrap();
        let decoded: RemoveLiquidity = bincode::deserialize(&encoded).unwrap();
        assert_eq!(remove_liquidity, decoded);

        // cbor
        let encoded = cbor::serialize(&remove_liquidity).unwrap();
        let decoded: RemoveLiquidity = cbor::deserialize(&encoded).unwrap();
        assert_eq!(remove_liquidity, decoded);
    }
}
