/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use std::collections::HashMap;
use std::io;
use std::io::Cursor;
use std::io::Read;
use std::io::Write;
use std::marker::PhantomData;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use std::path::PathBuf;
use std::slice;
use std::sync::Mutex;
use std::sync::RwLock;
use std::time::Duration;

use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use bytes::buf::UninitSlice;
use bytes::Buf;
use bytes::BufMut;
use bytes::Bytes;
use bytes::BytesMut;
use cxx::let_cxx_string;
use fbinit::FacebookInit;
use folly::StringPiece;
use once_cell::sync::OnceCell;

use crate::errors::ErrorKind;
use crate::ffi;

#[derive(Copy, Clone)]
pub enum RebalanceStrategy {
    /// Release slabs which aren't hit often
    HitsPerSlab {
        /// Relative improvement in hit ratio needed to choose an eviction.
        /// E.g. with diff_ratio of 0.1, a victim with a 0.8 hit ratio will only be removed if it
        /// can give memory to a slab with a hit ratio of at least 0.88. Increase diff_ratio to 0.2,
        /// and the slab receiving memory must have a hit ratio of at least 0.96.
        diff_ratio: f64,
        /// Minimum number of slabs to retain in each class.
        min_retained_slabs: u32,
        /// Minimum age (in seconds) of the oldest item in a class; if all items in a class are
        /// newer than this, leave it alone
        min_tail_age: Duration,
    },
    /// Release slabs to free up the least recently used items
    LruTailAge {
        /// How much older than average the oldest item needs to be to make a slab eligible for
        /// removal
        age_difference_ratio: f64,
        /// Minimum number of slabs to retain in each class.
        min_retained_slabs: u32,
    },
}

impl RebalanceStrategy {
    fn get_strategy(&self) -> Result<cxx::SharedPtr<ffi::RebalanceStrategy>> {
        match self {
            RebalanceStrategy::HitsPerSlab {
                diff_ratio,
                min_retained_slabs,
                min_tail_age,
            } => ffi::make_hits_per_slab_rebalancer(
                *diff_ratio,
                *min_retained_slabs,
                min_tail_age.as_secs() as u32,
            ),
            RebalanceStrategy::LruTailAge {
                age_difference_ratio,
                min_retained_slabs,
            } => ffi::make_lru_tail_age_rebalancer(*age_difference_ratio, *min_retained_slabs),
        }
        .map_err(Error::from)
    }
}

pub enum ShrinkMonitorType {
    /// Shrink the cache in response to the system running low on memory, and expand when there
    /// is plenty of free memory. This is meant for services running on bare metal
    FreeMemory {
        /// Once this much memory (in bytes) is free, start growing the cache back to the
        /// configured size.
        max_free_mem_gib: u32,
        /// If free memory drops this low, shrink the cache to allow the system to keep working
        min_free_mem_gib: u32,
    },
    /// Shrink the cache in response to the process becoming large, and expand when the process
    /// shrinks again. This is recommended for containers where you know the limits of the container
    ResidentSize {
        /// Once the process is this large, shrink the cache to allow the system to keep working
        max_process_size_gib: u32,
        /// When the process falls to this size, expand the cache back to the configured size
        min_process_size_gib: u32,
    },
}

pub struct ShrinkMonitor {
    /// Which shrinker type to use
    pub shrinker_type: ShrinkMonitorType,
    /// Seconds between monitor checks
    pub interval: Duration,
    /// How quickly to resize the cache, as a percentage of the difference between min_free_mem_gib
    /// and max_free_mem_gib.
    pub max_resize_per_iteration_percent: u32,
    /// How much of the total cache size can be removed to keep the cache size under control
    pub max_removed_percent: u32,
    /// What strategy to use when shrinking or growing the cache
    pub strategy: RebalanceStrategy,
}

pub struct PoolRebalanceConfig {
    /// How often to check for underutilized cache slabs (seconds)
    pub interval: Duration,
    /// What strategy to use for finding underutilized slabs
    pub strategy: RebalanceStrategy,
}

pub struct PoolResizeConfig {
    /// How often to check for slabs to remove from a pool (seconds)
    pub interval: Duration,
    /// How many slabs to move per iteration
    pub slabs_per_iteration: u32,
    /// What strategy to use for finding underutilized slabs
    pub strategy: RebalanceStrategy,
}

pub enum ShrinkerType {
    /// No shrinker - the cache is fixed size
    NoShrinker,
    /// Container-aware shrinking that aims to keep the cache within container limits.
    Container,
    /// Manually configured shrinking
    Config(ShrinkMonitor),
}

struct AccessConfig {
    bucket_power: u32,
    lock_power: u32,
}

/// LRU cache configuration options.
pub struct LruCacheConfig {
    size: usize,
    shrinker: ShrinkerType,
    cache_name: Option<String>,
    pool_rebalance: Option<PoolRebalanceConfig>,
    pool_resize: Option<PoolResizeConfig>,
    access_config: Option<AccessConfig>,
    cache_directory: Option<PathBuf>,
    base_address: Option<*mut std::ffi::c_void>,
}

impl LruCacheConfig {
    /// Configure an LRU cache that will evict elements to stay below `size` bytes
    pub fn new(size: usize) -> Self {
        Self {
            size,
            shrinker: ShrinkerType::NoShrinker,
            cache_name: None,
            pool_rebalance: None,
            pool_resize: None,
            access_config: None,
            cache_directory: None,
            base_address: None,
        }
    }

    /// Enables the container shrinker if running in a supported container runtime.
    /// So far, this only works for Facebook internal containers
    pub fn set_container_shrinker(mut self) -> Self {
        self.shrinker = ShrinkerType::Container;
        self
    }

    /// Enable automatic shrinking with the chosen `ShrinkMonitor` settings
    pub fn set_shrinker(mut self, shrinker: ShrinkMonitor) -> Self {
        self.shrinker = ShrinkerType::Config(shrinker);
        self
    }

    /// Enable pool rebalancing that evicts items early to increase overall cache utilization
    pub fn set_pool_rebalance(mut self, rebalancer: PoolRebalanceConfig) -> Self {
        self.pool_rebalance = Some(rebalancer);
        self
    }

    /// Enable pool reiszing at runtime.
    pub fn set_pool_resizer(mut self, resizer: PoolResizeConfig) -> Self {
        self.pool_resize = Some(resizer);
        self
    }

    /// Set access config parameters. Both parameters are log2 of the real value - consult cachelib
    /// C++ documentation for more details.
    /// lock_power is number of RW mutexes (small critical sections). 10 should be reasonable for
    /// millions of lookups per second (it represents 1024 locks).
    /// bucket_power is number of buckets in the hash table. You should aim to set this to
    /// log2(num elements in cache) + 1 - e.g. for 1 million items, 21 is appropriate (2,097,152
    /// buckets), while for 1 billion items, 31 is appropriate.
    /// Both values cap out at 32.
    pub fn set_access_config(mut self, bucket_power: u32, lock_power: u32) -> Self {
        self.access_config = Some(AccessConfig {
            bucket_power,
            lock_power,
        });
        self
    }

    /// Set cache name
    pub fn set_cache_name(mut self, cache_name: &str) -> Self {
        self.cache_name = Some(cache_name.to_string());
        self
    }

    /// Set cache directory.
    /// This disables volatile pools to reduce the risk of accidentally saving something
    /// whose layout is not stable
    pub fn set_cache_dir(mut self, cache_directory: impl Into<PathBuf>) -> Self {
        self.cache_directory = Some(cache_directory.into());
        self
    }

    /// Get cache directory.
    pub fn get_cache_dir(&self) -> Option<&Path> {
        self.cache_directory.as_deref()
    }

