use crate::util::PartialBuffer;
use std::io;

#[derive(Debug, Default)]
struct Flags {
    ascii: bool,
    crc: bool,
    extra: bool,
    filename: bool,
    comment: bool,
}

#[derive(Debug, Default)]
pub(super) struct Header {
    flags: Flags,
}

#[derive(Debug)]
enum State {
    Fixed(PartialBuffer<[u8; 10]>),
    ExtraLen(PartialBuffer<[u8; 2]>),
    Extra(PartialBuffer<Vec<u8>>),
    Filename(Vec<u8>),
    Comment(Vec<u8>),
    Crc(PartialBuffer<[u8; 2]>),
    Done,
}

impl Default for State {
    fn default() -> Self {
        State::Fixed(<_>::default())
    }
}

#[derive(Debug, Default)]
pub(super) struct Parser {
    state: State,
    header: Header,
}

impl Header {
    fn parse(input: &[u8; 10]) -> io::Result<Self> {
        if input[0..3] != [0x1f, 0x8b, 0x08] {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "Invalid gzip header",
            ));
        }

        let flag = input[3];

        let flags = Flags {
            ascii: (flag & 0b0000_0001) != 0,
            crc: (flag & 0b0000_0010) != 0,
            extra: (flag & 0b0000_0100) != 0,
            filename: (flag & 0b0000_1000) != 0,
            comment: (flag & 0b0001_0000) != 0,
        };

        Ok(Header { flags })
    }
}

impl Parser {
    pub(super) fn input(
        &mut self,
        input: &mut PartialBuffer<impl AsRef<[u8]>>,
    ) -> io::Result<Option<Header>> {
        loop {
            match &mut self.state {
                State::Fixed(data) => {
                    data.copy_unwritten_from(input);

                    if data.unwritten().is_empty() {
                        self.header = Header::parse(&data.take().into_inner())?;
                        self.state = State::ExtraLen(<_>::default());
                    } else {
                        return Ok(None);
                    }
                }

                State::ExtraLen(data) => {
                    if !self.header.flags.extra {
                        self.state = State::Filename(<_>::default());
                        continue;
                    }

                    data.copy_unwritten_from(input);

                    if data.unwritten().is_empty() {
                        let len = u16::from_le_bytes(data.take().into_inner());
                        self.state = State::Extra(vec![0; usize::from(len)].into());
                    } else {
                        return Ok(None);
                    }
                }

                State::Extra(data) => {
                    data.copy_unwritten_from(input);

                    if data.unwritten().is_empty() {
                        self.state = State::Filename(<_>::default());
                    } else {
                        return Ok(None);
                    }
                }

                State::Filename(data) => {
                    if !self.header.flags.filename {
                        self.state = State::Comment(<_>::default());
                        continue;
                    }

                    if let Some(len) = memchr::memchr(0, input.unwritten()) {
                        data.extend_from_slice(&input.unwritten()[..len]);
                        input.advance(len + 1);
                        self.state = State::Comment(<_>::default());
                    } else {
                        data.extend_from_slice(input.unwritten());
                        input.advance(input.unwritten().len());
                        return Ok(None);
                    }
                }

                State::Comment(data) => {
                    if !self.header.flags.comment {
                        self.state = State::Crc(<_>::default());
                        continue;
                    }

                    if let Some(len) = memchr::memchr(0, input.unwritten()) {
                        data.extend_from_slice(&input.unwritten()[..len]);
                        input.advance(len + 1);
                        self.state = State::Crc(<_>::default());
                    } else {
                        data.extend_from_slice(input.unwritten());
                        input.advance(input.unwritten().len());
                        return Ok(None);
                    }
                }

                State::Crc(data) => {
                    if !self.header.flags.crc {
                        self.state = State::Done;
                        return Ok(Some(std::mem::take(&mut self.header)));
                    }

                    data.copy_unwritten_from(input);

                    if data.unwritten().is_empty() {
                        self.state = State::Done;
                        return Ok(Some(std::mem::take(&mut self.header)));
                    } else {
                        return Ok(None);
                    }
                }

                State::Done => {
                    return Err(io::Error::other("parser used after done"));
                }
            };
        }
    }
}
