use std::io::{Read, Result, Write};
use std::str::FromStr;
use strum::EnumString;

pub mod grep;
/// Filter plugin module for processing input streams.
///
/// This module defines the `FilterPlugin` trait and `FilterChain` for chaining filters,
/// along with parsing utilities for filter strings. Filters can process data like head/tail,
/// grep, etc.
///
/// # Usage
///
/// Parse a filter string and apply to a reader:
///
/// ```
/// # use std::io::{Read, Write};
/// # use keep::filter_plugin::parse_filter_string;
/// let mut chain = parse_filter_string("head_lines(10)|grep(pattern=error)")?;
/// # let mut reader: &mut dyn Read = &mut std::io::empty();
/// # let mut writer: Vec<u8> = Vec::new();
/// # chain.filter(&mut reader, &mut writer)?;
/// # Ok::<(), std::io::Error>(())
/// ```
pub mod head;
pub mod skip;
pub mod strip_ansi;
pub mod tail;
#[cfg(feature = "tokens")]
pub mod tokens;
pub mod utils;

use std::collections::HashMap;

pub use grep::GrepFilter;
pub use head::{HeadBytesFilter, HeadLinesFilter};
pub use skip::{SkipBytesFilter, SkipLinesFilter};
pub use strip_ansi::StripAnsiFilter;
pub use tail::{TailBytesFilter, TailLinesFilter};

/// Represents an option for a filter plugin.
///
/// Defines a configurable parameter for filters, with name, default, and required flag.
///
/// # Fields
///
/// * `name` - Option name.
/// * `default` - Optional default value.
/// * `required` - If true, must be provided.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct FilterOption {
    pub name: String,
    #[cfg_attr(feature = "server", schema(value_type = Option<Object>))]
    pub default: Option<serde_json::Value>,
    pub required: bool,
}

/// Trait for filter plugins that process input streams.
///
/// Implement this trait to create a filter that reads from an input stream and writes filtered output.
///
/// # Required Methods
///
/// * `filter` - Process the stream.
/// * `clone_box` - For cloning dynamic instances.
/// * `options` - Describe configurable options.
///
/// # Examples
///
/// ```
/// # use std::io::{Read, Write, Result};
/// # use keep::filter_plugin::{FilterPlugin, FilterOption};
/// struct MyFilter;
/// impl FilterPlugin for MyFilter {
///     fn filter(&mut self, reader: &mut dyn Read, writer: &mut dyn Write) -> Result<()> {
///         // Implementation
///         Ok(())
///     }
///     fn clone_box(&self) -> Box<dyn FilterPlugin> {
///         Box::new(MyFilter)
///     }
///     fn options(&self) -> Vec<FilterOption> {
///         vec![]
///     }
/// }
/// ```
pub trait FilterPlugin: Send {
    /// Processes the input stream and writes the filtered output.
    ///
    /// This method reads from the input reader and applies filtering logic,
    /// writing the processed data to the output writer.
    ///
    /// # Arguments
    ///
    /// * `reader` - A mutable reference to the input reader providing the data to filter.
    /// * `writer` - A mutable reference to the output writer where the processed data is written.
    ///
    /// # Returns
    ///
    /// A `Result` indicating success (`Ok(())`) or failure with an `io::Error`.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::io::{Read, Write, Result};
    /// # use keep::filter_plugin::{FilterPlugin, FilterOption};
    /// struct MyFilter;
    /// impl FilterPlugin for MyFilter {
    ///     fn filter(&mut self, reader: &mut dyn Read, writer: &mut dyn Write) -> Result<()> {
    ///         // Read and filter data
    ///         let mut buf = [0; 1024];
    ///         loop {
    ///             let n = reader.read(&mut buf)?;
    ///             if n == 0 { break; }
    ///             // Apply filter logic to buf[0..n]
    ///             writer.write_all(&buf[0..n])?;
    ///         }
    ///         Ok(())
    ///     }
    ///     fn clone_box(&self) -> Box<dyn FilterPlugin> {
    ///         Box::new(MyFilter)
    ///     }
    ///     fn options(&self) -> Vec<FilterOption> {
    ///         vec![]
    ///     }
    /// }
    /// ```
    fn filter(&mut self, reader: &mut dyn Read, writer: &mut dyn Write) -> Result<()> {
        let _ = std::io::copy(reader, writer)?;
        Ok(())
    }