    /// Set the address at which the cache will be mounted.
    pub fn set_base_address(mut self, addr: *mut std::ffi::c_void) -> Self {
        self.base_address = Some(addr);
        self
    }

    /// Get the mounting address for the cache.
    pub fn get_base_address(&self) -> Option<*mut std::ffi::c_void> {
        self.base_address
    }
}

static GLOBAL_CACHE: OnceCell<LruCache> = OnceCell::new();

struct LruCache {
    pools: Mutex<HashMap<String, LruCachePool>>,
    admin: cxx::UniquePtr<ffi::CacheAdmin>,
    cache: cxx::UniquePtr<ffi::LruAllocator>,
    is_volatile: RwLock<bool>,
}

impl LruCache {
    fn new() -> Self {
        Self {
            cache: cxx::UniquePtr::null(),
            admin: cxx::UniquePtr::null(),
            pools: HashMap::new().into(),
            is_volatile: RwLock::new(true),
        }
    }

    fn init_cache_once(&mut self, _fb: FacebookInit, config: LruCacheConfig) -> Result<()> {
        let mut cache_config = ffi::make_lru_allocator_config()?;

        cache_config.pin_mut().setCacheSize(config.size);

        match config.shrinker {
            ShrinkerType::NoShrinker => {}
            ShrinkerType::Container => {
                if !ffi::enable_container_memory_monitor(cache_config.pin_mut())? {
                    return Err(ErrorKind::NotInContainer.into());
                }
            }
            ShrinkerType::Config(shrinker) => {
                let rebalancer = shrinker.strategy.get_strategy()?;
                match shrinker.shrinker_type {
                    ShrinkMonitorType::FreeMemory {
                        max_free_mem_gib,
                        min_free_mem_gib,
                    } => {
                        ffi::enable_free_memory_monitor(
                            cache_config.pin_mut(),
                            shrinker.interval.into(),
                            shrinker.max_resize_per_iteration_percent,
                            shrinker.max_removed_percent,
                            min_free_mem_gib,
                            max_free_mem_gib,
                            rebalancer,
                        )?;
                    }
                    ShrinkMonitorType::ResidentSize {
                        max_process_size_gib,
                        min_process_size_gib,
                    } => {
                        ffi::enable_resident_memory_monitor(
                            cache_config.pin_mut(),
                            shrinker.interval.into(),
                            shrinker.max_resize_per_iteration_percent,
                            shrinker.max_removed_percent,
                            min_process_size_gib,
                            max_process_size_gib,
                            rebalancer,
                        )?;
                    }
                }
            }
        };

        if let Some(pool_rebalance) = config.pool_rebalance {
            let rebalancer = pool_rebalance.strategy.get_strategy()?;
            ffi::enable_pool_rebalancing(
                cache_config.pin_mut(),
                rebalancer,
                pool_rebalance.interval.into(),
            )?;
        }
        if let Some(pool_resize) = config.pool_resize {
            let rebalancer = pool_resize.strategy.get_strategy()?;
            ffi::enable_pool_resizing(
                cache_config.pin_mut(),
                rebalancer,
                pool_resize.interval.into(),
                pool_resize.slabs_per_iteration,
            )?;
        }

        if let Some(AccessConfig {
            bucket_power,
            lock_power,
        }) = config.access_config
        {
            ffi::set_access_config(cache_config.pin_mut(), bucket_power, lock_power)?;
        }

        if let Some(cache_name) = config.cache_name {
            let_cxx_string!(name = cache_name);
            cache_config.pin_mut().setCacheName(&name);
        }

        if let Some(addr) = config.base_address {
            ffi::set_base_address(cache_config.pin_mut(), addr as usize)?;
        }

        if let Some(cache_directory) = config.cache_directory {
            // If cache directory is enabled, create a persistent shared-memory cache.
            let_cxx_string!(cache_directory = cache_directory.as_os_str().as_bytes());
            ffi::enable_cache_persistence(cache_config.pin_mut(), cache_directory);
            self.cache = ffi::make_shm_lru_allocator(cache_config)?;
        } else {
            let mut is_volatile = self.is_volatile.write().expect("lock poisoned");
            *is_volatile = true;
            self.cache = ffi::make_lru_allocator(cache_config)?;
        }

        if self.cache.is_null() {
            Err(ErrorKind::CacheNotInitialized.into())
        } else {
            Ok(())
        }
    }

    fn init_cacheadmin(&mut self, oncall: &str) -> Result<()> {
        let_cxx_string!(oncall = oncall);

        self.admin = ffi::make_cacheadmin(self.cache.pin_mut(), &oncall)?;

        Ok(())
    }

    fn get_allocator(&self) -> Result<&ffi::LruAllocator> {
        self.cache
            .as_ref()
            .ok_or_else(|| ErrorKind::CacheNotInitialized.into())
    }

    fn is_volatile(&self) -> bool {
        *self.is_volatile.read().expect("lock poisoned")
    }

    fn get_available_space(&self) -> Result<usize> {
        Ok(ffi::get_unreserved_size(self.get_allocator()?))
    }

    fn get_pool(&self, pool_name: &str) -> Option<LruCachePool> {
        let pools = self.pools.lock().expect("lock poisoned");

        pools.get(pool_name).cloned()
    }

    fn get_or_create_pool(&self, pool_name: &str, pool_bytes: usize) -> Result<LruCachePool> {
        let mut pools = self.pools.lock().expect("lock poisoned");

        pools.get(pool_name).cloned().ok_or(()).or_else(move |_| {
            let pool = self.create_pool(pool_name, pool_bytes)?;
            pools.insert(pool_name.to_string(), pool.clone());
            Ok(pool)
        })
    }

    fn create_pool(&self, pool_name: &str, pool_bytes: usize) -> Result<LruCachePool> {
        let name = StringPiece::from(pool_name);

        let pool = ffi::add_pool(self.get_allocator()?, name, pool_bytes)?;

        Ok(LruCachePool {
            pool,
            pool_name: pool_name.to_string(),
        })
    }
}

/// Initialise the LRU cache based on the supplied config. This should be called once and once
/// only per execution
pub fn init_cache(fb: FacebookInit, config: LruCacheConfig) -> Result<()> {
    GLOBAL_CACHE
        .get_or_try_init(|| {
            let mut cache = LruCache::new();
            cache.init_cache_once(fb, config)?;
            Ok(cache)
        })
        .map(|_| ())
}

/// Initialise the LRU cache based on the supplied config, and start CacheAdmin.
/// This should be called once and once only per execution
pub fn init_cache_with_cacheadmin(
    fb: FacebookInit,
    config: LruCacheConfig,
    oncall: &str,
) -> Result<()> {
    GLOBAL_CACHE
        .get_or_try_init(|| {
            let mut cache = LruCache::new();
            cache.init_cache_once(fb, config)?;
            cache.init_cacheadmin(oncall)?;
            Ok(cache)
        })
        .map(|_| ())
}

fn get_global_cache() -> Result<&'static LruCache> {
    GLOBAL_CACHE
        .get()
        .ok_or_else(|| ErrorKind::CacheNotInitialized.into())
}
/// Get the remaining unallocated space in the cache
pub fn get_available_space() -> Result<usize> {
    get_global_cache()?.get_available_space()
}

/// Obtain a new pool from the cache. Pools are sub-caches that have their own slice of the cache's
/// available memory, but that otherwise function as independent caches. You cannot write to a
/// cache without a pool. Note that pools are filled in slabs of 4 MiB, so the actual size you
/// receive is floor(pool_bytes / 4 MiB).
///
/// If the pool already exists, you will get the pre-existing pool instead of a new pool
///
/// Pools from this function are potentially persistent, and should not be used for items whose
/// layout might change - e.g. from abomonation
pub fn get_or_create_pool(pool_name: &str, pool_bytes: usize) -> Result<LruCachePool> {
    get_global_cache()?.get_or_create_pool(pool_name, pool_bytes)
}

