use crate::parser::{CommentParser, ElementParser, Parser, PiParser};

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DtdParser {
    /// If inside a PubidLiteral or SystemLiteral, it holds the quote type (either `'` or `"`).
    /// Otherwise, it holds `0` (this is an initial state).
    ///
    /// ```text
    /// [28]    doctypedecl     ::=   '<!DOCTYPE' S Name (S ExternalID)? S? ('[' intSubset ']' S?)? '>'
    /// ```
    BeforeInternalSubset(u8),
    /// Inside of the `intSubset` rule.
    ///
    /// ```text
    /// [28a]   DeclSep         ::=   PEReference | S
    /// [28b]   intSubset       ::=   (markupdecl | DeclSep)*
    /// [29]    markupdecl      ::=   elementdecl | AttlistDecl | EntityDecl | NotationDecl | PI | Comment
    /// ```
    InsideOfInternalSubset,
    /// After `]` but before `>`.
    AfterInternalSubset,
    InComment(CommentParser),
    InPi(PiParser),
    /// ```text
    /// [45]    elementdecl     ::=   '<!ELEMENT' S Name S contentspec S? '>'
    /// ```
    InElementDecl,
    /// This state handles ATTLIST, ENTITY and NOTATION elements, i.e. all elements that can have
    /// quotes strings (`'...'` or `"..."`) inside their markup, in which `>` should not be threated
    /// as the end of the markup.
    ///
    /// This state handles the following productions from XML grammar:
    ///
    /// ### ATTLIST
    ///
    /// ```text
    /// [52]    AttlistDecl     ::=   '<!ATTLIST' S Name AttDef* S? '>'
    /// [53]    AttDef          ::=   S Name S AttType S DefaultDecl
    /// [60]    DefaultDecl     ::=   '#REQUIRED' | '#IMPLIED' | (('#FIXED' S)? AttValue)
    /// ```
    ///
    /// ### ENTITY
    ///
    /// ```text
    /// [70]    EntityDecl      ::=   GEDecl | PEDecl
    /// [71]    GEDecl          ::=   '<!ENTITY' S Name S EntityDef S? '>'
    /// [72]    PEDecl          ::=   '<!ENTITY' S '%' S Name S PEDef S? '>'
    /// [73]    EntityDef       ::=   EntityValue | (ExternalID NDataDecl?)
    /// [74]    PEDef           ::=   EntityValue | ExternalID
    /// [75]    ExternalID      ::=   'SYSTEM' S SystemLiteral | 'PUBLIC' S PubidLiteral S SystemLiteral
    /// [76]    NDataDecl       ::=   S 'NDATA' S Name
    /// ```
    ///
    /// ### NOTATION
    ///
    /// ```text
    /// [82]    NotationDecl    ::=   '<!NOTATION' S Name S (ExternalID | PublicID) S? '>'
    /// ```
    InQuoteSensitive(ElementParser),
    /// The state where it was not possible to determine which markup it was during the previous iteration.  \
    /// It holds the number of bytes read since the start of the markup.
    UndecidedMarkup(usize),
    Finished,
}

