// SPDX-FileCopyrightText: 2023 Gleb Smirnov <glebsmirnov0708@gmail.com>
// SPDX-FileCopyrightText: 2024 WeetHet <weethet66@proton.me>
// SPDX-FileCoryrightText: 2022 Adrián Bíro
// SPDX-FileCopyrightText: 2025 Samy Djemaï
//
// SPDX-License-Identifier: GPL-3.0-or-later

//! This module provides a collection of
//! ready-to-use function-based actions.

use base64::Engine;
use cpp_demangle::Symbol;
use fancy_regex::Regex;
use itertools::Itertools;
use sha1::Sha1;
use sha2::{Sha256, Sha384, Sha512};
use textpieces_function_actions::{
    anyhow::Context, DynFunctionAction, FunctionAction, FunctionCollection, Functions,
};
use xml::{reader, EmitterConfig, ParserConfig};

/// This module contains utilitary functions used in actions definition.
mod utils {
    use std::collections::HashMap;

    use digest::Digest;
    use itertools::Itertools;
    use textpieces_function_actions::anyhow::{bail, Error};

    /// Computes checksum.
    pub fn checksum<D: Digest>(input: &str) -> String {
        let mut hasher = D::new();
        hasher.update(input.as_bytes());
        hex::encode(hasher.finalize())
    }

    /// Unespaces given string.
    pub fn unescape(input: &str, role: &str) -> Result<String, Error> {
        let mut chars = input.chars();
        let mut result = String::new();

        while let Some(c) = chars.next() {
            if c == '\\' {
                match chars.next() {
                    None => bail!("Invalid escape sequence at the end of {role}"),
                    Some(c) => result.push(match c {
                        't' => '\t',
                        'r' => '\r',
                        'n' => '\n',
                        '"' => '"',
                        '\'' => '\'',
                        '\\' => '\\',
                        '0' => bail!("unescaping null character is not supported"),
                        _ => bail!(r"invalid escape sequence: \{c}"),
                    }),
                }
            } else {
                result.push(c);
            }
        }

        Ok(result)
    }

    /// Removes duplicating (or unique) lines.
    pub fn deduplicate(
        input: &str,
        remove_unique: bool,
        non_consecutive: bool,
        leave_first: bool,
    ) -> String {
        /// Line uniqueness.
        #[derive(PartialEq, Eq)]
        enum Uniqueness {
            /// Line is unique.
            Unique,
            /// Line is non-unique, but this is the first occurrence.
            First,
            /// Line is non-unique, this is a non-first occurence.
            Repeatition,
        }

        let (first_pass, second_pass) = input.split('\n').tee();
        let mut map = HashMap::new();
        let mut line_type = Vec::new();
        for (p, line) in first_pass.enumerate() {
            line_type.push(Uniqueness::Unique);
            if let Some(&pos) = map.get(line) {
                line_type[p] = Uniqueness::Repeatition;

                if line_type[pos] == Uniqueness::Unique {
                    line_type[pos] = Uniqueness::First;
                }
            }
            if !non_consecutive {
                map.clear();
            }
            map.insert(line, p);
        }

        second_pass
            .into_iter()
            .zip(line_type)
            .filter_map(|(line, type_)| {
                if remove_unique {
                    type_ == Uniqueness::First || (!leave_first && type_ == Uniqueness::Repeatition)
                } else {
                    type_ == Uniqueness::Unique || (leave_first && type_ == Uniqueness::First)
                }
                .then_some(line)
            })
            .join("\n")
    }
}

/// Re-export of function-based action provider type.
pub type Provider = crate::providers::functions::Functions<Collection>;

/// Creates an action provider for the function-based action collection.
pub fn provider() -> Provider {
    Functions::new()
}

/// Collection of function-based actions.
pub struct Collection;