/// Obtain a new volatile pool from the cache.
///
/// Volatile pools cannot be created from a persistent cache, and are therefore safe for
/// all objects.
pub fn get_or_create_volatile_pool(
    pool_name: &str,
    pool_bytes: usize,
) -> Result<VolatileLruCachePool> {
    let cache = get_global_cache()?;
    if !cache.is_volatile() {
        return Err(ErrorKind::VolatileCachePoolError.into());
    }
    let pool = cache.get_or_create_pool(pool_name, pool_bytes)?;
    Ok(VolatileLruCachePool { inner: pool })
}

/// Returns an existing cache pool by name. Returns Some(pool) if the pool exists, None if the
/// pool has not yet been created.
///
/// This is a potentially persistent pool.
pub fn get_pool(pool_name: &str) -> Option<LruCachePool> {
    get_global_cache().ok()?.get_pool(pool_name)
}

/// Obtains an existing volatile cache pool by name.
///
/// Volatile pools cannot be created from a persistent cache, and are therefore safe for
/// all objects.
pub fn get_volatile_pool(pool_name: &str) -> Result<Option<VolatileLruCachePool>> {
    let cache = get_global_cache()?;
    if !cache.is_volatile() {
        return Err(ErrorKind::VolatileCachePoolError.into());
    }
    match cache.get_pool(pool_name) {
        None => Ok(None),
        Some(cache_pool) => Ok(Some(VolatileLruCachePool { inner: cache_pool })),
    }
}

/// A handle to data stored inside the cache. Can be used to get accessor structs
pub struct LruCacheHandle<T> {
    handle: cxx::UniquePtr<ffi::LruItemHandle>,
    _marker: PhantomData<T>,
}

/// Marker for a read-only handle
pub enum ReadOnly {}
/// Marker for a read-write handle
pub enum ReadWrite {}
/// Marker for a read-write handle that can be shared between processes using this cache
pub enum ReadWriteShared {}

impl<T> LruCacheHandle<T> {
    /// Get this item as a read-only streaming buffer
    pub fn get_reader(&self) -> Result<LruCacheHandleReader<'_>> {
        Ok(LruCacheHandleReader {
            buffer: Cursor::new(self.get_value_bytes()),
        })
    }

    /// Get this item as a byte slice
    pub fn get_value_bytes(&self) -> &[u8] {
        let len = ffi::get_size(&self.handle);
        let src = ffi::get_memory(&self.handle);
        unsafe { slice::from_raw_parts(src, len) }
    }
}

fn get_cache_handle_writer<'a>(
    handle: &'a mut cxx::UniquePtr<ffi::LruItemHandle>,
) -> Result<LruCacheHandleWriter<'a>> {
    let slice: &'a mut [u8] = {
        let len = ffi::get_size(&*handle);
        let src = ffi::get_writable_memory(handle.pin_mut())?;
        unsafe { slice::from_raw_parts_mut(src, len) }
    };

    Ok(LruCacheHandleWriter {
        buffer: Cursor::new(slice),
    })
}

impl LruCacheHandle<ReadWrite> {
    /// Get this item as a writeable streaming buffer
    pub fn get_writer(&mut self) -> Result<LruCacheHandleWriter<'_>> {
        get_cache_handle_writer(&mut self.handle)
    }
}

impl LruCacheHandle<ReadWriteShared> {
    /// Get this item as a writeable streaming buffer
    pub fn get_writer(&mut self) -> Result<LruCacheHandleWriter<'_>> {
        get_cache_handle_writer(&mut self.handle)
    }

    /// Get this item as a remote handle, giving the item's offset and size within
    /// the cache memory block, usable by another process sharing the cache.
    pub fn get_remote_handle(&self) -> Result<LruCacheRemoteHandle<'_>> {
        let cache = get_global_cache()?.get_allocator()?;
        let len = ffi::get_size(&*self.handle);
        let src = ffi::get_memory(&*self.handle);
        let offset = unsafe { ffi::get_item_ptr_as_offset(cache, src)? };

        Ok(LruCacheRemoteHandle {
            offset,
            len,
            _phantom: PhantomData,
        })
    }
}

/// A read-only handle to an element in the cache. Implements `std::io::Read` and `bytes::Buf`
/// for easy access to the data within the handle
pub struct LruCacheHandleReader<'a> {
    buffer: Cursor<&'a [u8]>,
}

impl<'a> Buf for LruCacheHandleReader<'a> {
    fn remaining(&self) -> usize {
        self.buffer.remaining()
    }

    fn chunk(&self) -> &[u8] {
        Buf::chunk(&self.buffer)
    }

    fn advance(&mut self, cnt: usize) {
        self.buffer.advance(cnt)
    }
}

impl<'a> Read for LruCacheHandleReader<'a> {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        self.buffer.read(buf)
    }

    fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
        self.buffer.read_exact(buf)
    }
}

/// A writable handle to an element in the cache. Implements `std::io::{Read, Write}` and
/// `bytes::{Buf, BufMut}` for easy access to the data within the handle
pub struct LruCacheHandleWriter<'a> {
    buffer: Cursor<&'a mut [u8]>,
}

// SAFETY: Only calls to advance_mut modify the current position.
unsafe impl<'a> BufMut for LruCacheHandleWriter<'a> {
    #[inline]
    fn remaining_mut(&self) -> usize {
        let pos = self.buffer.position();
        let len = self.buffer.get_ref().len();
        len.saturating_sub(pos as usize)
    }

    #[inline]
    unsafe fn advance_mut(&mut self, cnt: usize) {
        self.buffer
            .set_position(self.buffer.position() + cnt as u64);
    }

    #[inline]
    fn chunk_mut(&mut self) -> &mut UninitSlice {
        let pos = self.buffer.position();
        let remaining = self
            .buffer
            .get_mut()
            .get_mut(pos as usize..)
            .unwrap_or(&mut []);

        unsafe { UninitSlice::from_raw_parts_mut(remaining.as_mut_ptr(), remaining.len()) }
    }
}

impl<'a> Write for LruCacheHandleWriter<'a> {
    #[inline]
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.buffer.write(buf)
    }

    #[inline]
    fn flush(&mut self) -> io::Result<()> {
        self.buffer.flush()
    }
}

/// A handle remotely access data stored inside the cache. Tied to the lifetime of the
/// LruCacheHandle it is created from.
pub struct LruCacheRemoteHandle<'a> {
    offset: usize,
    len: usize,
    _phantom: PhantomData<&'a ()>,
}

impl<'a> LruCacheRemoteHandle<'a> {
    pub fn get_offset(&self) -> usize {
        self.offset
    }

    pub fn get_length(&self) -> usize {
        self.len
    }
}

/// An LRU cache pool
///
/// There are two interfaces to the cache, depending on the complexity of your use case:
/// 1. `get`/`set`, treating the cache as a simple KV store.
/// 2. `allocate`/`insert_handle`/`get_handle`, which give you smart references to the cache's memory.
///
/// The simple `get`/`set` interface involves multiple copies of data, but protects you from making mistakes
/// The `allocate`/`insert_handle`/`get_handle` interface allows you to pin data in the cache by mistake,
/// but allows you to avoid copying into and out of the cache.
#[derive(Clone)]
pub struct LruCachePool {
    pool: i8,
    pool_name: String,
}

