use super::super::*;

pub(in super::super) fn eval_left_function(
    function_name: &str,
    args: Vec<StoredValue>,
) -> Result<StoredValue> {
    if args.len() != 2 {
        return Err(Error::Other(format!(
            "DB9 function '{}' expects 2 args, got {}",
            function_name,
            args.len()
        )));
    }

    let mut iter = args.into_iter();
    let StoredValue::Text(text) = iter.next().unwrap() else {
        return Ok(StoredValue::Null);
    };
    let len = match iter.next().unwrap() {
        StoredValue::Int32(value) => i64::from(value),
        StoredValue::Int64(value) => value,
        _ => return Ok(StoredValue::Null),
    };

    if len >= 0 {
        text_prefix_by_chars(&text, usize::try_from(len).unwrap_or(usize::MAX))
    } else {
        let trim = usize::try_from(len.unsigned_abs()).unwrap_or(usize::MAX);
        let char_count = text.chars().count();
        let keep = char_count.saturating_sub(trim);
        text_prefix_by_chars(&text, keep)
    }
    .map(StoredValue::Text)
}

pub(in super::super) fn eval_right_function(
    function_name: &str,
    args: Vec<StoredValue>,
) -> Result<StoredValue> {
    if args.len() != 2 {
        return Err(Error::Other(format!(
            "DB9 function '{}' expects 2 args, got {}",
            function_name,
            args.len()
        )));
    }

    let mut iter = args.into_iter();
    let StoredValue::Text(text) = iter.next().unwrap() else {
        return Ok(StoredValue::Null);
    };
    let len = match iter.next().unwrap() {
        StoredValue::Int32(value) => i64::from(value),
        StoredValue::Int64(value) => value,
        _ => return Ok(StoredValue::Null),
    };

    let char_count = text.chars().count();
    let start = if len >= 0 {
        let len = usize::try_from(len).unwrap_or(usize::MAX);
        char_count.saturating_sub(len)
    } else {
        usize::try_from(len.unsigned_abs())
            .unwrap_or(usize::MAX)
            .min(char_count)
    };
    let start_byte = byte_index_after_chars(&text, start);
    check_output_byte_size(text.len().saturating_sub(start_byte))?;
    Ok(StoredValue::Text(text[start_byte..].to_owned()))
}

pub(in super::super) fn eval_trim_function(
    function_name: &str,
    args: Vec<StoredValue>,
    side: TrimSide,
) -> Result<StoredValue> {
    if !(1..=2).contains(&args.len()) {
        return Err(Error::Other(format!(
            "DB9 function '{}' expects 1 or 2 args, got {}",
            function_name,
            args.len()
        )));
    }

    let mut iter = args.into_iter();
    let value = iter.next().unwrap();
    let chars_to_trim = iter.next();

    let StoredValue::Text(text) = value else {
        return Ok(StoredValue::Null);
    };

    let char_set = match chars_to_trim {
        None => None,
        Some(StoredValue::Null) => return Ok(StoredValue::Null),
        Some(StoredValue::Text(chars)) => {
            Some(chars.chars().collect::<std::collections::HashSet<_>>())
        }
        Some(_) => return Ok(StoredValue::Null),
    };

    let trimmed = match (side, char_set.as_ref()) {
        (TrimSide::Both, Some(char_set)) => text.trim_matches(|c| char_set.contains(&c)),
        (TrimSide::Leading, Some(char_set)) => text.trim_start_matches(|c| char_set.contains(&c)),
        (TrimSide::Trailing, Some(char_set)) => text.trim_end_matches(|c| char_set.contains(&c)),
        (TrimSide::Both, None) => text.trim_matches(' '),
        (TrimSide::Leading, None) => text.trim_start_matches(' '),
        (TrimSide::Trailing, None) => text.trim_end_matches(' '),
    };

    check_output_byte_size(trimmed.len())?;
    Ok(StoredValue::Text(trimmed.to_owned()))
}