impl FunctionCollection for Collection {
    const COLLECTION: &'static [DynFunctionAction] = &[
        /* Checksums */
        &FunctionAction {
            id: "sha1",
            name: "SHA1 Checksum",
            description: "Compute SHA1 hash sum",
            parameter_labels: [],
            function: |input, ()| Ok(utils::checksum::<Sha1>(input)),
        },
        &FunctionAction {
            id: "sha256",
            name: "SHA256 Checksum",
            description: "Compute SHA256 hash sum",
            parameter_labels: [],
            function: |input, ()| Ok(utils::checksum::<Sha256>(input)),
        },
        &FunctionAction {
            id: "sha384",
            name: "SHA384 Checksum",
            description: "Compute SHA384 hash sum",
            parameter_labels: [],
            function: |input, ()| Ok(utils::checksum::<Sha384>(input)),
        },
        &FunctionAction {
            id: "sha512",
            name: "SHA512 Checksum",
            description: "Compute SHA512 hash sum",
            parameter_labels: [],
            function: |input, ()| Ok(utils::checksum::<Sha512>(input)),
        },
        &FunctionAction {
            id: "md5",
            name: "MD5 Checksum",
            description: "Compute MD5 hash sum",
            parameter_labels: [],
            function: |input, ()| Ok(hex::encode(md5::compute(input).as_ref())),
        },
        /* Base64 encoding */
        &FunctionAction {
            id: "base64-encode",
            name: "Base64 Encode",
            description: "Encode text to Base64",
            parameter_labels: [],
            function: |input, ()| Ok(base64::engine::general_purpose::STANDARD.encode(input)),
        },
        &FunctionAction {
            id: "base64-decode",
            name: "Base64 Decode",
            description: "Decode text from Base64",
            parameter_labels: [],
            function: |input, ()| {
                Ok(String::from_utf8(
                    base64::engine::general_purpose::STANDARD.decode(input)?,
                )?)
            },
        },
        /* Base64URL encoding */
        &FunctionAction {
            id: "base64url-encode",
            name: "Base64URL Encode",
            description: "Encode text to url-safe Base64URL",
            parameter_labels: [],
            function: |input, ()| Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(input)),
        },
        &FunctionAction {
            id: "base64url-decode",
            name: "Base64URL Decode",
            description: "Decode text from Base64URL",
            parameter_labels: [],
            function: |input, ()| {
                Ok(String::from_utf8(
                    base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(input)?,
                )?)
            },
        },
        /* Quoted-printable */
        &FunctionAction {
            id: "quoted-printable-encode",
            name: "Quoted-Printable Encode",
            description: "Encode text with QP encoding",
            parameter_labels: [],
            function: |input, ()| Ok(quoted_printable::encode_to_str(input)),
        },
        &FunctionAction {
            id: "quoted-printable-decode",
            name: "Quoted-Printable Decode",
            description: "Decode text from QP encoding",
            parameter_labels: [],
            function: |input, ()| {
                Ok(String::from_utf8(quoted_printable::decode(
                    input,
                    quoted_printable::ParseMode::Robust,
                )?)?)
            },
        },
        /* `data:` URL */
        &FunctionAction {
            id: "data-url-decode",
            name: "Data URL Decode",
            description: "Decode text from 'data:' URL",
            parameter_labels: [],
            function: |input, ()| {
                input
                    .strip_prefix("data:")
                    .context("Data URL must start with 'data:'")?
                    .split_once(',')
                    .context("Data URL must contain `,` between prefix and data")
                    .and_then(|(prefix, data)| {
                        let is_base64 = matches!(prefix.split(';').next_back(), Some("base64"));

                        if is_base64 {
                            Ok(String::from_utf8(
                                base64::engine::general_purpose::URL_SAFE.decode(data)?,
                            )?)
                        } else {
                            Ok(urlencoding::decode(data)?.into_owned())
                        }
                    })
            },
        },
        /* Trim */
        &FunctionAction {
            id: "remove-trailing",
            name: "Remove Trailing",
            description: "Remove trailing whitespaces and newlines",
            parameter_labels: [],
            function: |input, ()| Ok(input.trim_end().split('\n').map(str::trim_end).join("\n")),
        },
        &FunctionAction {
            id: "trim-lines",
            name: "Trim Lines",
            description: "Remove leading and trailing whitespaces and newlines",
            parameter_labels: [],
            function: |input, ()| Ok(input.trim().split('\n').map(str::trim).join("\n")),
        },
        /* Join */
        &FunctionAction {
            id: "join-lines",
            name: "Join Lines",
            description: "Replace line breaks with whitespaces",
            parameter_labels: [],
            function: |input, ()| Ok(input.lines().join(" ")),
        },
        /* Count */
        &FunctionAction {
            id: "count-symbols",
            name: "Count Symbols",
            description: "Count how many symbols are in text",
            parameter_labels: [],
            function: |input, ()| Ok(input.chars().count().to_string()),
        },
        &FunctionAction {
            id: "count-lines",
            name: "Count Lines",
            description: "Count how many symbols are in text",
            parameter_labels: [],
            function: |input, ()| Ok(input.split('\n').count().to_string()),
        },
        &FunctionAction {
            id: "count-words",
            name: "Count Words",
            description: "Count how many words are in text",
            parameter_labels: [],
            function: |input, ()| {
                Ok(input
                    .split(char::is_whitespace)
                    .filter(|&s| !s.is_empty())
                    .count()
                    .to_string())
            },
        },
        /* Reverse */
        &FunctionAction {
            id: "reverse-lines",
            name: "Reverse Lines",
            description: "Reorder lines in reverse order",
            parameter_labels: [],
            function: |input, ()| Ok(input.split('\n').rev().join("\n")),
        },
        &FunctionAction {
            id: "reverse-text",
            name: "Reverse Text",
            description: "Reorder characters in reverse order",
            parameter_labels: [],
            function: |input, ()| Ok(input.chars().rev().collect()),
        },
        /* Sort */
        &FunctionAction {
            id: "sort-lines",
            name: "Sort Lines",
            description: "Sort lines in text in alphabetical order",
            parameter_labels: [],
            function: |input, ()| Ok(input.split('\n').sorted().join("\n")),
        },
        &FunctionAction {
            id: "sort-lines-reverse",
            name: "Reverse Sort Lines",
            description: "Sort lines in text in reverse alphabetical order",
            parameter_labels: [],
            function: |input, ()| Ok(input.split('\n').sorted().rev().join("\n")),
        },
        /* Filter lines */
        &FunctionAction {
            id: "filter-lines-substring",
            name: "Filter Lines by Substring",
            description: "Remove all lines which don't contain the substring",
            parameter_labels: ["Substring", "Allow Escape Sequences"],
            function: |input, (substring, unescape): (String, bool)| {
                let substring = if unescape {
                    utils::unescape(&substring, "filter substring")?
                } else {
                    substring
                };
                Ok(input
                    .split('\n')
                    .filter(|&s| s.contains(&substring))
                    .join("\n"))
            },
        },
        &FunctionAction {
            id: "filter-lines-substring-reverse",
            name: "Reverse Filter Lines by Substring",
            description: "Remove all lines which contain the substring",
            parameter_labels: ["Substring", "Allow Escape Sequences"],
            function: |input, (substring, unescape): (String, bool)| {
                let substring = if unescape {
                    utils::unescape(&substring, "filter substring")?
                } else {
                    substring
                };
                Ok(input
                    .split('\n')
                    .filter(|&s| !s.contains(&substring))
                    .join("\n"))
            },
        },
        &FunctionAction {
            id: "filter-lines-regex",
            name: "Filter Lines by Regex",
            description: "Remove all lines that do not contain matches to the regular expression",
            parameter_labels: ["Regex"],
            function: |input, (substring,): (String,)| {
                let regex = Regex::new(&substring)?;
                Ok(input
                    .split('\n')
                    .filter_map(|s| regex.is_match(s).map(|b| b.then_some(s)).transpose())
                    .collect::<Result<Vec<_>, _>>()?
                    .join("\n"))
            },
        },
        &FunctionAction {
            id: "filter-lines-regex-reverse",
            name: "Reverse Filter Lines by Regex",
            description: "Remove all lines that contain matches to the regular expression",
            parameter_labels: ["Regex"],
            function: |input, (regex,): (String,)| {
                let regex = Regex::new(&regex)?;
                Ok(input
                    .split('\n')
                    .filter_map(|s| regex.is_match(s).map(|b| (!b).then_some(s)).transpose())
                    .collect::<Result<Vec<_>, _>>()?
                    .join("\n"))
            },
        },
        /* Replacements */
        &FunctionAction {
            id: "remove-substring",
            name: "Remove Substring",
            description: "Remove all occurences of the substring from the text",
            parameter_labels: ["Substring", "Allow Escape Sequences"],
            function: |input, (substring, unescape): (String, bool)| {
                let substring = if unescape {
                    utils::unescape(&substring, "search substring")?
                } else {
                    substring
                };
                Ok(input.replace(&substring, ""))
            },
        },
        &FunctionAction {
            id: "replace-substring",
            name: "Replace Substring",
            description:
                "Replace all occurences of the substring with in the text with another string",
            parameter_labels: ["Substring", "Replace With", "Allow Escape Sequences"],
            function: |input, (substring, replacement, unescape): (String, String, bool)| {
                let (substring, replacement) = if unescape {
                    (
                        utils::unescape(&substring, "search substring")?,
                        utils::unescape(&replacement, "replacement")?,
                    )
                } else {
                    (substring, replacement)
                };

                Ok(input.replace(&substring, &replacement))
            },
        },
        &FunctionAction {
            id: "remove-regex",
            name: "Remove Regex",
            description: "Remove all matches of the regular expression from the text",
            parameter_labels: ["Regex"],
            function: |input, (regex,): (String,)| {
                let regex = Regex::new(&regex)?;
                Ok(regex.try_replacen(input, 0, "")?.into_owned())
            },
        },
        &FunctionAction {
            id: "replace-regex",
            name: "Replace Regex",
            description: "Replace all matches of the regular expression",
            parameter_labels: ["Repex", "Replace With"],
            function: |input, (regex, replacement): (String, String)| {
                let regex = Regex::new(&regex)?;
                Ok(regex.try_replacen(input, 0, replacement)?.into_owned())
            },
        },
        /* Prettify & minify */
        &FunctionAction {
            id: "prettify-json",
            name: "Prettify JSON",
            description: "Parse JSON and prettify it",
            parameter_labels: [],
            function: |input, ()| {
                let json = serde_json::from_str::<serde_json::Value>(input)?;
                Ok(serde_json::to_string_pretty(&json)?)
            },
        },
        &FunctionAction {
            id: "minify-json",
            name: "Minify JSON",
            description: "Parse JSON and minify it",
            parameter_labels: [],
            function: |input, ()| {
                let json = serde_json::from_str::<serde_json::Value>(input)?;
                Ok(serde_json::to_string(&json)?)
            },
        },
        &FunctionAction {
            id: "prettify-xml",
            name: "Prettify XML",
            description: "Parse XML and prettify it",
            parameter_labels: [],
            function: |input, ()| {
                let mut output = Vec::new();
                let reader = xml::EventReader::new_with_config(
                    input.as_bytes(),
                    ParserConfig::new()
                        .ignore_comments(false)
                        .trim_whitespace(true),
                );
                let mut writer = xml::EventWriter::new_with_config(
                    &mut output,
                    EmitterConfig {
                        perform_indent: true,
                        ..Default::default()
                    },
                );

                for event in reader {
                    if let Some(event) = event?.as_writer_event() {
                        writer.write(event)?;
                    }
                }

                Ok(String::from_utf8(output)?)
            },
        },
        /* Convert */
        &FunctionAction {
            id: "json-to-yaml",
            name: "Convert JSON to YAML",
            description: "Parse JSON and convert it to YAML format",
            parameter_labels: [],
            function: |input, ()| {
                let json = serde_json::from_str::<serde_json::Value>(input)?;
                Ok(serde_yaml::to_string(&json)?)
            },
        },
        &FunctionAction {
            id: "json-to-toml",
            name: "Convert JSON to TOML",
            description: "Parse JSON and convert it to TOML format",
            parameter_labels: [],
            function: |input, ()| {
                let json = serde_json::from_str::<serde_json::Value>(input)?;
                Ok(toml::to_string_pretty(&json)?)
            },
        },
        &FunctionAction {
            id: "yaml-to-json",
            name: "Convert YAML to JSON",
            description: "Parse YAML and convert it to JSON format",
            parameter_labels: [],
            function: |input, ()| {
                let yaml = serde_yaml::from_str::<serde_json::Value>(input)?;
                Ok(serde_json::to_string_pretty(&yaml)?)
            },
        },
        &FunctionAction {
            id: "yaml-to-toml",
            name: "Convert YAML to TOML",
            description: "Parse YAML and convert it to TOML format",
            parameter_labels: [],
            function: |input, ()| {
                let yaml = serde_yaml::from_str::<toml::Value>(input)?;
                Ok(toml::to_string_pretty(&yaml)?)
            },
        },
        &FunctionAction {
            id: "toml-to-json",
            name: "Convert TOML to JSON",
            description: "Parse TOML and convert it to JSON format",
            parameter_labels: [],
            function: |input, ()| {
                let toml = toml::from_str::<toml::Value>(input)?;
                Ok(serde_json::to_string_pretty(&toml)?)
            },
        },
        &FunctionAction {
            id: "toml-to-yaml",
            name: "Convert TOML to YAML",
            description: "Parse TOML and convert it to YAML format",
            parameter_labels: [],
            function: |input, ()| {
                let toml = toml::from_str::<toml::Value>(input)?;
                Ok(serde_yaml::to_string(&toml)?)
            },
        },
        &FunctionAction {
            id: "opml-to-rss-links",
            name: "Convert OPML to RSS link",
            description: "Extract RSS links from OPML outline",
            parameter_labels: [],
            function: |input, ()| {
                let reader = xml::EventReader::from_str(input);

                let urls: Vec<String> = reader
                    .into_iter()
                    .filter_map(|event| match event {
                        Err(err) => Some(Err(err)),
                        Ok(reader::XmlEvent::StartElement {
                            name, attributes, ..
                        }) => {
                            if name.local_name == "outline" {
                                for attr in attributes {
                                    if attr.name.local_name == "xmlUrl" {
                                        return Some(Ok(attr.value));
                                    }
                                }
                            }

                            None
                        }
                        _ => None,
                    })
                    .try_collect()?;

                Ok(urls.join("\n"))
            },
        },
        &FunctionAction {
            id: "markdown-to-html",
            name: "Markdown to HTML",
            description: "Convert Markdown to HTML",
            parameter_labels: [],
            function: |input, ()| Ok(markdown::to_html(input)),
        },
        /* Escape */
        &FunctionAction {
            id: "escape-string",
            name: "Escape String",
            description: "Replace special characters with escape sequences",
            parameter_labels: [],
            function: |input, ()| {
                Ok({
                    let mut result = String::new();

                    for c in input.chars() {
                        match c {
                            '\\' => result.push_str(r"\\"),
                            '\0' => result.push_str(r"\0"),
                            '\t' => result.push_str(r"\t"),
                            '\r' => result.push_str(r"\r"),
                            '\n' => result.push_str(r"\n"),
                            '\"' => result.push_str(r#"\""#),
                            '\'' => result.push_str(r"\'"),
                            _ => result.push(c),
                        }
                    }

                    result
                })
            },
        },
        &FunctionAction {
            id: "unescape-string",
            name: "Unescape String",
            description: "Replace escape sequences with characters",
            parameter_labels: [],
            function: |input, ()| utils::unescape(input, "text"),
        },
        &FunctionAction {
            id: "cpp-demangle",
            name: "Demangle C++ symbol",
            description: "Demangle C++ symbol name from nm",
            parameter_labels: [],
            function: |input, ()| Ok(Symbol::new(input.as_bytes())?.demangle()?),
        },
        &FunctionAction {
            id: "escape-url",
            name: "Escape URL",
            description: "Replace special characters with URL-safe sequences",
            parameter_labels: [],
            function: |input, ()| Ok(urlencoding::encode(input).into_owned()),
        },
        &FunctionAction {
            id: "unescape-url",
            name: "Unescape URL",
            description: "Replace URL sequences with characters",
            parameter_labels: [],
            function: |input, ()| Ok(urlencoding::decode(input)?.into_owned()),
        },
        &FunctionAction {
            id: "escape-html",
            name: "Escape HTML",
            description: "Replace special characters with HTML-safe sequences",
            parameter_labels: [],
            function: |input, ()| Ok(html_escape::encode_safe(input).into_owned()),
        },
        &FunctionAction {
            id: "unescape-html",
            name: "Unescape HTML",
            description: "Replace HTML sequences with characters",
            parameter_labels: [],
            function: |input, ()| Ok(html_escape::decode_html_entities(input).into_owned()),
        },
        /* Uniq */
        &FunctionAction {
            id: "remove-duplicates",
            name: "Deduplicate Lines",
            description: "Remove duplicate lines",
            parameter_labels: ["Remove Non-Consecutive", "Remove All"],
            function: |input, (non_consecutive, remove_all): (bool, bool)| {
                Ok(utils::deduplicate(
                    input,
                    false,
                    non_consecutive,
                    !remove_all,
                ))
            },
        },
        &FunctionAction {
            id: "remove-unique",
            name: "Remove Unique Lines",
            description: "Leave only duplicate lines",
            parameter_labels: ["Remove Non-Consecutive", "Deduplicate"],
            function: |input, (non_consecutive, deduplicate): (bool, bool)| {
                Ok(utils::deduplicate(
                    input,
                    true,
                    non_consecutive,
                    deduplicate,
                ))
            },
        },
        /* Casing */
        &FunctionAction {
            id: "uppercase",
            name: "Uppercase",
            description: "Convert text to uppercase",
            parameter_labels: [],
            function: |input, ()| Ok(input.to_uppercase()),
        },
        &FunctionAction {
            id: "lowercase",
            name: "Lowercase",
            description: "Convert text to lowercase",
            parameter_labels: [],
            function: |input, ()| Ok(input.to_lowercase()),
        },
        &FunctionAction {
            id: "capitalize",
            name: "Capitalize",
            description: "Make all words capitalized",
            parameter_labels: [],
            function: |input, ()| {
                input
                    .chars()
                    .zip(
                        std::iter::once(false)
                            .chain(input.chars().map(|c| c.is_lowercase() || c.is_uppercase())),
                    )
                    .flat_map(|(c, prev_cased)| {
                        if prev_cased {
                            c.to_lowercase().collect_vec()
                        } else {
                            c.to_uppercase().collect_vec()
                        }
                    })
                    .map(Ok)
                    .collect()
            },
        },
        /* For debug purpose */
        #[cfg(debug_assertions)]
        &FunctionAction {
            id: "debug-stuck",
            name: "Stuck",
            description: "Debug action that runs forever",
            parameter_labels: [],
            function: |_, ()| loop {
                std::thread::park();
            },
        },
    ];
}