impl LruCachePool {
    /// Allocate memory for a key of known size; this will claim the memory until the handle is
    /// dropped or inserted into the cache.
    ///
    /// Note that if you do not insert the handle, it will not be visible to `get`, and the
    /// associated memory will be pinned until the handle is inserted or dropped. Do not hold onto
    /// handles for long time periods, as this will reduce cachelib's efficiency.
    pub fn allocate_with_ttl<K>(
        &self,
        key: K,
        size: usize,
        ttl: Option<Duration>,
    ) -> Result<Option<LruCacheHandle<ReadWriteShared>>>
    where
        K: AsRef<[u8]>,
    {
        let cache = get_global_cache()?.get_allocator()?;
        let mut full_key = self.pool_name.clone().into_bytes();
        full_key.extend_from_slice(key.as_ref());
        let key = StringPiece::from(full_key.as_slice());
        // Cachelib uses a 0 TTL for "infinite". Turn larger than 2**32 seconds
        // into 2**32, and no TTL into infinite
        // If you ask for a TTL less than 1 second, turn it into 1 second
        let ttl_secs = ttl
            .map_or(0, |d| std::cmp::min(d.as_secs(), 1))
            .try_into()
            .unwrap_or(u32::MAX);
        let size = size.try_into().context("Cache allocation too large")?;
        let handle = ffi::allocate_item(cache, self.pool, key, size, ttl_secs)?;
        if handle.is_null() {
            Ok(None)
        } else {
            Ok(Some(LruCacheHandle {
                handle,
                _marker: PhantomData,
            }))
        }
    }

    /// As allocate_with_ttl, but implicit infinite TTL
    pub fn allocate<K>(
        &self,
        key: K,
        size: usize,
    ) -> Result<Option<LruCacheHandle<ReadWriteShared>>>
    where
        K: AsRef<[u8]>,
    {
        self.allocate_with_ttl(key, size, None)
    }

    /// Insert a previously allocated handle into the cache, making it visible to `get`
    ///
    /// Returns `false` if the handle could not be inserted (e.g. another handle with the same
    /// key was inserted first)
    pub fn insert_handle(&self, mut handle: LruCacheHandle<ReadWriteShared>) -> Result<bool> {
        let cache = get_global_cache()?.get_allocator()?;

        ffi::insert_handle(cache, handle.handle.pin_mut()).map_err(Error::from)
    }

    /// Insert a previously allocated handle into the cache, replacing any pre-existing data.
    pub fn insert_or_replace_handle(
        &self,
        mut handle: LruCacheHandle<ReadWriteShared>,
    ) -> Result<()> {
        let cache = get_global_cache()?.get_allocator()?;

        ffi::insert_or_replace_handle(cache, handle.handle.pin_mut()).map_err(Error::from)
    }

    /// Insert a previously allocated handle into the cache, making it visible to
    /// `get`. Ownership of the handle is given to the caller in cases where the
    /// caller needs the memory to remain pinned. Attempting another insert on
    /// this handle will result in an error.
    pub fn insert_and_keep_handle(
        &self,
        handle: &mut LruCacheHandle<ReadWriteShared>,
    ) -> Result<bool> {
        let cache = get_global_cache()?.get_allocator()?;

        ffi::insert_handle(cache, handle.handle.pin_mut()).map_err(Error::from)
    }

    /// Allocate a new handle, and write value to it.
    pub fn allocate_populate_with_ttl<K, V>(
        &self,
        key: K,
        value: V,
        ttl: Option<Duration>,
    ) -> Result<Option<LruCacheHandle<ReadWriteShared>>>
    where
        K: AsRef<[u8]>,
        V: Buf,
    {
        let datalen = value.remaining();

        match self.allocate_with_ttl(key, datalen, ttl)? {
            None => Ok(None),
            Some(mut handle) => {
                handle.get_writer()?.put(value);
                Ok(Some(handle))
            }
        }
    }

    /// Allocate a new handle, and write value to it. TTL is implicitly infinite
    pub fn allocate_populate<K, V>(
        &self,
        key: K,
        value: V,
    ) -> Result<Option<LruCacheHandle<ReadWriteShared>>>
    where
        K: AsRef<[u8]>,
        V: Buf,
    {
        self.allocate_populate_with_ttl(key, value, None)
    }

    /// Insert a key->value mapping into the pool. Returns true if the insertion was successful,
    /// false otherwise. This will not overwrite existing data.
    pub fn set_with_ttl<K, V>(&self, key: K, value: V, ttl: Option<Duration>) -> Result<bool>
    where
        K: AsRef<[u8]>,
        V: Buf,
    {
        match self.allocate_populate_with_ttl(key, value, ttl)? {
            None => Ok(false),
            Some(handle) => self.insert_handle(handle),
        }
    }

    /// Insert a key->value mapping into the pool. Returns true if the insertion was successful,
    /// false otherwise. This will not overwrite existing data, and sets the TTL to infinite
    pub fn set<K, V>(&self, key: K, value: V) -> Result<bool>
    where
        K: AsRef<[u8]>,
        V: Buf,
    {
        self.set_with_ttl(key, value, None)
    }

    /// Insert a key->value mapping into the pool. Returns true if the insertion was successful,
    /// false otherwise. This will overwrite existing data.
    pub fn set_or_replace_with_ttl<K, V>(
        &self,
        key: K,
        value: V,
        ttl: Option<Duration>,
    ) -> Result<bool>
    where
        K: AsRef<[u8]>,
        V: Buf,
    {
        match self.allocate_populate_with_ttl(key, value, ttl)? {
            None => Ok(false),
            Some(handle) => {
                self.insert_or_replace_handle(handle)?;
                Ok(true)
            }
        }
    }

    /// Insert a key->value mapping into the pool. Returns true if the insertion was successful,
    /// false otherwise. This will overwrite existing data, and sets the TTL to infinite
    pub fn set_or_replace<K, V>(&self, key: K, value: V) -> Result<bool>
    where
        K: AsRef<[u8]>,
        V: Buf,
    {
        self.set_or_replace_with_ttl(key, value, None)
    }

    /// Fetch a read handle for a key. Returns None if the key could not be found in the pool,
    /// Some(handle) if the key was found in the pool
    ///
    /// Note that the handle will stop the key being evicted from the cache until dropped -
    /// do not hold onto the handle for longer than the minimum necessary time.
    pub fn get_handle<K>(&self, key: K) -> Result<Option<LruCacheHandle<ReadWriteShared>>>
    where
        K: AsRef<[u8]>,
    {
        let cache = get_global_cache()?.get_allocator()?;
        let mut full_key = self.pool_name.clone().into_bytes();
        full_key.extend_from_slice(key.as_ref());
        let key = StringPiece::from(full_key.as_slice());
        let handle = ffi::find_item(cache, key)?;

        if handle.is_null() {
            Ok(None)
        } else {
            Ok(Some(LruCacheHandle {
                handle,
                _marker: PhantomData,
            }))
        }
    }

    /// Fetch the value for a key. Returns None if the key could not be found in the pool,
    /// Some(value) if the key was found in the pool
    pub fn get<K>(&self, key: K) -> Result<Option<Bytes>>
    where
        K: AsRef<[u8]>,
    {
        // I have option handle, I want option bytes.
        match self.get_handle(key)? {
            None => Ok(None),
            Some(handle) => {
                let mut bytes = BytesMut::new();
                bytes.put(handle.get_reader()?);
                Ok(Some(bytes.freeze()))
            }
        }
    }