pub(in super::super) fn eval_substring_function(
    function_name: &str,
    args: Vec<StoredValue>,
) -> Result<StoredValue> {
    if !(2..=3).contains(&args.len()) {
        return Err(Error::Other(format!(
            "DB9 function '{}' expects 2 or 3 args, got {}",
            function_name,
            args.len()
        )));
    }

    let mut iter = args.into_iter();
    let value = iter.next().unwrap();
    let from_value = iter.next().unwrap();
    let for_value = iter.next();

    match value {
        StoredValue::Text(text) => {
            if matches!(from_value, StoredValue::Text(_)) && for_value.is_none() {
                return Err(Error::Other(format!(
                    "DB9 function '{}' regex mode is not supported yet",
                    function_name
                )));
            }

            let start = match substring_position_arg(&from_value)? {
                Some(value) => value,
                None => return Ok(StoredValue::Null),
            };
            let len = match for_value {
                Some(StoredValue::Null) => return Ok(StoredValue::Null),
                Some(StoredValue::Int32(value)) => Some(i64::from(value)),
                Some(StoredValue::Int64(value)) => Some(value),
                Some(other) => {
                    return Err(Error::Other(format!(
                        "DB9 substring length must be integer, got {}",
                        other.kind_name()
                    )));
                }
                None => None,
            };
            if matches!(len, Some(value) if value < 0) {
                return Err(Error::Other(
                    "negative substring length not allowed".to_owned(),
                ));
            }

            Ok(StoredValue::Text(substring_text_positional(
                &text, start, len,
            )?))
        }
        StoredValue::Bytes(bytes) => {
            let start = match substring_position_arg(&from_value)? {
                Some(value) => value,
                None => return Ok(StoredValue::Null),
            };
            let len = match for_value {
                Some(StoredValue::Null) => return Ok(StoredValue::Null),
                Some(StoredValue::Int32(value)) => Some(i64::from(value)),
                Some(StoredValue::Int64(value)) => Some(value),
                Some(other) => {
                    return Err(Error::Other(format!(
                        "DB9 substring length must be integer, got {}",
                        other.kind_name()
                    )));
                }
                None => None,
            };
            if matches!(len, Some(value) if value < 0) {
                return Err(Error::Other(
                    "negative substring length not allowed".to_owned(),
                ));
            }
            Ok(StoredValue::Bytes(substring_bytes_positional(
                bytes, start, len,
            )?))
        }
        _ => Ok(StoredValue::Null),
    }
}

pub(in super::super) fn substring_position_arg(value: &StoredValue) -> Result<Option<i64>> {
    match value {
        StoredValue::Null => Ok(None),
        StoredValue::Int32(value) => Ok(Some(i64::from(*value))),
        StoredValue::Int64(value) => Ok(Some(*value)),
        StoredValue::Text(_) => Err(Error::Other(
            "DB9 substring regex mode is not supported yet".to_owned(),
        )),
        other => Err(Error::Other(format!(
            "DB9 substring position must be integer, got {}",
            other.kind_name()
        ))),
    }
}

fn byte_index_after_chars(text: &str, char_count: usize) -> usize {
    if char_count == 0 {
        return 0;
    }
    text.char_indices()
        .nth(char_count)
        .map_or(text.len(), |(idx, _)| idx)
}

fn text_prefix_by_chars(text: &str, char_count: usize) -> Result<String> {
    let end = byte_index_after_chars(text, char_count);
    check_output_byte_size(end)?;
    Ok(text[..end].to_owned())
}

fn substring_text_positional(text: &str, start: i64, len: Option<i64>) -> Result<String> {
    let start_idx = if start <= 1 {
        0
    } else {
        usize::try_from(start.saturating_sub(1)).unwrap_or(usize::MAX)
    };

    let (start_idx, count) = match len {
        Some(len) if len <= 0 => return Ok(String::new()),
        Some(len) if start <= 0 => {
            let adjusted = len.saturating_add(start).saturating_sub(1);
            if adjusted <= 0 {
                return Ok(String::new());
            } else {
                (0, Some(usize::try_from(adjusted).unwrap_or(usize::MAX)))
            }
        }
        Some(len) => (start_idx, Some(usize::try_from(len).unwrap_or(usize::MAX))),
        None => (start_idx, None),
    };

    let start_byte = byte_index_after_chars(text, start_idx);
    let end_byte = count.map_or(text.len(), |count| {
        start_byte + byte_index_after_chars(&text[start_byte..], count)
    });
    check_output_byte_size(end_byte.saturating_sub(start_byte))?;
    Ok(text[start_byte..end_byte].to_owned())
}

