use std::{
    borrow::Cow,
};

use base64::Engine;
use base64::engine::general_purpose::STANDARD as base64std;

use crate::{
    packet::Header,
};

/// Remove whitespace, etc. from the base64 data.
///
/// This function returns the filtered base64 data (i.e., stripped of
/// all skippable data like whitespace), and the amount of unfiltered
/// data that corresponds to.  Thus, if we have the following 7 bytes:
///
/// ```text
///     ab  cde
///     0123456
/// ```
///
/// This function returns ("abcd", 6), because the 'd' is the last
/// character in the last complete base64 chunk, and it is at offset 5.
///
/// If 'd' is followed by whitespace, it is undefined whether that
/// whitespace is included in the count.
///
/// This function only returns full chunks of base64 data.  As a
/// consequence, if base64_data_max is less than 4, then this will not
/// return any data.
///
/// This function will stop after it sees base64 padding, and if it
/// sees invalid base64 data.
pub fn base64_filter(mut bytes: Cow<[u8]>, base64_data_max: usize,
                     mut prefix_remaining: usize, prefix_len: usize)
    -> (Cow<[u8]>, usize, usize)
{
    let mut leading_whitespace = 0;

    // Round down to the nearest chunk size.
    let base64_data_max = base64_data_max / 4 * 4;

    // Number of bytes of base64 data.  Since we update `bytes` in
    // place, the base64 data is `&bytes[..base64_len]`.
    let mut base64_len = 0;

    // Offset of the next byte of unfiltered data to process.
    let mut unfiltered_offset = 0;

    // Offset of the last byte of the last ***complete*** base64 chunk
    // in the unfiltered data.
    let mut unfiltered_complete_len = 0;

    // Number of bytes of padding that we've seen so far.
    let mut padding = 0;

    while unfiltered_offset < bytes.len()
        && base64_len < base64_data_max
        // A valid base64 chunk never starts with padding.
        && ! (padding > 0 && base64_len % 4 == 0)
    {
        // If we have some prefix to skip, skip it.
        if prefix_remaining > 0 {
            prefix_remaining -= 1;
            if unfiltered_offset == 0 {
                match bytes {
                    Cow::Borrowed(s) => {
                        // We're at the beginning.  Avoid moving
                        // data by cutting off the start of the
                        // slice.
                        bytes = Cow::Borrowed(&s[1..]);
                        leading_whitespace += 1;
                        continue;
                    }
                    Cow::Owned(_) => (),
                }
            }
            unfiltered_offset += 1;
            continue;
        }
        match bytes[unfiltered_offset] {
            // White space.
            c if c.is_ascii_whitespace() => {
                if c == b'\n' {
                    prefix_remaining = prefix_len;
                }
                if unfiltered_offset == 0 {
                    match bytes {
                        Cow::Borrowed(s) => {
                            // We're at the beginning.  Avoid moving
                            // data by cutting off the start of the
                            // slice.
                            bytes = Cow::Borrowed(&s[1..]);
                            leading_whitespace += 1;
                            continue;
                        }
                        Cow::Owned(_) => (),
                    }
                }
            }

            // Padding.
            b'=' => {
                if padding == 2 {
                    // There can never be more than two bytes of
                    // padding.
                    break;
                }
                if base64_len % 4 == 0 {
                    // Padding can never occur at the start of a
                    // base64 chunk.
                    break;
                }

                if unfiltered_offset != base64_len {
                    bytes.to_mut()[base64_len] = b'=';
                }
                base64_len += 1;
                if base64_len % 4 == 0 {
                    unfiltered_complete_len = unfiltered_offset + 1;
                }
                padding += 1;
            }

            // The only thing that can occur after padding is
            // whitespace or padding.  Those cases were covered above.
            _ if padding > 0 => break,

            // Base64 data!
            b if is_base64_char(&b) => {
                if unfiltered_offset != base64_len {
                    bytes.to_mut()[base64_len] = b;
                }
                base64_len += 1;
                if base64_len % 4 == 0 {
                    unfiltered_complete_len = unfiltered_offset + 1;
                }
            }

            // Not base64 data.
            _ => break,
        }

        unfiltered_offset += 1;
    }

    let base64_len = base64_len - (base64_len % 4);
    unfiltered_complete_len += leading_whitespace;
    match bytes {
        Cow::Borrowed(s) =>
            (Cow::Borrowed(&s[..base64_len]), unfiltered_complete_len,
             prefix_remaining),
        Cow::Owned(mut v) => {
            crate::vec_truncate(&mut v, base64_len);
            (Cow::Owned(v), unfiltered_complete_len, prefix_remaining)
        }
    }
}

/// Checks whether the given bytes contain armored OpenPGP data.
pub fn is_armored_pgp_blob(bytes: &[u8]) -> bool {
    // Get up to 32 bytes of base64 data.  That's 24 bytes of data
    // (ignoring padding), which is more than enough to get the first
    // packet's header.
    let (bytes, _, _) = base64_filter(Cow::Borrowed(bytes), 32, 0, 0);

    match base64std.decode(bytes) {
        Ok(d) => {
            // Don't consider an empty message to be valid.
            if d.is_empty() {
                false
            } else {
                let mut br = buffered_reader::Memory::new(&d);
                if let Ok(header) = Header::parse(&mut br) {
                    header.ctb().tag().valid_start_of_message()
                        && header.valid(false).is_ok()
                } else {
                    false
                }
            }
        },
        Err(_err) => false,
    }
}

/// Checks whether the given byte is in the base64 character set.
pub fn is_base64_char(b: &u8) -> bool {
    b.is_ascii_alphanumeric() || *b == b'+' || *b == b'/'
}

/// Returns the number of bytes of base64 data are needed to encode
/// `s` bytes of raw data.
pub fn base64_size(s: usize) -> usize {
    (s + 3 - 1) / 3 * 4
}

#[test]
fn base64_size_test() {
    assert_eq!(base64_size(0), 0);
    assert_eq!(base64_size(1), 4);
    assert_eq!(base64_size(2), 4);
    assert_eq!(base64_size(3), 4);
    assert_eq!(base64_size(4), 8);
    assert_eq!(base64_size(5), 8);
    assert_eq!(base64_size(6), 8);
    assert_eq!(base64_size(7), 12);
}