    /// Remove the value for a key. Returns true is successful
    pub fn remove<K>(&self, key: K) -> Result<()>
    where
        K: AsRef<[u8]>,
    {
        let cache = get_global_cache()?.get_allocator()?;
        let mut full_key = self.pool_name.clone().into_bytes();
        full_key.extend_from_slice(key.as_ref());
        let key = StringPiece::from(full_key.as_slice());

        ffi::remove_item(cache, key).map_err(Error::from)
    }

    /// Return the current size of this pool
    pub fn get_size(&self) -> Result<usize> {
        let cache = get_global_cache()?.get_allocator()?;

        ffi::get_pool_size(cache, self.pool).map_err(Error::from)
    }

    /// Increase the size of the pool by size, returning true if it grew, false if there is
    /// insufficent available memory to grow this pool
    pub fn grow_pool(&self, size: usize) -> Result<bool> {
        let cache = get_global_cache()?.get_allocator()?;

        ffi::grow_pool(cache, self.pool, size).map_err(Error::from)
    }

    /// Decrease the size of the pool by size, returning `true` if the pool will shrink, `false`
    /// if the pool is already smaller than size.
    ///
    /// Note that the actual shrinking is done asynchronously, based on the PoolResizeConfig
    /// supplied at the creation of the cachelib setup.
    pub fn shrink_pool(&self, size: usize) -> Result<bool> {
        let cache = get_global_cache()?.get_allocator()?;

        ffi::shrink_pool(cache, self.pool, size).map_err(Error::from)
    }

    /// Move bytes from this pool to another pool, returning true if this pool can shrink,
    /// false if you asked to move more bytes than are available
    ///
    /// Note that the actual movement of capacity is done asynchronously, based on the
    /// PoolResizeConfig supplied at the creation of the cachelib setup.
    pub fn transfer_capacity_to(&self, dest: &Self, bytes: usize) -> Result<bool> {
        let cache = get_global_cache()?.get_allocator()?;

        ffi::resize_pools(cache, self.pool, dest.pool, bytes).map_err(Error::from)
    }

    /// Get the pool name supplied at creation time
    pub fn get_pool_name(&self) -> &str {
        &self.pool_name
    }
}

/// A volatile cache pool. See `LruCachePool` for more details
#[derive(Clone)]
pub struct VolatileLruCachePool {
    inner: LruCachePool,
}

impl VolatileLruCachePool {
    /// Allocate memory for a key of known size; this will claim the memory until the handle is
    /// dropped or inserted into the cache.
    ///
    /// Note that if you do not insert the handle, it will not be visible to `get`, and the
    /// associated memory will be pinned until the handle is inserted or dropped. Do not hold onto
    /// handles for long time periods, as this will reduce cachelib's efficiency.
    pub fn allocate_with_ttl<K>(
        &self,
        key: K,
        size: usize,
        ttl: Option<Duration>,
    ) -> Result<Option<LruCacheHandle<ReadWrite>>>
    where
        K: AsRef<[u8]>,
    {
        let result = self.inner.allocate_with_ttl(key, size, ttl)?;
        Ok(result.map(|cache_handle| LruCacheHandle {
            handle: cache_handle.handle,
            _marker: PhantomData,
        }))
    }

    /// As allocate_with_ttl, but sets the TTL to infinite
    pub fn allocate<K>(&self, key: K, size: usize) -> Result<Option<LruCacheHandle<ReadWrite>>>
    where
        K: AsRef<[u8]>,
    {
        self.allocate_with_ttl(key, size, None)
    }

    /// Insert a previously allocated handle into the cache, making it visible to `get`
    ///
    /// Returns `false` if the handle could not be inserted (e.g. another handle with the same
    /// key was inserted first)
    pub fn insert_handle(&self, handle: LruCacheHandle<ReadWrite>) -> Result<bool> {
        self.inner.insert_handle(LruCacheHandle {
            handle: handle.handle,
            _marker: PhantomData,
        })
    }

    /// Insert a key->value mapping into the pool. Returns true if the insertion was successful,
    /// false otherwise. This will not overwrite existing data.
    pub fn set_with_ttl<K, V>(&self, key: K, value: V, ttl: Option<Duration>) -> Result<bool>
    where
        K: AsRef<[u8]>,
        V: Buf,
    {
        self.inner.set_with_ttl(key, value, ttl)
    }

    /// Insert a key->value mapping into the pool. Returns true if the insertion was successful,
    /// false otherwise. This will not overwrite existing data, and uses an infinite TTL
    pub fn set<K, V>(&self, key: K, value: V) -> Result<bool>
    where
        K: AsRef<[u8]>,
        V: Buf,
    {
        self.inner.set(key, value)
    }

    /// Insert a key->value mapping into the pool. Returns true if the insertion was successful,
    /// false otherwise. This will overwrite existing data.
    pub fn set_or_replace_with_ttl<K, V>(
        &self,
        key: K,
        value: V,
        ttl: Option<Duration>,
    ) -> Result<bool>
    where
        K: AsRef<[u8]>,
        V: Buf,
    {
        self.inner.set_or_replace_with_ttl(key, value, ttl)
    }

    /// Insert a key->value mapping into the pool. Returns true if the insertion was successful,
    /// false otherwise. This will overwrite existing data, and uses an infinite TTL
    pub fn set_or_replace<K, V>(&self, key: K, value: V) -> Result<bool>
    where
        K: AsRef<[u8]>,
        V: Buf,
    {
        self.inner.set_or_replace(key, value)
    }

    /// Fetch a read handle for a key. Returns None if the key could not be found in the pool,
    /// Some(handle) if the key was found in the pool
    ///
    /// Note that the handle will stop the key being evicted from the cache until dropped -
    /// do not hold onto the handle for longer than the minimum necessary time.
    pub fn get_handle<K>(&self, key: K) -> Result<Option<LruCacheHandle<ReadOnly>>>
    where
        K: AsRef<[u8]>,
    {
        let result = self.inner.get_handle(key)?;
        Ok(result.map(|cache_handle| LruCacheHandle {
            handle: cache_handle.handle,
            _marker: PhantomData,
        }))
    }

    /// Remove the value for a key. Returns true is successful
    pub fn remove<K>(&self, key: K) -> Result<()>
    where
        K: AsRef<[u8]>,
    {
        self.inner.remove(key)
    }

    /// Fetch the value for a key. Returns None if the key could not be found in the pool,
    /// Some(value) if the key was found in the pool
    pub fn get<K>(&self, key: K) -> Result<Option<Bytes>>
    where
        K: AsRef<[u8]>,
    {
        self.inner.get(key)
    }

    /// Return the current size of this pool
    pub fn get_size(&self) -> Result<usize> {
        self.inner.get_size()
    }

    /// Increase the size of the pool by size, returning true if it grew, false if there is
    /// insufficent available memory to grow this pool
    pub fn grow_pool(&self, size: usize) -> Result<bool> {
        self.inner.grow_pool(size)
    }

    /// Decrease the size of the pool by size, returning `true` if the pool will shrink, `false`
    /// if the pool is already smaller than size.
    ///
    /// Note that the actual shrinking is done asynchronously, based on the PoolResizeConfig
    /// supplied at the creation of the cachelib setup.
    pub fn shrink_pool(&self, size: usize) -> Result<bool> {
        self.inner.shrink_pool(size)
    }

    /// Move bytes from this pool to another pool, returning true if this pool can shrink,
    /// false if you asked to move more bytes than are available
    ///
    /// Note that the actual movement of capacity is done asynchronously, based on the
    /// PoolResizeConfig supplied at the creation of the cachelib setup.
    pub fn transfer_capacity_to(&self, dest: &Self, bytes: usize) -> Result<bool> {
        self.inner.transfer_capacity_to(&dest.inner, bytes)
    }
}