impl DtdParser {
    /// Skip DTD contents.
    ///
    /// # Parameters (as same as `reader::BangType::parse`)
    /// - `buf`: buffer with data consumed on previous iterations
    /// - `chunk`: data read on current iteration and not yet consumed from reader
    pub fn feed(&mut self, buf: &[u8], chunk: &[u8]) -> Option<usize> {
        // This method assumes the DTD is well-formed.
        // Since this crate does not support parsing DTDs, the inability to read non-well-formed DTDs
        // is not particularly problematic; the only point of interest is reporting well-formed DTDs
        // to the user without errors.

        let mut cur = chunk;
        while !cur.is_empty() {
            match *self {
                Self::BeforeInternalSubset(0) => {
                    // Find the
                    // - start of quoted string ('...' or "...")
                    // - start of internal subset ([...])
                    // - end of DOCTYPE declaration (>)
                    if let Some(i) = cur
                        .iter()
                        .position(|&b| matches!(b, b'\'' | b'"' | b'[' | b'>'))
                    {
                        let b = cur[i];
                        match b {
                            b'\'' | b'"' => {
                                // SystemLiteral or PubidLiteral
                                *self = Self::BeforeInternalSubset(b);
                                cur = &cur[i + 1..]; // +1 to skip `'` or `"`
                                continue;
                            }
                            b'[' => {
                                *self = Self::InsideOfInternalSubset;
                                cur = &cur[i + 1..]; // +1 to skip `[`
                                continue;
                            }
                            b'>' => {
                                *self = Self::Finished;
                                return Some(chunk.len() - cur.len() + i);
                            }
                            _ => {}
                        }
                        continue;
                    }
                    break;
                }
                // Inside the quoted string (this is PubidLiteral or SystemLiteral) we do not want to
                // recognize other special characters (namely [ and >). Find only the closing quote
                Self::BeforeInternalSubset(quote) => {
                    // ExternalID handling
                    if let Some(i) = memchr::memchr(quote, cur) {
                        *self = Self::BeforeInternalSubset(0);
                        cur = &cur[i + 1..];
                        continue;
                    }
                    break;
                }
                Self::InsideOfInternalSubset => {
                    // Find the end of internal subset (]) or the start of the markup inside (<)
                    if let Some(i) = memchr::memchr2(b']', b'<', cur) {
                        if cur[i] == b']' {
                            *self = Self::AfterInternalSubset;
                            cur = &cur[i + 1..]; // +1 to skip `]`
                            continue;
                        }
                        // +1 to start after `<`
                        if let Some(skip) = self.switch(&cur[i + 1..]) {
                            cur = &cur[i + 1 + skip..]; // +1 to skip `<`
                            continue;
                        }
                        // Keep the number of already looked bytes (started from byte after `<`, so -1),
                        // try to decide after feeding the new chunk.
                        let skipped = cur.len() - i - 1;
                        // The 9-byte work buffer in `UndecidedMarkup` is sized
                        // for `!NOTATION` (the longest keyword). If the chunk
                        // already gave us 9+ bytes after `<` and `switch()`
                        // returned `None`, the markup is definitively not one
                        // of `<!--`, `<![CDATA[`, `<!ELEMENT`, `<!ATTLIST`,
                        // `<!ENTITY`, `<!NOTATION`, so skip until `>` rather
                        // than staging more bytes than the buffer can hold
                        // (which would panic on the slice copy in
                        // `UndecidedMarkup`).
                        if skipped >= 9 {
                            cur = &cur[i + 1..];
                            *self = Self::InElementDecl;
                            continue;
                        }
                        *self = Self::UndecidedMarkup(skipped);
                    }
                    break;
                }
                Self::AfterInternalSubset => {
                    if let Some(i) = memchr::memchr(b'>', cur) {
                        *self = Self::Finished;
                        return Some(chunk.len() - cur.len() + i);
                    }
                    break;
                }
                Self::InComment(ref mut parser) => {
                    // If comment is ended, return to the main state, otherwise keep in the current state
                    if let Some(i) = parser.feed(cur) {
                        *self = Self::InsideOfInternalSubset;
                        cur = &cur[i..];
                        continue;
                    }
                    break;
                }
                Self::InPi(ref mut parser) => {
                    // If processing instruction is ended, return to the main state,
                    // otherwise keep in the current state
                    if let Some(i) = parser.feed(cur) {
                        *self = Self::InsideOfInternalSubset;
                        cur = &cur[i..];
                        continue;
                    }
                    break;
                }
                Self::InElementDecl => {
                    // `<!ELEMENT >` does not have places where `>` could be escaped
                    // so the first occurrence ends that state
                    if let Some(i) = memchr::memchr(b'>', cur) {
                        *self = Self::InsideOfInternalSubset;
                        cur = &cur[i + 1..]; // +1 for `>`
                        continue;
                    }
                    break;
                }
                Self::InQuoteSensitive(ref mut parser) => {
                    // If ATTLIST, ENTITY or NOTATION is ended, return to the main state,
                    // otherwise keep in the current state
                    if let Some(i) = parser.feed(cur) {
                        *self = Self::InsideOfInternalSubset;
                        cur = &cur[i..];
                        continue;
                    }
                    break;
                }
                Self::UndecidedMarkup(skipped) => {
                    // Buffer is long enough to store the longest possible keyword `!NOTATION`
                    let mut bytes = [0u8; 9];

                    // Copy the last `skipped` bytes from the previous iteration into buffer,
                    // for example, "!NOT" (skipped = 4 in that case)...
                    bytes[..skipped].copy_from_slice(&buf[buf.len() - skipped..]);

                    // ...add new bytes to the buffer from current iteration,
                    // for example, "ATION"...
                    let end = bytes.len().min(skipped + cur.len());
                    bytes[skipped..end].copy_from_slice(&cur[..end - skipped]);

                    // ...and try to match over it.
                    // For example, "!NOTATION" will return 9, and we skip 9-4=5 bytes of "ATION"
                    if let Some(skip) = self.switch(&bytes[..end]) {
                        cur = &cur[skip - skipped..];
                        continue;
                    }
                    // No keyword matched. If we have a full 9-byte window the
                    // markup is definitively not one of `<!--`, `<![CDATA[`,
                    // `<!ELEMENT`, `<!ATTLIST`, `<!ENTITY`, `<!NOTATION`, so
                    // fall back to skipping until the closing `>` instead of
                    // accumulating `skipped` past `bytes.len()` (which would
                    // panic on the slice-copy above on a later iteration).
                    if end == bytes.len() {
                        cur = &cur[end - skipped..];
                        *self = Self::InElementDecl;
                        continue;
                    }
                    *self = Self::UndecidedMarkup(skipped + cur.len());
                    break;
                }
                Self::Finished => break,
            }
        }

        None
    }

