// License: see LICENSE file at root directory of `master` branch

use {
    alloc::{
        string::{String, ToString},
    },
    core::{
        cmp::Ordering,
        iter::Skip,
        str::{CharIndices, FromStr},
    },

    crate::{ParseSemverError, Result, Semver, SemverErrorKind},
};

/// # Parser
pub (crate) struct Parser<'a> {

    /// # String to be parsed
    src: &'a str,

    /// # Strict mode
    strict: bool,

    /// # Chars of source string
    chars: Skip<CharIndices<'a>>,

}

impl<'a> Parser<'a> {

    /// # Parses input string
    pub (crate) fn parse(src: &'a str, strict: bool) -> Result<Semver> {
        let src = match strict {
            true => src,
            false => src.trim(),
        };

        match src.len() {
            0 => return Err(ParseSemverError { kind: SemverErrorKind::Empty }),
            other => if other.cmp(&Semver::MAX_INPUT_STR_LEN.into()) == Ordering::Greater {
                return Err(ParseSemverError { kind: SemverErrorKind::TooLarge });
            },
        };

        let mut parser = Self {
            src: src,
            strict,
            chars: src.char_indices().skip(0),
        };

        // Major, minor, patch
        let major = parser.parse_major_minor_patch_version(SemverErrorKind::InvalidMajor)?;
        let minor = parser.parse_major_minor_patch_version(SemverErrorKind::InvalidMinor)?;
        let patch = parser.parse_major_minor_patch_version(SemverErrorKind::InvalidPatch)?;

        // Pre-release (optional)
        let pre_release = parser.parse_pre_release_or_build_metadata(super::PRE_RELEASE_STARTER);
        match pre_release {
            Ok(_) => (),
            // Starter can be BUILD_METADATA_STARTER, we'll handle this error later
            Err(ParseSemverError { kind: SemverErrorKind::InvalidToken }) => (),
            Err(err) => return Err(err),
        };

        // Build metadata (optional)
        let build_metadata = parser.parse_pre_release_or_build_metadata(super::BUILD_METADATA_STARTER)?;

        // Pre-release
        let pre_release = {
            match pre_release {
                Ok(pre_release) => pre_release,
                // If there's no build metadata, pre-release must be some string or None -- otherwise it's invalid.
                Err(ParseSemverError { kind: SemverErrorKind::InvalidToken }) => match build_metadata {
                    Some(_) => None,
                    None => return Err(ParseSemverError { kind: SemverErrorKind::InvalidToken }),
                },
                Err(err) => return Err(err),
            }
        };

        Ok(Semver {
            major: major, minor: minor, patch: patch,
            pre_release: pre_release, build_metadata: build_metadata,
        })
    }

    /// # Parses major/minor/patch version
    fn parse_major_minor_patch_version(&mut self, err_kind: SemverErrorKind) -> Result<u64> {
        let err = |err_kind| Err(ParseSemverError { kind: err_kind });

        let mut skip_last_char = false;
        let mut start_index: Option<usize> = None;
        let mut end_index: Option<usize> = None;
        let mut invalid_char_index: Option<usize> = None;
        let mut first_char_is_zero = false;

        for (index, chr) in &mut self.chars {
            match chr {
                '.' => match start_index {
                    Some(_) => { skip_last_char = true; break; },
                    None => return err(err_kind),
                },
                '0'..='9' => {
                    if let Some(start_index) = start_index {
                        if let Some(gap) = index.checked_sub(start_index) {
                            if (gap > 0 && first_char_is_zero) || gap.cmp(&super::MAX_STR_LEN_OF_A_VERSION_NUMBER) != Ordering::Less {
                                return err(err_kind);
                            }
                        }
                    } else {
                        start_index = Some(index);
                        first_char_is_zero = chr == '0';
                    }
                    end_index = Some(index);
                },
                _ => {
                    invalid_char_index = Some(index);
                    break;
                },
            };
        }

        match (start_index, end_index) {
            (Some(start_index), Some(end_index)) if end_index >= start_index => match u64::from_str(&self.src[start_index..=end_index]) {
                Ok(v) => {
                    self.chars = self.src.char_indices().skip(
                        end_index.checked_add(if skip_last_char { 2 } else { 1 }).unwrap_or(usize::max_value())
                    );
                    Ok(v)
                },
                Err(_) => err(err_kind),
            },
            _ => {
                if let Some(index) = invalid_char_index { self.chars = self.src.char_indices().skip(index); }
                match err_kind {
                    SemverErrorKind::InvalidMinor => match self.strict {
                        true => match invalid_char_index {
                            Some(_) => err(err_kind),
                            None => Err(ParseSemverError { kind: SemverErrorKind::MissingMinor }),
                        },
                        false => Ok(0),
                    },
                    SemverErrorKind::InvalidPatch => match self.strict {
                        true => match invalid_char_index {
                            Some(_) => err(err_kind),
                            None => Err(ParseSemverError { kind: SemverErrorKind::MissingPatch }),
                        },
                        false => Ok(0),
                    },
                    _ => err(err_kind),
                }
            },
        }
    }