#[cfg(test)]
mod test {
    use tempdir::TempDir;

    use super::*;

    fn create_cache(fb: FacebookInit) {
        let config = LruCacheConfig::new(128 * 1024 * 1024)
            .set_shrinker(ShrinkMonitor {
                shrinker_type: ShrinkMonitorType::ResidentSize {
                    max_process_size_gib: 16,
                    min_process_size_gib: 1,
                },
                interval: Duration::new(1, 0),
                max_resize_per_iteration_percent: 10,
                max_removed_percent: 90,
                strategy: RebalanceStrategy::LruTailAge {
                    age_difference_ratio: 0.1,
                    min_retained_slabs: 1,
                },
            })
            .set_pool_resizer(PoolResizeConfig {
                interval: Duration::new(1, 0),
                slabs_per_iteration: 100,
                strategy: RebalanceStrategy::LruTailAge {
                    age_difference_ratio: 0.1,
                    min_retained_slabs: 1,
                },
            })
            .set_pool_rebalance(PoolRebalanceConfig {
                interval: Duration::new(1, 0),
                strategy: RebalanceStrategy::LruTailAge {
                    age_difference_ratio: 0.1,
                    min_retained_slabs: 1,
                },
            });

        if let Err(e) = init_cache(fb, config) {
            panic!("{}", e);
        }
    }

    fn create_temp_dir(dir_prefix: &str) -> TempDir {
        TempDir::new(dir_prefix).expect("failed to create temp dir")
    }

    fn create_shared_cache(fb: FacebookInit, cache_directory: PathBuf) {
        let config = LruCacheConfig::new(128 * 1024 * 1024)
            .set_shrinker(ShrinkMonitor {
                shrinker_type: ShrinkMonitorType::ResidentSize {
                    max_process_size_gib: 16,
                    min_process_size_gib: 1,
                },
                interval: Duration::new(1, 0),
                max_resize_per_iteration_percent: 10,
                max_removed_percent: 90,
                strategy: RebalanceStrategy::LruTailAge {
                    age_difference_ratio: 0.1,
                    min_retained_slabs: 1,
                },
            })
            .set_pool_resizer(PoolResizeConfig {
                interval: Duration::new(1, 0),
                slabs_per_iteration: 100,
                strategy: RebalanceStrategy::LruTailAge {
                    age_difference_ratio: 0.1,
                    min_retained_slabs: 1,
                },
            })
            .set_cache_dir(cache_directory)
            .set_pool_rebalance(PoolRebalanceConfig {
                interval: Duration::new(1, 0),
                strategy: RebalanceStrategy::LruTailAge {
                    age_difference_ratio: 0.1,
                    min_retained_slabs: 1,
                },
            });

        if let Err(e) = init_cache(fb, config) {
            panic!("{}", e);
        }
    }

    #[fbinit::test]
    fn only_create_cache(fb: FacebookInit) {
        create_cache(fb);
    }

    #[fbinit::test]
    fn only_create_shared_cache(fb: FacebookInit) {
        create_shared_cache(
            fb,
            create_temp_dir("test_create_shared_cache").path().into(),
        );
    }

    #[fbinit::test]
    fn set_item(fb: FacebookInit) {
        // Insert only, and confirm insert success
        create_cache(fb);

        let pool = get_or_create_volatile_pool("set_item", 4 * 1024 * 1024)
            .unwrap()
            .inner;

        assert!(
            pool.set(b"rimmer", Bytes::from(b"I am a fish".as_ref()))
                .unwrap(),
            "Set failed"
        );
    }

    #[fbinit::test]
    fn set_or_replace_item(fb: FacebookInit) {
        // Insert only, and confirm insert success
        create_cache(fb);

        let pool = get_or_create_volatile_pool("set_or_replace_item", 4 * 1024 * 1024)
            .unwrap()
            .inner;

        pool.set(b"foo", Bytes::from(b"bar1".as_ref())).unwrap();
        pool.set_or_replace(b"foo", Bytes::from(b"bar2".as_ref()))
            .unwrap();

        assert_eq!(
            pool.get(b"foo").unwrap(),
            Some(Bytes::from(b"bar2".as_ref())),
            "Fetch failed"
        );
    }

    #[fbinit::test]
    fn remove_item(fb: FacebookInit) {
        // Set and remove item. Confirm removal
        create_cache(fb);

        let pool = get_or_create_volatile_pool("remove_item", 4 * 1024 * 1024)
            .unwrap()
            .inner;

        pool.set(b"foo", Bytes::from(b"bar1".as_ref())).unwrap();
        pool.remove(b"foo").unwrap();

        assert_eq!(pool.get(b"foo").unwrap(), None, "Remove failed");
    }

    #[fbinit::test]
    fn get_bad_item(fb: FacebookInit) {
        // Fetch an item that doesn't exist
        create_cache(fb);

        let pool = get_or_create_volatile_pool("set_item", 4 * 1024 * 1024).unwrap();

        assert_eq!(
            pool.get(b"rimmer").unwrap(),
            None,
            "Successfully fetched a bad value"
        );
    }

    #[fbinit::test]
    fn set_and_get(fb: FacebookInit) {
        // Set an item, confirm I can get it
        // Insert only, and confirm insert success
        create_cache(fb);

        let pool = get_or_create_volatile_pool("set_item", 4 * 1024 * 1024).unwrap();

        assert!(
            pool.set(b"rimmer", Bytes::from(b"I am a fish".as_ref()))
                .unwrap(),
            "Set failed"
        );

        assert_eq!(
            pool.get(b"rimmer").unwrap(),
            Some(Bytes::from(b"I am a fish".as_ref())),
            "Fetch failed"
        );
    }

    #[fbinit::test]
    fn find_pool_by_name(fb: FacebookInit) -> Result<()> {
        create_cache(fb);

        let pool = get_or_create_volatile_pool("find_pool_by_name", 4 * 1024 * 1024)?;

        assert!(
            pool.set(b"rimmer", Bytes::from(b"I am a fish".as_ref()))
                .unwrap(),
            "Set failed"
        );

        let pool = get_volatile_pool("find_pool_by_name")?.unwrap();
        assert_eq!(
            pool.get(b"rimmer").unwrap(),
            Some(Bytes::from(b"I am a fish".as_ref())),
            "Fetch failed"
        );

        assert!(
            get_volatile_pool("There is no pool")?.is_none(),
            "non-existent pool found"
        );

        Ok(())
    }

    #[fbinit::test]
    fn pool_resizing(fb: FacebookInit) {
        create_cache(fb);
        let pool = get_or_create_volatile_pool("resize", 4 * 1024 * 1024).unwrap();
        let other = get_or_create_volatile_pool("other_pool", 12 * 1024 * 1024).unwrap();

        assert_eq!(
            pool.get_size().unwrap(),
            4 * 1024 * 1024,
            "New pool not of requested size"
        );

        assert!(
            pool.grow_pool(12 * 1024 * 1024).unwrap(),
            "Could not grow pool"
        );
        assert_eq!(
            pool.get_size().unwrap(),
            16 * 1024 * 1024,
            "Pool did not grow"
        );
        assert!(
            other.transfer_capacity_to(&pool, 8 * 1024 * 1024).unwrap(),
            "Could not move capacity"
        );
        assert_eq!(
            pool.get_size().unwrap(),
            24 * 1024 * 1024,
            "Pool stayed too small"
        );
        assert!(
            pool.shrink_pool(20 * 1024 * 1024).unwrap(),
            "Could not shrink pool"
        );
        assert_eq!(
            pool.get_size().unwrap(),
            4 * 1024 * 1024,
            "Pool did not shrink"
        );
    }