fn substring_bytes_positional(
    mut bytes: Vec<u8>,
    start: i64,
    count: Option<i64>,
) -> Result<Vec<u8>> {
    let start_idx = if start <= 1 {
        0usize
    } else {
        usize::try_from(start - 1).unwrap_or(usize::MAX)
    };
    if start_idx >= bytes.len() {
        bytes.clear();
        return Ok(bytes);
    }

    let mut end = bytes.len();
    if let Some(count) = count {
        let count = if start <= 0 {
            count.saturating_add(start).saturating_sub(1).max(0) as usize
        } else {
            count.max(0) as usize
        };
        end = start_idx.saturating_add(count).min(bytes.len());
    }

    check_output_byte_size(end.saturating_sub(start_idx))?;
    bytes.truncate(end);
    if start_idx > 0 {
        bytes.drain(..start_idx);
    }
    Ok(bytes)
}

pub(in super::super) fn eval_strpos_function(
    function_name: &str,
    args: Vec<StoredValue>,
) -> Result<StoredValue> {
    if args.len() != 2 {
        return Err(Error::Other(format!(
            "DB9 function '{}' expects 2 args, got {}",
            function_name,
            args.len()
        )));
    }

    let mut iter = args.into_iter();
    let haystack = iter.next().unwrap();
    let needle = iter.next().unwrap();

    let StoredValue::Text(haystack) = haystack else {
        return Ok(StoredValue::Null);
    };
    let StoredValue::Text(needle) = needle else {
        return Ok(StoredValue::Null);
    };

    match haystack.find(&needle) {
        Some(pos) => Ok(StoredValue::Int32(
            (haystack[..pos].chars().count() + 1) as i32,
        )),
        None => Ok(StoredValue::Int32(0)),
    }
}

pub(in super::super) fn eval_reverse_function(
    function_name: &str,
    args: Vec<StoredValue>,
) -> Result<StoredValue> {
    eval_unary_function(function_name, args, |value| match value {
        StoredValue::Text(text) => {
            check_output_byte_size(text.len())?;
            Ok(StoredValue::Text(text.chars().rev().collect()))
        }
        _ => Ok(StoredValue::Null),
    })
}

pub(in super::super) fn eval_ascii_function(
    function_name: &str,
    args: Vec<StoredValue>,
) -> Result<StoredValue> {
    eval_unary_function(function_name, args, |value| match value {
        StoredValue::Text(text) => Ok(text
            .chars()
            .next()
            .map(|ch| StoredValue::Int32(ch as i32))
            .unwrap_or(StoredValue::Int32(0))),
        _ => Ok(StoredValue::Null),
    })
}

pub(in super::super) fn eval_chr_function(
    function_name: &str,
    args: Vec<StoredValue>,
) -> Result<StoredValue> {
    eval_unary_function(function_name, args, |value| match value {
        StoredValue::Int32(code) => chr_from_code(i64::from(code)),
        StoredValue::Int64(code) => chr_from_code(code),
        _ => Ok(StoredValue::Null),
    })
}

fn chr_from_code(code: i64) -> Result<StoredValue> {
    if code < 0 {
        return Err(Error::Other("character number must be positive".to_owned()));
    }
    if code == 0 {
        return Err(Error::Other("null character not permitted".to_owned()));
    }

    let codepoint = u32::try_from(code).map_err(|_| {
        Error::Other(format!(
            "requested character too large for encoding: {code}"
        ))
    })?;
    if codepoint > 0x10FFFF {
        return Err(Error::Other(format!(
            "requested character too large for encoding: {code}"
        )));
    }
    let ch = char::from_u32(codepoint).ok_or_else(|| {
        Error::Other(format!(
            "requested character not valid for encoding: {code}"
        ))
    })?;
    Ok(StoredValue::Text(ch.to_string()))
}

pub(in super::super) fn eval_split_part_function(
    function_name: &str,
    args: Vec<StoredValue>,
) -> Result<StoredValue> {
    if args.len() != 3 {
        return Err(Error::Other(format!(
            "DB9 function '{}' expects 3 args, got {}",
            function_name,
            args.len()
        )));
    }

    let mut iter = args.into_iter();
    let StoredValue::Text(text) = iter.next().unwrap() else {
        return Ok(StoredValue::Null);
    };
    let StoredValue::Text(delimiter) = iter.next().unwrap() else {
        return Ok(StoredValue::Null);
    };
    let field_num = match iter.next().unwrap() {
        StoredValue::Int32(value) => i64::from(value),
        StoredValue::Int64(value) => value,
        _ => return Ok(StoredValue::Null),
    };
    Ok(StoredValue::Text(split_part_impl(
        &text, &delimiter, field_num,
    )?))
}