    #[inline]
    fn switch(&mut self, markup: &[u8]) -> Option<usize> {
        match markup {
            [b'?', ..] => {
                // <?
                *self = Self::InPi(PiParser(false));
                Some(1)
            }
            [b'!', b'-', b'-', ..] => {
                // <!--
                *self = Self::InComment(CommentParser::Seen0);
                Some(3)
            }
            [b'!', b'E', b'L', b'E', b'M', b'E', b'N', b'T', ..] => {
                // <!ELEMENT
                *self = Self::InElementDecl;
                Some(8)
            }
            [b'!', b'E', b'N', b'T', b'I', b'T', b'Y', ..] => {
                // <!ENTITY
                *self = Self::InQuoteSensitive(ElementParser::Outside);
                Some(7)
            }
            [b'!', b'A', b'T', b'T', b'L', b'I', b'S', b'T', ..] => {
                // <!ATTLIST
                *self = Self::InQuoteSensitive(ElementParser::Outside);
                Some(8)
            }
            [b'!', b'N', b'O', b'T', b'A', b'T', b'I', b'O', b'N', ..] => {
                // <!NOTATION
                *self = Self::InQuoteSensitive(ElementParser::Outside);
                Some(9)
            }
            // <... - `markup` does not have enough data to determine markup
            // or markup is not known.
            // Undecided markup bytes will be written to `buf` to be available on
            // next iteration.
            _ => {
                // FIXME: to correctly report error position in DTD we need to provide
                // DTD events. For now our task just to skip (correct) DTD, so we postpone
                // error reporting and go with ending the unknown markup with `>`.
                if let Some(i) = memchr::memchr(b'>', markup) {
                    *self = Self::InsideOfInternalSubset;
                    Some(i + 1) // +1 to skip `>`
                } else {
                    None
                }
            }
        }
    }
}