    #[fbinit::test]
    fn test_shared_cache(fb: FacebookInit) -> Result<()> {
        // All in same test to avoid race conditions when creating shared cache
        let temp_dir = create_temp_dir("test_shared_cache");
        create_shared_cache(fb, temp_dir.path().into());

        // Test set
        let pool = get_or_create_pool("find_pool_by_name", 4 * 1024 * 1024)?;
        assert!(
            pool.set(b"rimmer", Bytes::from(b"I am a fish".as_ref()))
                .unwrap(),
            "Set failed"
        );

        // Fetch an item that doesn't exist
        assert_eq!(
            pool.get(b"doesnotexist").unwrap(),
            None,
            "Successfully fetched a bad value"
        );

        // Test get
        assert_eq!(
            pool.get(b"rimmer").unwrap(),
            Some(Bytes::from(b"I am a fish".as_ref())),
            "Fetch failed"
        );

        // Test that RemoteHandle length is correct
        pool.set(b"not-rimmer", Bytes::from(b"I am a fish".as_ref()))?;
        assert_eq!(
            pool.get_handle(b"not-rimmer")?
                .unwrap()
                .get_remote_handle()?
                .get_length(),
            b"I am a fish".len(),
            "RemoteHandle handle length is incorrect"
        );

        // Test that two data offsets are not the same
        assert_ne!(
            pool.get_handle(b"rimmer")?
                .unwrap()
                .get_remote_handle()?
                .get_offset(),
            pool.get_handle(b"not-rimmer")?
                .unwrap()
                .get_remote_handle()?
                .get_offset(),
            "Two handles have same offset"
        );

        // Test getting pool by name
        let pool = get_pool("find_pool_by_name").unwrap();
        assert_eq!(
            pool.get(b"rimmer").unwrap(),
            Some(Bytes::from(b"I am a fish".as_ref())),
            "Fetch failed"
        );

        // Test getting nonexistent pool
        assert!(
            get_pool("There is no pool").is_none(),
            "non-existent pool found"
        );
        Ok(())
    }
}


// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree

#![allow(dead_code)]

use ocamlrep::FromOcamlRep;
use ocamlrep::ToOcamlRep;
use serde::Deserialize;
use serde::Serialize;

#[derive(ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(attr = "deriving show")]
pub struct RecoverableErrorWithLocation {
    pub error: String,
    pub lineno: isize,
    pub col_offset: isize,
    pub end_lineno: isize,
    pub end_col_offset: isize,
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(attr = "deriving show")]
#[repr(C, u8)]
pub enum Num {
    Int(isize),
    Float(f64),
    Complex(f64),
    /// pyast uses Py.Object.t for Big_int, but this is presumably not what we
    /// want (if it's an opaque wrapper for a CPython object). Just have the
    /// source text representing the integer digits for now instead.
    #[rust_to_ocaml(name = "Big_int")]
    BigInt(String),
}

#[rust_to_ocaml(attr = "deriving show")]
pub type Object_ = Num;

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(attr = "deriving show")]
#[repr(C, u8)]
pub enum ConstantDesc {
    Ellipsis,
    Bool(bool),
    Num(Num),
    Str(String),
    ByteStr(String),
}

#[rust_to_ocaml(attr = "deriving show")]
pub type Constant = Option<ConstantDesc>;

#[rust_to_ocaml(attr = "deriving show")]
pub type Singleton = Option<bool>;

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[repr(C)]
pub struct Withitem {
    pub context_expr: Expr,
    pub optional_vars: Option<Expr>,
}

#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(u8)]
pub enum Unaryop {
    Invert,
    Not,
    UAdd,
    USub,
}

#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(C, u8)]
pub enum TypeIgnore {
    TypeIgnore { lineno: isize, tag: String },
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(C)]
pub struct Stmt {
    pub desc: StmtDesc,
    pub lineno: isize,
    pub col_offset: isize,
    pub end_lineno: Option<isize>,
    pub end_col_offset: Option<isize>,
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(C, u8)]
pub enum StmtDesc {
    FunctionDef {
        name: String,
        args: Arguments,
        body: Vec<Stmt>,
        decorator_list: Vec<Expr>,
        returns: Option<Expr>,
        type_comment: Option<String>,
    },
    AsyncFunctionDef {
        name: String,
        args: Arguments,
        body: Vec<Stmt>,
        decorator_list: Vec<Expr>,
        returns: Option<Expr>,
        type_comment: Option<String>,
    },
    ClassDef {
        name: String,
        bases: Vec<Expr>,
        keywords: Vec<Keyword>,
        body: Vec<Stmt>,
        decorator_list: Vec<Expr>,
    },
    Return(Option<Expr>),
    Delete(Vec<Expr>),
    Assign {
        targets: Vec<Expr>,
        value: Expr,
        type_comment: Option<String>,
    },
    AugAssign {
        target: Expr,
        op: Operator,
        value: Expr,
    },
    AnnAssign {
        target: Expr,
        annotation: Expr,
        value: Option<Expr>,
        simple: isize,
    },
    For {
        target: Expr,
        iter: Expr,
        body: Vec<Stmt>,
        orelse: Vec<Stmt>,
        type_comment: Option<String>,
    },
    AsyncFor {
        target: Expr,
        iter: Expr,
        body: Vec<Stmt>,
        orelse: Vec<Stmt>,
        type_comment: Option<String>,
    },
    While {
        test: Expr,
        body: Vec<Stmt>,
        orelse: Vec<Stmt>,
    },
    If {
        test: Expr,
        body: Vec<Stmt>,
        orelse: Vec<Stmt>,
    },
    With {
        items: Vec<Withitem>,
        body: Vec<Stmt>,
        type_comment: Option<String>,
    },
    AsyncWith {
        items: Vec<Withitem>,
        body: Vec<Stmt>,
        type_comment: Option<String>,
    },
    Match {
        subject: Expr,
        cases: Vec<MatchCase>,
    },
    Raise {
        exc: Option<Expr>,
        cause: Option<Expr>,
    },
    Try {
        body: Vec<Stmt>,
        handlers: Vec<Excepthandler>,
        orelse: Vec<Stmt>,
        finalbody: Vec<Stmt>,
    },
    Assert {
        test: Expr,
        msg: Option<Expr>,
    },
    Import(Vec<Alias>),
    ImportFrom {
        module__: Option<String>,
        names: Vec<Alias>,
        level: Option<isize>,
    },
    Global(Vec<String>),
    Nonlocal(Vec<String>),
    Expr(Expr),
    Pass,
    Break,
    Continue,
}

#[rust_to_ocaml(and)]
pub type Slice = Expr;

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(C)]
pub struct Pattern {
    pub desc: Box<PatternDesc>,
    pub lineno: isize,
    pub col_offset: isize,
    pub end_lineno: isize,
    pub end_col_offset: isize,
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(C, u8)]
pub enum PatternDesc {
    MatchValue(Expr),
    MatchSingleton(Constant),
    MatchSequence(Vec<Pattern>),
    MatchMapping {
        keys: Vec<Expr>,
        patterns: Vec<Pattern>,
        rest: Option<String>,
    },
    MatchClass {
        cls: Expr,
        patterns: Vec<Pattern>,
        kwd_attrs: Vec<String>,
        kwd_patterns: Vec<Pattern>,
    },
    MatchStar(Option<String>),
    MatchAs {
        pattern: Option<Pattern>,
        name: Option<String>,
    },
    MatchOr(Vec<Pattern>),
}