pub(in super::super) fn split_part_impl(
    text: &str,
    delimiter: &str,
    field_num: i64,
) -> Result<String> {
    if field_num == 0 {
        return Err(Error::Other("field position must not be zero".to_owned()));
    }
    if delimiter.is_empty() {
        let result = if matches!(field_num, 1 | -1) {
            text
        } else {
            ""
        };
        check_output_byte_size(result.len())?;
        return Ok(result.to_owned());
    }

    let result = if field_num > 0 {
        let index = usize::try_from(field_num - 1).unwrap_or(usize::MAX);
        text.split(delimiter).nth(index).unwrap_or("")
    } else {
        // PostgreSQL 14+ supports negative field positions from the end.
        let index =
            usize::try_from(field_num.unsigned_abs().saturating_sub(1)).unwrap_or(usize::MAX);
        text.rsplit(delimiter).nth(index).unwrap_or("")
    };
    check_output_byte_size(result.len())?;
    Ok(result.to_owned())
}

pub(in super::super) fn db9_like_match(
    source: &str,
    pattern: &str,
    escape_char: Option<char>,
    case_insensitive: bool,
) -> Result<bool> {
    db9_like_match_impl(source, pattern, escape_char, case_insensitive)
}

fn db9_like_char_eq(left: char, right: char, case_insensitive: bool) -> bool {
    if left == right {
        return true;
    }
    if !case_insensitive {
        return false;
    }
    simple_unicode_lower_char(left) == simple_unicode_lower_char(right)
}

pub(in super::super) fn db9_like_match_impl(
    source: &str,
    pattern: &str,
    escape_char: Option<char>,
    case_insensitive: bool,
) -> Result<bool> {
    #[inline]
    fn next_char_at(value: &str, idx: usize) -> Option<(char, usize)> {
        let ch = value[idx..].chars().next()?;
        Some((ch, idx + ch.len_utf8()))
    }

    let mut source_idx = 0usize;
    let mut pattern_idx = 0usize;
    let mut backtrack_pattern: Option<usize> = None;
    let mut backtrack_source = 0usize;

    while source_idx < source.len() {
        if pattern_idx < pattern.len() {
            let (pattern_ch, pattern_next) =
                next_char_at(pattern, pattern_idx).expect("pattern_idx < len");

            if escape_char.is_some_and(|escape| pattern_ch == escape) {
                let Some((literal, pattern_after)) = next_char_at(pattern, pattern_next) else {
                    return Err(Error::Other(
                        "LIKE pattern must not end with escape character".to_owned(),
                    ));
                };
                if let Some((source_ch, source_next)) = next_char_at(source, source_idx) {
                    if db9_like_char_eq(source_ch, literal, case_insensitive) {
                        source_idx = source_next;
                        pattern_idx = pattern_after;
                        continue;
                    }
                }
            } else if pattern_ch == '%' {
                let mut pattern_after = pattern_next;
                while pattern_after < pattern.len() {
                    let (next_pattern_ch, next_next) =
                        next_char_at(pattern, pattern_after).expect("pattern_after < len");
                    if next_pattern_ch != '%' {
                        break;
                    }
                    pattern_after = next_next;
                }
                backtrack_pattern = Some(pattern_after);
                backtrack_source = source_idx;
                pattern_idx = pattern_after;
                continue;
            } else if pattern_ch == '_' {
                if let Some((_source_ch, source_next)) = next_char_at(source, source_idx) {
                    source_idx = source_next;
                    pattern_idx = pattern_next;
                    continue;
                }
            } else if let Some((source_ch, source_next)) = next_char_at(source, source_idx) {
                if db9_like_char_eq(source_ch, pattern_ch, case_insensitive) {
                    source_idx = source_next;
                    pattern_idx = pattern_next;
                    continue;
                }
            }
        }

        if let Some(pattern_after_percent) = backtrack_pattern {
            if backtrack_source < source.len() {
                let (_source_ch, source_next) =
                    next_char_at(source, backtrack_source).expect("backtrack_source < len");
                backtrack_source = source_next;
                source_idx = backtrack_source;
                pattern_idx = pattern_after_percent;
                continue;
            }
        }

        return Ok(false);
    }

    while pattern_idx < pattern.len() {
        let (pattern_ch, pattern_next) = next_char_at(pattern, pattern_idx).expect("pattern_idx");
        if escape_char.is_some_and(|escape| pattern_ch == escape) {
            return Ok(false);
        }
        if pattern_ch == '%' {
            pattern_idx = pattern_next;
            continue;
        }
        return Ok(false);
    }

    Ok(true)
}