    /// Clones this plugin into a new boxed instance.
    ///
    /// This method is required for dynamic dispatch and cloning in filter chains.
    ///
    /// # Returns
    ///
    /// A new `Box<dyn FilterPlugin>` clone of the current plugin.
    ///
    /// # Examples
    ///
    /// ```
    /// # use keep::filter_plugin::FilterPlugin;
    /// fn example_clone_box(filter: &dyn FilterPlugin) -> Box<dyn FilterPlugin> {
    ///     filter.clone_box()
    /// }
    /// ```
    fn clone_box(&self) -> Box<dyn FilterPlugin>;

    /// Returns the configuration options for this plugin.
    ///
    /// Describes the configurable parameters, including names, defaults, and required flags.
    ///
    /// # Returns
    ///
    /// A vector of `FilterOption` structs describing the plugin's options.
    ///
    /// # Examples
    ///
    /// ```
    /// # use keep::filter_plugin::FilterOption;
    /// fn example_options() -> Vec<FilterOption> {
    ///     vec![
    ///         FilterOption {
    ///             name: "pattern".to_string(),
    ///             default: None,
    ///             required: true,
    ///         },
    ///     ]
    /// }
    /// ```
    fn options(&self) -> Vec<FilterOption>;

    /// Returns a human-readable description of this filter.
    ///
    /// # Returns
    ///
    /// A description string (empty by default).
    fn description(&self) -> &str {
        ""
    }
}

/// Enum representing the different types of filters.
///
/// Used for parsing and instantiating specific filter plugins.
///
/// # Variants
///
/// * `HeadBytes` - Head by bytes.
/// * `HeadLines` - Head by lines.
/// * ... etc.
#[derive(Debug, EnumString, strum::VariantNames, strum::Display)]
#[strum(serialize_all = "snake_case")]
pub enum FilterType {
    HeadBytes,
    HeadLines,
    TailBytes,
    TailLines,
    SkipBytes,
    SkipLines,
    Grep,
    StripAnsi,
    #[cfg(feature = "tokens")]
    HeadTokens,
    #[cfg(feature = "tokens")]
    SkipTokens,
    #[cfg(feature = "tokens")]
    TailTokens,
}

/// Maximum buffer size (256 MB) for filter chain intermediate results.
/// Prevents OOM on large files by rejecting inputs that exceed this limit.
const MAX_FILTER_BUFFER_SIZE: usize = 256 * 1024 * 1024;

/// A chain of filter plugins applied sequentially.
///
/// Chains multiple filters, applying them in order to the input stream.
///
/// # Fields
///
/// * `plugins` - Vector of boxed filter plugins.
pub struct FilterChain {
    plugins: Vec<Box<dyn FilterPlugin>>,
}

/// A chain of filter plugins applied sequentially.
///
/// Chains multiple filters, applying them in order to the input stream.
///
/// # Fields
///
/// * `plugins` - Vector of boxed filter plugins.
///
/// # Examples
///
/// ```
/// # use std::io::{Read, Write, Result};
/// # use keep::filter_plugin::{FilterChain, HeadLinesFilter};
/// let mut chain = FilterChain::new();
/// chain.add_plugin(Box::new(HeadLinesFilter::new(10)));
/// # let mut reader: &mut dyn Read = &mut std::io::empty();
/// # let mut writer: Vec<u8> = Vec::new();
/// # chain.filter(&mut reader, &mut writer)?;
/// # Ok::<(), std::io::Error>(())
/// ```
impl Clone for FilterChain {
    /// Clones this filter chain.
    ///
    /// # Returns
    ///
    /// A new `FilterChain` with cloned plugins.
    fn clone(&self) -> Self {
        let mut plugins = Vec::with_capacity(self.plugins.len());
        for plugin in &self.plugins {
            plugins.push(plugin.clone_box());
        }
        FilterChain { plugins }
    }
}