#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(u8)]
pub enum Operator {
    Add,
    Sub,
    Mult,
    MatMult,
    Div,
    Mod,
    Pow,
    LShift,
    RShift,
    BitOr,
    BitXor,
    BitAnd,
    FloorDiv,
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(C, u8)]
pub enum Mod_ {
    Module {
        body: Vec<Stmt>,
        type_ignores: Vec<TypeIgnore>,
    },
    Interactive(Vec<Stmt>),
    Expression(Expr),
    FunctionType {
        argtypes: Vec<Expr>,
        returns: Expr,
    },
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(C)]
pub struct MatchCase {
    pub pattern: Pattern,
    pub guard: Option<Expr>,
    pub body: Vec<Stmt>,
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(C)]
pub struct Keyword {
    pub arg: Option<String>,
    pub value: Expr,
    pub lineno: isize,
    pub col_offset: isize,
    pub end_lineno: Option<isize>,
    pub end_col_offset: Option<isize>,
}

#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(u8)]
pub enum ExprContext {
    Load,
    Store,
    Del,
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(C)]
pub struct Expr {
    pub desc: Box<ExprDesc>,
    pub lineno: isize,
    pub col_offset: isize,
    pub end_lineno: Option<isize>,
    pub end_col_offset: Option<isize>,
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(C, u8)]
pub enum ExprDesc {
    BoolOp {
        op: Boolop,
        values: Vec<Expr>,
    },
    NamedExpr {
        target: Expr,
        value: Expr,
    },
    BinOp {
        left: Expr,
        op: Operator,
        right: Expr,
    },
    UnaryOp {
        op: Unaryop,
        operand: Expr,
    },
    Lambda {
        args: Arguments,
        body: Expr,
    },
    IfExp {
        test: Expr,
        body: Expr,
        orelse: Expr,
    },
    Dict {
        // keys is of Optional Expr type because dictionary splats `{**arg1} are
        // represented by a value and a corresponding None key
        // normally of course we expect a non None key and value pair e.g.
        // for `{'a': 12}`
        keys: Vec<Option<Expr>>,
        values: Vec<Expr>,
    },
    Set(Vec<Expr>),
    ListComp {
        elt: Expr,
        generators: Vec<Comprehension>,
    },
    SetComp {
        elt: Expr,
        generators: Vec<Comprehension>,
    },
    DictComp {
        key: Expr,
        value: Expr,
        generators: Vec<Comprehension>,
    },
    GeneratorExp {
        elt: Expr,
        generators: Vec<Comprehension>,
    },
    Await(Expr),
    Yield(Option<Expr>),
    YieldFrom(Expr),
    Compare {
        left: Expr,
        ops: Vec<Cmpop>,
        comparators: Vec<Expr>,
    },
    Call {
        func: Expr,
        args: Vec<Expr>,
        keywords: Vec<Keyword>,
    },
    FormattedValue {
        value: Expr,
        conversion: Option<isize>,
        format_spec: Option<Expr>,
    },
    JoinedStr(Vec<Expr>),
    Constant {
        value: Constant,
        kind: Option<String>,
    },
    Attribute {
        value: Expr,
        attr: String,
        ctx: ExprContext,
    },
    Subscript {
        value: Expr,
        slice: Expr,
        ctx: ExprContext,
    },
    Starred {
        value: Expr,
        ctx: ExprContext,
    },
    Name {
        id: String,
        ctx: ExprContext,
    },
    List {
        elts: Vec<Expr>,
        ctx: ExprContext,
    },
    Tuple {
        elts: Vec<Expr>,
        ctx: ExprContext,
    },
    Slice {
        lower: Option<Expr>,
        upper: Option<Expr>,
        step: Option<Expr>,
    },
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(C)]
pub struct Excepthandler {
    pub desc: ExcepthandlerDesc,
    pub lineno: isize,
    pub col_offset: isize,
    pub end_lineno: Option<isize>,
    pub end_col_offset: Option<isize>,
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(C, u8)]
pub enum ExcepthandlerDesc {
    ExceptHandler {
        type__: Option<Expr>,
        name: Option<String>,
        body: Vec<Stmt>,
    },
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(C)]
pub struct Comprehension {
    pub target: Expr,
    pub iter: Expr,
    pub ifs: Vec<Expr>,
    pub is_async: bool,
}

#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(u8)]
pub enum Cmpop {
    Eq,
    NotEq,
    Lt,
    LtE,
    Gt,
    GtE,
    Is,
    IsNot,
    In,
    NotIn,
}

#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(u8)]
pub enum Boolop {
    And,
    Or,
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(C)]
pub struct Arguments {
    pub posonlyargs: Vec<Arg>,
    pub args: Vec<Arg>,
    pub vararg: Option<Arg>,
    pub kwonlyargs: Vec<Arg>,
    pub kw_defaults: Vec<Option<Expr>>,
    pub kwarg: Option<Arg>,
    pub defaults: Vec<Expr>,
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and)]
#[repr(C)]
pub struct Arg {
    pub arg: String,
    pub annotation: Option<Expr>,
    pub type_comment: Option<String>,
    pub lineno: isize,
    pub col_offset: isize,
    pub end_lineno: Option<isize>,
    pub end_col_offset: Option<isize>,
}

#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(and, attr = "deriving show")]
#[repr(C)]
pub struct Alias {
    pub name: String,
    pub asname: Option<String>,
    pub lineno: isize,
    pub col_offset: isize,
    pub end_lineno: Option<isize>,
    pub end_col_offset: Option<isize>,
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(attr = "deriving show")]
#[repr(C, u8)]
pub enum ArgumentsArgs {
    #[rust_to_ocaml(name = "Expr_list")]
    ExprList(Vec<Expr>),
    #[rust_to_ocaml(name = "Arg_list")]
    ArgList(Vec<Arg>),
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(attr = "deriving show")]
#[repr(C, u8)]
pub enum ArgumentsKwarg {
    #[rust_to_ocaml(name = "Identifier_opt")]
    IdentifierOpt(Option<String>),
    #[rust_to_ocaml(name = "Arg_opt")]
    ArgOpt(Option<Arg>),
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(attr = "deriving show")]
#[repr(C, u8)]
pub enum ArgumentsVararg {
    #[rust_to_ocaml(name = "Identifier_opt")]
    IdentifierOpt(Option<String>),
    #[rust_to_ocaml(name = "Arg_opt")]
    ArgOpt(Option<Arg>),
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(attr = "deriving show")]
#[repr(C, u8)]
pub enum ExcepthandlerExcepthandlerName {
    #[rust_to_ocaml(name = "Identifier_opt")]
    IdentifierOpt(Option<String>),
    #[rust_to_ocaml(name = "Expr_opt")]
    ExprOpt(Option<Expr>),
}

#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(attr = "deriving show")]
#[repr(C, u8)]
pub enum ExprBytesS {
    String(String),
    Bytes(String),
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(attr = "deriving show")]
#[repr(C, u8)]
pub enum ExprSubscriptSlice {
    Slice(Slice),
    Expr(Expr),
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(attr = "deriving show")]
#[repr(C, u8)]
pub enum ExprYieldfromValue {
    #[rust_to_ocaml(name = "Expr_opt")]
    ExprOpt(Option<Expr>),
    Expr(Expr),
}

#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(attr = "deriving show")]
#[repr(C, u8)]
pub enum KeywordArg {
    #[rust_to_ocaml(name = "Identifier_opt")]
    IdentifierOpt(Option<String>),
    Identifier(String),
}

#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize, ToOcamlRep, FromOcamlRep)]
#[rust_to_ocaml(attr = "deriving show")]
#[repr(C, u8)]
pub enum StmtImportfromModule {
    #[rust_to_ocaml(name = "Identifier_opt")]
    IdentifierOpt(Option<String>),
    Identifier(String),
}