pub(in super::super) fn eval_like_function(
    function_name: &str,
    args: Vec<StoredValue>,
    case_insensitive: bool,
    negate: bool,
) -> Result<StoredValue> {
    if !(2..=3).contains(&args.len()) {
        return Err(Error::Other(format!(
            "DB9 function '{}' expects 2 or 3 args, got {}",
            function_name,
            args.len()
        )));
    }

    let mut iter = args.into_iter();
    let source = text_arg(iter.next().unwrap())?;
    let Some(pattern) = text_arg(iter.next().unwrap())? else {
        return Ok(StoredValue::Null);
    };
    // PostgreSQL raises `invalid escape string` for malformed ESCAPE values
    // even when the source input is NULL, but still lets a NULL pattern
    // short-circuit.
    let escape_char = match iter.next() {
        None => Some('\\'),
        Some(StoredValue::Null) => return Ok(StoredValue::Null),
        Some(StoredValue::Text(value)) => {
            if value.chars().count() > 1 {
                return Err(Error::Other("invalid escape string".to_owned()));
            }
            value.chars().next()
        }
        Some(other) => {
            let value = stored_value_to_string(&other)?;
            if value.chars().count() > 1 {
                return Err(Error::Other("invalid escape string".to_owned()));
            }
            value.chars().next()
        }
    };
    let Some(source) = source else {
        return Ok(StoredValue::Null);
    };
    let matched = db9_like_match(&source, &pattern, escape_char, case_insensitive)?;
    Ok(StoredValue::Boolean(if negate {
        !matched
    } else {
        matched
    }))
}

fn text_arg(value: StoredValue) -> Result<Option<String>> {
    match value {
        StoredValue::Null => Ok(None),
        StoredValue::Text(value) => Ok(Some(value)),
        other => stored_value_to_string(&other).map(Some),
    }
}

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

    #[test]
    fn bounded_text_functions_check_output_size_before_returning() {
        let err = super::super::super::with_db9_string_output_byte_limit(2, || {
            eval_left_function(
                "left",
                vec![
                    StoredValue::Text("abcdef".to_owned()),
                    StoredValue::Int32(3),
                ],
            )
        })
        .unwrap_err();
        assert_eq!(err.to_string(), "requested length too large");

        let err = super::super::super::with_db9_string_output_byte_limit(2, || {
            eval_substring_function(
                "substring",
                vec![
                    StoredValue::Text("abcdef".to_owned()),
                    StoredValue::Int32(2),
                    StoredValue::Int32(3),
                ],
            )
        })
        .unwrap_err();
        assert_eq!(err.to_string(), "requested length too large");

        let err = super::super::super::with_db9_string_output_byte_limit(2, || {
            eval_substring_function(
                "substring",
                vec![
                    StoredValue::Bytes(b"abcdef".to_vec()),
                    StoredValue::Int32(2),
                    StoredValue::Int32(3),
                ],
            )
        })
        .unwrap_err();
        assert_eq!(err.to_string(), "requested length too large");

        let err = super::super::super::with_db9_string_output_byte_limit(2, || {
            split_part_impl("a,big", ",", 2)
        })
        .unwrap_err();
        assert_eq!(err.to_string(), "requested length too large");

        let err = super::super::super::with_db9_string_output_byte_limit(2, || {
            eval_right_function(
                "right",
                vec![
                    StoredValue::Text("abcdef".to_owned()),
                    StoredValue::Int32(3),
                ],
            )
        })
        .unwrap_err();
        assert_eq!(err.to_string(), "requested length too large");

        let err = super::super::super::with_db9_string_output_byte_limit(2, || {
            eval_trim_function(
                "trim",
                vec![StoredValue::Text("abcdef".to_owned())],
                TrimSide::Both,
            )
        })
        .unwrap_err();
        assert_eq!(err.to_string(), "requested length too large");

        let err = super::super::super::with_db9_string_output_byte_limit(2, || {
            eval_reverse_function("reverse", vec![StoredValue::Text("abcdef".to_owned())])
        })
        .unwrap_err();
        assert_eq!(err.to_string(), "requested length too large");
    }
}