impl Clone for Box<dyn FilterPlugin> {
    /// Clones the boxed filter plugin.
    ///
    /// # Returns
    ///
    /// A new boxed clone of the filter plugin.
    fn clone(&self) -> Self {
        self.clone_box()
    }
}

impl Default for FilterChain {
    fn default() -> Self {
        Self::new()
    }
}

impl FilterChain {
    /// Creates a new empty filter chain.
    ///
    /// # Returns
    ///
    /// A new `FilterChain` with no plugins.
    ///
    /// # Examples
    ///
    /// ```
    /// # use keep::filter_plugin::FilterChain;
    /// let chain = FilterChain::new();
    /// // Chain starts empty
    /// ```
    pub fn new() -> Self {
        Self {
            plugins: Vec::new(),
        }
    }

    /// Adds a plugin to the chain.
    ///
    /// Plugins are applied in the order they are added.
    ///
    /// # Arguments
    ///
    /// * `plugin` - The boxed filter plugin to add to the chain.
    ///
    /// # Examples
    ///
    /// ```
    /// # use keep::filter_plugin::{FilterChain, GrepFilter};
    /// let mut chain = FilterChain::new();
    /// chain.add_plugin(Box::new(GrepFilter::new("error".to_string()).unwrap()));
    /// ```
    pub fn add_plugin(&mut self, plugin: Box<dyn FilterPlugin>) {
        self.plugins.push(plugin);
    }

    /// Applies the filter chain to the input and writes to the output.
    ///
    /// If no plugins are present, data is copied directly from reader to writer.
    /// For multiple plugins, intermediate results are buffered.
    ///
    /// # Arguments
    ///
    /// * `reader` - A mutable reference to the input reader providing the data stream.
    /// * `writer` - A mutable reference to the output writer where the fully filtered data is sent.
    ///
    /// # Returns
    ///
    /// A `Result` indicating success (`Ok(())`) or failure with an `io::Error` if any filter in the chain fails.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::io::{Read, Write, Result};
    /// # use keep::filter_plugin::{FilterChain, HeadBytesFilter};
    /// let mut chain = FilterChain::new();
    /// chain.add_plugin(Box::new(HeadBytesFilter::new(100)));
    /// # let mut input_reader: &mut dyn Read = &mut std::io::empty();
    /// # let mut output_writer: Vec<u8> = Vec::new();
    /// # chain.filter(&mut input_reader, &mut output_writer)?;
    /// # Ok::<(), std::io::Error>(())
    /// ```
    pub fn filter(&mut self, reader: &mut dyn Read, writer: &mut dyn Write) -> Result<()> {
        if self.plugins.is_empty() {
            // If no plugins, just copy the input to output
            std::io::copy(reader, writer)?;
            return Ok(());
        }

        // For multiple plugins, we need to chain them together
        // We'll use a temporary buffer to hold intermediate results
        let mut current_data = Vec::new();
        std::io::copy(reader, &mut current_data)?;

        if current_data.len() > MAX_FILTER_BUFFER_SIZE {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                format!(
                    "Input size ({} bytes) exceeds maximum filter buffer size ({} bytes). \
                     Consider using fewer filter plugins or smaller inputs.",
                    current_data.len(),
                    MAX_FILTER_BUFFER_SIZE
                ),
            ));
        }

        // Store the plugins length to avoid borrowing issues
        let plugins_len = self.plugins.len();

        for i in 0..plugins_len {
            // Create a cursor for the current data
            let mut input = std::io::Cursor::new(std::mem::take(&mut current_data));

            // For the last plugin, write directly to the output writer
            if i == plugins_len - 1 {
                self.plugins[i].filter(&mut input, writer)?;
            } else {
                // For intermediate plugins, write to a buffer
                let mut output_vec = Vec::new();
                self.plugins[i].filter(&mut input, &mut output_vec)?;

                if output_vec.len() > MAX_FILTER_BUFFER_SIZE {
                    return Err(std::io::Error::new(
                        std::io::ErrorKind::InvalidData,
                        format!(
                            "Filter output size ({} bytes) exceeds maximum filter buffer size ({} bytes).",
                            output_vec.len(),
                            MAX_FILTER_BUFFER_SIZE
                        ),
                    ));
                }

                current_data = output_vec;
            }
        }
        Ok(())
    }
}