    /// # Parses pre-release or build metadata
    ///
    /// `starter`: can be [`PRE_RELEASE_STARTER`][const:PRE_RELEASE_STARTER] or [`BUILD_METADATA_STARTER`][const:BUILD_METADATA_STARTER].
    ///
    /// [const:PRE_RELEASE_STARTER]: ../constant.PRE_RELEASE_STARTER.html
    /// [const:BUILD_METADATA_STARTER]: ../constant.BUILD_METADATA_STARTER.html
    fn parse_pre_release_or_build_metadata(&mut self, starter: char) -> Result<Option<String>> {
        // Check starter and setup error kind
        let err_kind = match starter {
            super::PRE_RELEASE_STARTER => SemverErrorKind::InvalidPreRelease,
            super::BUILD_METADATA_STARTER => SemverErrorKind::InvalidBuildMetadata,
            _ => return Err(ParseSemverError{ kind: SemverErrorKind::ParserError }),
        };
        let err = || Err(ParseSemverError { kind: err_kind });

        let mut start_index: Option<usize> = None;
        let mut end_index: Option<usize> = None;
        let mut first_char_is_zero: Option<bool> = None;
        let mut last_char: Option<char> = None;
        let mut all_are_numbers = false;

        // Check token
        match self.chars.next() {
            Some((index, chr)) => if chr != starter {
                self.chars = self.src.char_indices().skip(index);
                return Err(ParseSemverError{ kind: SemverErrorKind::InvalidToken });
            },
            None => return Ok(None),
        };

        for (index, chr) in &mut self.chars {
            if start_index.is_none() { start_index = Some(index); }

            // This is for section 9: a pre-release numeric identifier MUST NOT include leading zeroes
            if starter == super::PRE_RELEASE_STARTER && first_char_is_zero.is_none() {
                if chr == '.' { return err(); }
                first_char_is_zero = Some(chr == '0');
                all_are_numbers = true;
            }

            match chr {
                'a'..='z' | 'A'..='Z' | '-' => {
                    end_index = Some(index);
                    all_are_numbers = false;
                },
                '0'..='9' => end_index = Some(index),
                // Pre-release is allowed to have build metadata go after it. But build metadata is not.
                super::BUILD_METADATA_STARTER => if starter == super::PRE_RELEASE_STARTER { break; } else { return err(); },
                '.' => {
                    if let Some(true) = first_char_is_zero {
                        if all_are_numbers { return err(); }
                        first_char_is_zero = None;
                    }
                    match last_char {
                        Some(c) => match c {
                            '.' | '-' => return err(),
                            _ => last_char = None,
                        },
                        None => return err(),
                    };
                    continue;
                },
                _ => return err(),
            };

            last_char = Some(chr);
        }

        match (last_char, start_index, end_index) {
            (Some(last_char), Some(start_index), Some(end_index)) if last_char.is_ascii_alphanumeric() => {
                self.chars = self.src.char_indices().skip(end_index.checked_add(1).unwrap_or(usize::max_value()));
                Ok(Some(self.src[start_index..=end_index].to_string()))
            },
            _ => err(),
        }
    }

}