/// Parses a filter string into a `FilterChain`.
///
/// # Arguments
///
/// * `filter_str` - The filter string specifying the chain, e.g., "head_lines(10)|grep(pattern=error)".
///
/// # Returns
///
/// A `Result` containing the parsed `FilterChain` on success, or an `io::Error` if the string is invalid.
pub fn parse_filter_string(filter_str: &str) -> Result<FilterChain> {
    let mut chain = FilterChain::new();

    for part in filter_str.split('|') {
        let part = part.trim();
        if part.is_empty() {
            continue;
        }

        // Parse the filter name and parameters
        if let Some((filter_name, params)) = part.split_once('(') {
            if let Some(params) = params.strip_suffix(')') {
                // Parse parameters
                let mut options = HashMap::new();
                let mut unnamed_params = Vec::new();

                // Split parameters by commas
                for param in params.split(',') {
                    let param = param.trim();
                    if param.is_empty() {
                        continue;
                    }

                    // Check if it's a named parameter (key=value)
                    if let Some((key, value)) = param.split_once('=') {
                        let key = key.trim();
                        let value = parse_option_value(value.trim())?;
                        options.insert(key.to_string(), value);
                    } else {
                        // Unnamed parameter
                        let value = parse_option_value(param)?;
                        unnamed_params.push(value);
                    }
                }

                // Create the appropriate filter plugin
                if let Ok(filter_type) = FilterType::from_str(filter_name) {
                    let plugin =
                        create_filter_with_options(filter_type, &unnamed_params, &options)?;
                    chain.add_plugin(plugin);
                    continue;
                }
            }
        } else {
            // Handle filters without parameters
            if let Ok(filter_type) = FilterType::from_str(part) {
                match filter_type {
                    FilterType::StripAnsi => {
                        chain.add_plugin(Box::new(strip_ansi::StripAnsiFilter::new()));
                        continue;
                    }
                    _ => {
                        return Err(std::io::Error::new(
                            std::io::ErrorKind::InvalidInput,
                            format!("Filter '{part}' requires parameters"),
                        ));
                    }
                }
            }
        }

        // If we get here, the filter wasn't recognized
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("Unknown filter: {part}"),
        ));
    }

    Ok(chain)
}

/// Creates a filter plugin with the given options.
///
/// # Arguments
///
/// * `filter_type` - The enum variant indicating the type of filter to instantiate.
/// * `unnamed_params` - A slice of unnamed JSON parameters passed to the filter.
/// * `named_options` - A hashmap of named options as key-value pairs.
///
/// # Returns
///
/// A `Result` containing a boxed `FilterPlugin` on success, or an `io::Error` if creation fails.
fn create_filter_with_options(
    filter_type: FilterType,
    unnamed_params: &[serde_json::Value],
    named_options: &HashMap<String, serde_json::Value>,
) -> Result<Box<dyn FilterPlugin>> {
    // Get the default options for this filter type by creating a temporary instance
    // To do this, we need to create a default instance of the appropriate filter
    let option_defs = match filter_type {
        FilterType::Grep => grep::GrepFilter::new("".to_string())?.options(),
        FilterType::HeadBytes => head::HeadBytesFilter::new(0).options(),
        FilterType::HeadLines => head::HeadLinesFilter::new(0).options(),
        FilterType::TailBytes => tail::TailBytesFilter::new(0).options(),
        FilterType::TailLines => tail::TailLinesFilter::new(0).options(),
        FilterType::SkipBytes => skip::SkipBytesFilter::new(0).options(),
        FilterType::SkipLines => skip::SkipLinesFilter::new(0).options(),
        FilterType::StripAnsi => strip_ansi::StripAnsiFilter::new().options(),
        #[cfg(feature = "tokens")]
        FilterType::HeadTokens => tokens::HeadTokensFilter::new(0).options(),
        #[cfg(feature = "tokens")]
        FilterType::SkipTokens => tokens::SkipTokensFilter::new(0).options(),
        #[cfg(feature = "tokens")]
        FilterType::TailTokens => tokens::TailTokensFilter::new(0).options(),
    };

    let mut options = HashMap::new();

    // Process unnamed parameters
    if unnamed_params.len() > option_defs.len() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!(
                "Too many unnamed parameters (expected at most {})",
                option_defs.len()
            ),
        ));
    }

    for (i, param) in unnamed_params.iter().enumerate() {
        if i >= option_defs.len() {
            break;
        }
        let option_name = &option_defs[i].name;
        options.insert(option_name.clone(), param.clone());
    }

    // Process named options
    for (key, value) in named_options {
        // Check if the option exists
        if !option_defs.iter().any(|opt| &opt.name == key) {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                format!("Unknown option '{key}'"),
            ));
        }
        options.insert(key.clone(), value.clone());
    }

    // Fill in defaults and check required options
    for opt_def in option_defs {
        if !options.contains_key(&opt_def.name) {
            if let Some(default) = &opt_def.default {
                options.insert(opt_def.name.clone(), default.clone());
            } else if opt_def.required {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::InvalidInput,
                    format!("Missing required option '{}'", opt_def.name),
                ));
            }
        }
    }

    // Create the specific filter with the processed options
    create_specific_filter(filter_type, &options)
}

/// Creates a specific filter instance based on type and options.
///
/// # Arguments
///
/// * `filter_type` - The enum variant indicating the type of filter to instantiate.
/// * `options` - A reference to the hashmap of processed options for the filter.
///
/// # Returns
///
/// A `Result` containing a boxed `FilterPlugin` on success, or an `io::Error` if instantiation fails.
fn create_specific_filter(
    filter_type: FilterType,
    options: &HashMap<String, serde_json::Value>,
) -> Result<Box<dyn FilterPlugin>> {
    match filter_type {
        FilterType::Grep => {
            let pattern = options
                .get("pattern")
                .and_then(|v| v.as_str())
                .ok_or_else(|| {
                    std::io::Error::new(
                        std::io::ErrorKind::InvalidInput,
                        "grep filter requires 'pattern' parameter",
                    )
                })?;
            grep::GrepFilter::new(pattern.to_string()).map(|f| Box::new(f) as Box<dyn FilterPlugin>)
        }
        FilterType::HeadBytes => {
            let count = options
                .get("count")
                .and_then(|v| v.as_u64())
                .map(|n| n as usize)
                .ok_or_else(|| {
                    std::io::Error::new(
                        std::io::ErrorKind::InvalidInput,
                        "head_bytes filter requires 'count' parameter",
                    )
                })?;
            Ok(Box::new(head::HeadBytesFilter::new(count)))
        }
        FilterType::HeadLines => {
            let count = options
                .get("count")
                .and_then(|v| v.as_u64())
                .map(|n| n as usize)
                .ok_or_else(|| {
                    std::io::Error::new(
                        std::io::ErrorKind::InvalidInput,
                        "head_lines filter requires 'count' parameter",
                    )
                })?;
            Ok(Box::new(head::HeadLinesFilter::new(count)))
        }
        FilterType::TailBytes => {
            let count = options
                .get("count")
                .and_then(|v| v.as_u64())
                .map(|n| n as usize)
                .ok_or_else(|| {
                    std::io::Error::new(
                        std::io::ErrorKind::InvalidInput,
                        "tail_bytes filter requires 'count' parameter",
                    )
                })?;
            Ok(Box::new(tail::TailBytesFilter::new(count)))
        }
        FilterType::TailLines => {
            let count = options
                .get("count")
                .and_then(|v| v.as_u64())
                .map(|n| n as usize)
                .ok_or_else(|| {
                    std::io::Error::new(
                        std::io::ErrorKind::InvalidInput,
                        "tail_lines filter requires 'count' parameter",
                    )
                })?;
            Ok(Box::new(tail::TailLinesFilter::new(count)))
        }
        FilterType::SkipBytes => {
            let count = options
                .get("count")
                .and_then(|v| v.as_u64())
                .map(|n| n as usize)
                .ok_or_else(|| {
                    std::io::Error::new(
                        std::io::ErrorKind::InvalidInput,
                        "skip_bytes filter requires 'count' parameter",
                    )
                })?;
            Ok(Box::new(skip::SkipBytesFilter::new(count)))
        }
        FilterType::SkipLines => {
            let count = options
                .get("count")
                .and_then(|v| v.as_u64())
                .map(|n| n as usize)
                .ok_or_else(|| {
                    std::io::Error::new(
                        std::io::ErrorKind::InvalidInput,
                        "skip_lines filter requires 'count' parameter",
                    )
                })?;
            Ok(Box::new(skip::SkipLinesFilter::new(count)))
        }
        FilterType::StripAnsi => {
            // StripAnsi doesn't take any parameters
            if !options.is_empty() {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::InvalidInput,
                    "strip_ansi filter doesn't take parameters",
                ));
            }
            Ok(Box::new(strip_ansi::StripAnsiFilter::new()))
        }
        #[cfg(feature = "tokens")]
        FilterType::HeadTokens => {
            let count = options
                .get("count")
                .and_then(|v| v.as_u64())
                .map(|n| n as usize)
                .ok_or_else(|| {
                    std::io::Error::new(
                        std::io::ErrorKind::InvalidInput,
                        "head_tokens filter requires 'count' parameter",
                    )
                })?;
            let encoding = options
                .get("encoding")
                .and_then(|v| v.as_str())
                .and_then(|s| s.parse::<crate::tokenizer::TokenEncoding>().ok())
                .unwrap_or_default();
            let mut f = tokens::HeadTokensFilter::new(count);
            f.tokenizer = crate::tokenizer::get_tokenizer(encoding).clone();
            f.encoding = encoding;
            Ok(Box::new(f))
        }
        #[cfg(feature = "tokens")]
        FilterType::SkipTokens => {
            let count = options
                .get("count")
                .and_then(|v| v.as_u64())
                .map(|n| n as usize)
                .ok_or_else(|| {
                    std::io::Error::new(
                        std::io::ErrorKind::InvalidInput,
                        "skip_tokens filter requires 'count' parameter",
                    )
                })?;
            let encoding = options
                .get("encoding")
                .and_then(|v| v.as_str())
                .and_then(|s| s.parse::<crate::tokenizer::TokenEncoding>().ok())
                .unwrap_or_default();
            let mut f = tokens::SkipTokensFilter::new(count);
            f.tokenizer = crate::tokenizer::get_tokenizer(encoding).clone();
            f.encoding = encoding;
            Ok(Box::new(f))
        }
        #[cfg(feature = "tokens")]
        FilterType::TailTokens => {
            let count = options
                .get("count")
                .and_then(|v| v.as_u64())
                .map(|n| n as usize)
                .ok_or_else(|| {
                    std::io::Error::new(
                        std::io::ErrorKind::InvalidInput,
                        "tail_tokens filter requires 'count' parameter",
                    )
                })?;
            let encoding = options
                .get("encoding")
                .and_then(|v| v.as_str())
                .and_then(|s| s.parse::<crate::tokenizer::TokenEncoding>().ok())
                .unwrap_or_default();
            let mut f = tokens::TailTokensFilter::new(count);
            f.tokenizer = crate::tokenizer::get_tokenizer(encoding).clone();
            f.encoding = encoding;
            Ok(Box::new(f))
        }
    }
}

/// Parses an option value from a string into a JSON value.
///
/// # Arguments
///
/// * `input` - The input string.
///
/// # Returns
///
/// A `Result` containing the parsed JSON value.
fn parse_option_value(input: &str) -> Result<serde_json::Value> {
    // Remove quotes if present
    let input = input.trim_matches(|c| c == '\'' || c == '"');

    // Try to parse as number
    if let Ok(num) = input.parse::<i64>() {
        return Ok(serde_json::Value::Number(num.into()));
    }
    if let Ok(num) = input.parse::<f64>()
        && let Some(number) = serde_json::Number::from_f64(num)
    {
        return Ok(serde_json::Value::Number(number));
    }

    // Try to parse as boolean
    if input.eq_ignore_ascii_case("true") {
        return Ok(serde_json::Value::Bool(true));
    }
    if input.eq_ignore_ascii_case("false") {
        return Ok(serde_json::Value::Bool(false));
    }

    // Treat as string
    Ok(serde_json::Value::String(input.to_string()))
}
